@iternio/react-native-auto-play 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/android/src/automotive/AndroidManifest.xml +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +8 -1
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +108 -38
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
- package/ios/extensions/NitroImageExtensions.swift +10 -1
- package/ios/hybrid/HybridAutoPlay.swift +51 -4
- package/ios/hybrid/HybridMapTemplate.swift +2 -5
- package/ios/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +55 -0
- package/ios/templates/Parser.swift +109 -11
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/templates/MapTemplate.d.ts +7 -1
- package/lib/templates/MapTemplate.js +10 -2
- package/lib/types/Image.d.ts +13 -0
- package/lib/types/Maneuver.d.ts +6 -0
- package/lib/utils/NitroImage.d.ts +6 -1
- package/lib/utils/NitroImage.js +7 -0
- package/lib/utils/NitroManeuver.d.ts +2 -0
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +8 -2
- package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
- package/nitrogen/generated/android/c++/JNitroImage.hpp +19 -2
- package/nitrogen/generated/android/c++/JNitroLoadingManeuver.hpp +15 -4
- package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
- package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
- package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MapTemplateConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroLoadingManeuver.kt +9 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/MapTemplateConfig.swift +12 -1
- package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
- package/nitrogen/generated/ios/swift/NitroLoadingManeuver.swift +25 -2
- package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
- package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
- package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
- package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/MapTemplateConfig.hpp +8 -1
- package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroLoadingManeuver.hpp +15 -4
- package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
- package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
- package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
- package/package.json +1 -1
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/templates/MapTemplate.ts +23 -2
- package/src/types/Image.ts +14 -0
- package/src/types/Maneuver.ts +6 -0
- package/src/utils/NitroImage.ts +15 -1
- package/src/utils/NitroManeuver.ts +2 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt
CHANGED
|
@@ -40,6 +40,7 @@ import com.facebook.datasource.DataSources
|
|
|
40
40
|
import com.facebook.drawee.backends.pipeline.Fresco
|
|
41
41
|
import com.facebook.imagepipeline.image.CloseableBitmap
|
|
42
42
|
import com.facebook.imagepipeline.image.CloseableXml
|
|
43
|
+
import com.facebook.imagepipeline.request.ImageRequest
|
|
43
44
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
|
44
45
|
import com.facebook.react.views.imagehelper.ImageSource
|
|
45
46
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.AndroidAutoScreen
|
|
@@ -67,10 +68,11 @@ import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroRow
|
|
|
67
68
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroSectionType
|
|
68
69
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OffRampType
|
|
69
70
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OnRampType
|
|
71
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.RemoteImage
|
|
70
72
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TrafficSide
|
|
71
73
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TravelEstimates
|
|
72
74
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TurnType
|
|
73
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.
|
|
75
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.Variant_GlyphImage_AssetImage_RemoteImage
|
|
74
76
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.BitmapCache
|
|
75
77
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.SymbolFont
|
|
76
78
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.get
|
|
@@ -79,6 +81,7 @@ import java.util.Calendar
|
|
|
79
81
|
import java.util.Locale
|
|
80
82
|
import java.util.TimeZone
|
|
81
83
|
import kotlin.math.abs
|
|
84
|
+
import androidx.core.net.toUri
|
|
82
85
|
|
|
83
86
|
object Parser {
|
|
84
87
|
const val TAG = "Parser"
|
|
@@ -210,27 +213,35 @@ object Parser {
|
|
|
210
213
|
}.build()
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
fun parseImage(context: CarContext, image:
|
|
214
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
216
|
+
fun parseImage(context: CarContext, image: Variant_GlyphImage_AssetImage_RemoteImage): CarIcon {
|
|
217
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
fun parseImage(context: CarContext, image: NitroImage): CarIcon {
|
|
218
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
221
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
fun parseImage(
|
|
222
|
-
|
|
224
|
+
fun parseImage(
|
|
225
|
+
context: CarContext,
|
|
226
|
+
glyphImage: GlyphImage?,
|
|
227
|
+
assetImage: AssetImage?,
|
|
228
|
+
remoteImage: RemoteImage?
|
|
229
|
+
): CarIcon {
|
|
230
|
+
val bitmap = parseImageToBitmap(context, glyphImage, assetImage, remoteImage)
|
|
223
231
|
|
|
224
232
|
bitmap?.let {
|
|
225
233
|
return CarIcon.Builder(IconCompat.createWithBitmap(it)).build()
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
//
|
|
229
|
-
return CarIcon.
|
|
236
|
+
// remote images might fail to load so we provide some placeholder then
|
|
237
|
+
return CarIcon.ALERT
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
fun parseImageToBitmap(
|
|
233
|
-
context: CarContext,
|
|
241
|
+
context: CarContext,
|
|
242
|
+
glyphImage: GlyphImage?,
|
|
243
|
+
assetImage: AssetImage?,
|
|
244
|
+
remoteImage: RemoteImage?
|
|
234
245
|
): Bitmap? {
|
|
235
246
|
glyphImage?.let {
|
|
236
247
|
return SymbolFont.imageFromNitroImage(
|
|
@@ -240,6 +251,9 @@ object Parser {
|
|
|
240
251
|
assetImage?.let {
|
|
241
252
|
return parseAssetImage(context, it)
|
|
242
253
|
}
|
|
254
|
+
remoteImage?.let {
|
|
255
|
+
return parseRemoteImage(context, it)
|
|
256
|
+
}
|
|
243
257
|
|
|
244
258
|
return null
|
|
245
259
|
}
|
|
@@ -251,10 +265,14 @@ object Parser {
|
|
|
251
265
|
fun imageFromNitroImages(
|
|
252
266
|
context: CarContext, images: List<NitroImage>
|
|
253
267
|
): IconCompat {
|
|
254
|
-
val bitmaps = images.
|
|
268
|
+
val bitmaps = images.mapNotNull {
|
|
255
269
|
parseImageToBitmap(
|
|
256
|
-
context, it.asFirstOrNull(), it.asSecondOrNull()
|
|
257
|
-
)
|
|
270
|
+
context, it.asFirstOrNull(), it.asSecondOrNull(), it.asThirdOrNull()
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (bitmaps.isEmpty()) {
|
|
275
|
+
return IconCompat.createWithBitmap(createBitmap(1, 1))
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
val height = bitmaps.maxOf { it.height }
|
|
@@ -512,54 +530,106 @@ object Parser {
|
|
|
512
530
|
}
|
|
513
531
|
|
|
514
532
|
fun parseAssetImage(context: CarContext, assetImage: AssetImage): Bitmap? {
|
|
515
|
-
|
|
533
|
+
BitmapCache.get(context, assetImage)?.let { return it }
|
|
516
534
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
535
|
+
val imageRequest = buildImageRequest(ImageSource(context, assetImage.uri).uri)
|
|
536
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = null) ?: return null
|
|
520
537
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
.
|
|
538
|
+
return applyTintAndCache(
|
|
539
|
+
context = context,
|
|
540
|
+
color = assetImage.color,
|
|
541
|
+
bitmap = bitmap,
|
|
542
|
+
cache = { result -> BitmapCache.put(context, assetImage, result) }
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fun parseRemoteImage(context: CarContext, remoteImage: RemoteImage): Bitmap? {
|
|
547
|
+
BitmapCache.get(context, remoteImage)?.let { return it }
|
|
548
|
+
|
|
549
|
+
val imageRequest = buildImageRequest(remoteImage.uri.toUri())
|
|
550
|
+
val timeoutMs = remoteImage.timeoutMs?.toLong() ?: 500L
|
|
551
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = timeoutMs) ?: return null
|
|
552
|
+
|
|
553
|
+
return applyTintAndCache(
|
|
554
|
+
context = context,
|
|
555
|
+
color = remoteImage.color,
|
|
556
|
+
bitmap = bitmap,
|
|
557
|
+
cache = { result -> BitmapCache.put(context, remoteImage, result) }
|
|
558
|
+
)
|
|
559
|
+
}
|
|
524
560
|
|
|
525
|
-
|
|
526
|
-
|
|
561
|
+
// Disable Fresco's own caching — BitmapCache handles all caching uniformly
|
|
562
|
+
private fun buildImageRequest(uri: android.net.Uri): ImageRequest =
|
|
563
|
+
ImageRequestBuilder.newBuilderWithSource(uri)
|
|
564
|
+
.disableDiskCache().disableMemoryCache().build()
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Fetches a bitmap via Fresco's image pipeline.
|
|
568
|
+
* When [timeoutMs] is provided, the fetch runs on a background thread and is abandoned on timeout.
|
|
569
|
+
* When `null`, the fetch runs synchronously (bundled assets, no network I/O).
|
|
570
|
+
*/
|
|
571
|
+
private fun fetchBitmap(
|
|
572
|
+
context: CarContext,
|
|
573
|
+
imageRequest: ImageRequest,
|
|
574
|
+
timeoutMs: Long?
|
|
575
|
+
): Bitmap? {
|
|
576
|
+
if (timeoutMs == null) return fetchSync(context, imageRequest)
|
|
577
|
+
|
|
578
|
+
var fetched: Bitmap? = null
|
|
579
|
+
val thread = Thread { fetched = fetchSync(context, imageRequest) }
|
|
580
|
+
thread.start()
|
|
581
|
+
thread.join(timeoutMs)
|
|
582
|
+
if (thread.isAlive) {
|
|
583
|
+
// Fresco's waitForFinalResult is not interruptible, but setting the flag
|
|
584
|
+
// lets any subsequent interruptible call exit and signals intent to exit.
|
|
585
|
+
thread.interrupt()
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
return fetched
|
|
589
|
+
}
|
|
527
590
|
|
|
591
|
+
private fun fetchSync(context: CarContext, imageRequest: ImageRequest): Bitmap? {
|
|
592
|
+
val dataSource = try {
|
|
593
|
+
Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
|
|
594
|
+
} catch (_: Exception) { return null }
|
|
595
|
+
val result = try {
|
|
596
|
+
DataSources.waitForFinalResult(dataSource)
|
|
597
|
+
} catch (_: Exception) { dataSource.close(); return null }
|
|
528
598
|
val image = result?.get()
|
|
529
599
|
try {
|
|
530
600
|
if (image is CloseableBitmap) {
|
|
531
|
-
|
|
601
|
+
return image.underlyingBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
|
532
602
|
} else if (image is CloseableXml) {
|
|
533
603
|
val drawable = image.buildDrawable()
|
|
534
|
-
|
|
535
|
-
width = image.width, height = image.height, Bitmap.Config.ARGB_8888
|
|
536
|
-
)
|
|
604
|
+
return drawable?.toBitmap(width = image.width, height = image.height, Bitmap.Config.ARGB_8888)
|
|
537
605
|
}
|
|
538
606
|
} finally {
|
|
539
607
|
image?.close()
|
|
540
608
|
result?.close()
|
|
541
609
|
dataSource.close()
|
|
542
610
|
}
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
543
613
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
614
|
+
private fun applyTintAndCache(
|
|
615
|
+
context: CarContext,
|
|
616
|
+
color: NitroColor?,
|
|
617
|
+
bitmap: Bitmap,
|
|
618
|
+
cache: (Bitmap) -> Unit
|
|
619
|
+
): Bitmap {
|
|
620
|
+
color?.get(context)?.let { tint ->
|
|
621
|
+
val tinted = createBitmap(bitmap.width, bitmap.height)
|
|
622
|
+
val canvas = Canvas(tinted)
|
|
551
623
|
val paint = Paint()
|
|
552
624
|
|
|
553
|
-
paint.colorFilter = PorterDuffColorFilter(
|
|
625
|
+
paint.colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN)
|
|
554
626
|
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
|
555
627
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return result
|
|
628
|
+
cache(tinted)
|
|
629
|
+
return tinted
|
|
559
630
|
}
|
|
560
631
|
|
|
561
|
-
|
|
562
|
-
|
|
632
|
+
cache(bitmap)
|
|
563
633
|
return bitmap
|
|
564
634
|
}
|
|
565
635
|
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt
CHANGED
|
@@ -6,6 +6,7 @@ import androidx.car.app.CarContext
|
|
|
6
6
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.AssetImage
|
|
7
7
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
|
|
8
8
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroColor
|
|
9
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.RemoteImage
|
|
9
10
|
|
|
10
11
|
object BitmapCache {
|
|
11
12
|
private val maxMemory = Runtime.getRuntime().maxMemory()
|
|
@@ -37,6 +38,16 @@ object BitmapCache {
|
|
|
37
38
|
put(key, bitmap)
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
fun get(context: CarContext, image: RemoteImage): Bitmap? {
|
|
42
|
+
val key = image.cacheKey(context)
|
|
43
|
+
return get(key)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun put(context: CarContext, image: RemoteImage, bitmap: Bitmap) {
|
|
47
|
+
val key = image.cacheKey(context)
|
|
48
|
+
put(key, bitmap)
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
private fun get(key: String): Bitmap? {
|
|
41
52
|
synchronized(bitmapCache) {
|
|
42
53
|
return bitmapCache.get(key)
|
|
@@ -56,5 +67,8 @@ fun NitroColor.get(context: CarContext): Int =
|
|
|
56
67
|
fun AssetImage.cacheKey(context: CarContext): String =
|
|
57
68
|
this.color?.let { "${this.uri}/${it.get(context)}" } ?: run { this.uri }
|
|
58
69
|
|
|
70
|
+
fun RemoteImage.cacheKey(context: CarContext): String =
|
|
71
|
+
this.color?.let { "${this.uri}/${it.get(context)}" } ?: run { this.uri }
|
|
72
|
+
|
|
59
73
|
fun GlyphImage.cacheKey(context: CarContext): String =
|
|
60
74
|
"$glyph/${color.get(context)}/${backgroundColor.get(context)}/$fontScale"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
protocol ImageProtocol {
|
|
9
9
|
var glyphImage: GlyphImage? { get }
|
|
10
10
|
var assetImage: AssetImage? { get }
|
|
11
|
+
var remoteImage: RemoteImage? { get }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
extension NitroImage: ImageProtocol {
|
|
@@ -19,9 +20,13 @@ extension NitroImage: ImageProtocol {
|
|
|
19
20
|
if case .second(let asset) = self { return asset }
|
|
20
21
|
return nil
|
|
21
22
|
}
|
|
23
|
+
var remoteImage: RemoteImage? {
|
|
24
|
+
if case .third(let remote) = self { return remote }
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
extension
|
|
29
|
+
extension Variant_GlyphImage_AssetImage_RemoteImage: ImageProtocol {
|
|
25
30
|
var glyphImage: GlyphImage? {
|
|
26
31
|
if case .first(let glyph) = self { return glyph }
|
|
27
32
|
return nil
|
|
@@ -30,4 +35,8 @@ extension Variant_GlyphImage_AssetImage: ImageProtocol {
|
|
|
30
35
|
if case .second(let asset) = self { return asset }
|
|
31
36
|
return nil
|
|
32
37
|
}
|
|
38
|
+
var remoteImage: RemoteImage? {
|
|
39
|
+
if case .third(let remote) = self { return remote }
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import AVFoundation
|
|
1
2
|
import CarPlay
|
|
2
3
|
import NitroModules
|
|
3
4
|
|
|
@@ -20,6 +21,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
20
21
|
private static var listeners = [EventName: [StateListener]]()
|
|
21
22
|
private static var renderStateListeners = [String: [RenderStateListener]]()
|
|
22
23
|
private static var safeAreaInsetsListeners = [String: [SafeAreaListener]]()
|
|
24
|
+
private static var voiceInputManager: VoiceInputManager?
|
|
23
25
|
|
|
24
26
|
override init() {
|
|
25
27
|
HybridAutoPlay.listeners.removeAll()
|
|
@@ -117,10 +119,55 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
117
119
|
func addListenerVoiceInput(
|
|
118
120
|
callback: @escaping (Location?, String?) -> Void
|
|
119
121
|
) throws -> () -> Void {
|
|
120
|
-
//
|
|
122
|
+
// iOS does not use the OS-triggered voice input path — use startVoiceInput() instead.
|
|
121
123
|
return {}
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
func hasVoiceInputPermission() throws -> Bool {
|
|
127
|
+
return AVAudioSession.sharedInstance().recordPermission == .granted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func requestVoiceInputPermission() throws -> Promise<Bool> {
|
|
131
|
+
return Promise.async {
|
|
132
|
+
return await withCheckedContinuation { cont in
|
|
133
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
134
|
+
cont.resume(returning: granted)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?) throws -> Promise<
|
|
141
|
+
ArrayBuffer
|
|
142
|
+
> {
|
|
143
|
+
return Promise.async {
|
|
144
|
+
let interfaceController = try? await RootModule.withInterfaceController { $0 }
|
|
145
|
+
|
|
146
|
+
let manager = VoiceInputManager()
|
|
147
|
+
HybridAutoPlay.voiceInputManager = manager
|
|
148
|
+
|
|
149
|
+
defer {
|
|
150
|
+
HybridAutoPlay.voiceInputManager = nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let data = try await manager.start(
|
|
154
|
+
interfaceController: interfaceController,
|
|
155
|
+
silenceThresholdMs: silenceThresholdMs ?? 1_500,
|
|
156
|
+
maxDurationMs: maxDurationMs ?? 10_000,
|
|
157
|
+
listeningText: listeningText ?? "Listening..."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return try ArrayBuffer.copy(data: data)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func stopVoiceInput() throws {
|
|
165
|
+
Task { @MainActor in
|
|
166
|
+
let interfaceController = try? await RootModule.withInterfaceController { $0 }
|
|
167
|
+
HybridAutoPlay.voiceInputManager?.stop(interfaceController: interfaceController)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
124
171
|
// MARK: set/push/pop templates
|
|
125
172
|
func setRootTemplate(templateId: String) throws -> Promise<Void> {
|
|
126
173
|
return Promise.async {
|
|
@@ -151,7 +198,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
151
198
|
}
|
|
152
199
|
|
|
153
200
|
func pushTemplate(templateId: String) throws
|
|
154
|
-
->
|
|
201
|
+
-> Promise<Void>
|
|
155
202
|
{
|
|
156
203
|
return Promise.async {
|
|
157
204
|
try await RootModule.withSceneAndInterfaceController {
|
|
@@ -201,7 +248,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
250
|
|
|
204
|
-
func popTemplate(animate: Bool?) throws ->
|
|
251
|
+
func popTemplate(animate: Bool?) throws -> Promise<Void> {
|
|
205
252
|
return Promise.async {
|
|
206
253
|
try await RootModule.withInterfaceController {
|
|
207
254
|
interfaceController in
|
|
@@ -222,7 +269,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
222
269
|
}
|
|
223
270
|
}
|
|
224
271
|
|
|
225
|
-
func popToRootTemplate(animate: Bool?) throws ->
|
|
272
|
+
func popToRootTemplate(animate: Bool?) throws -> Promise<Void> {
|
|
226
273
|
return Promise.async {
|
|
227
274
|
try await RootModule.withInterfaceController {
|
|
228
275
|
interfaceController in
|
|
@@ -138,12 +138,9 @@ class HybridMapTemplate: HybridMapTemplateSpec {
|
|
|
138
138
|
{
|
|
139
139
|
template.updateManeuvers(messageManeuver: messageManeuver)
|
|
140
140
|
}()
|
|
141
|
-
case .third(let
|
|
141
|
+
case .third(let loading):
|
|
142
142
|
{
|
|
143
|
-
template.
|
|
144
|
-
for: .loading,
|
|
145
|
-
description: nil
|
|
146
|
-
)
|
|
143
|
+
template.updateManeuversLoading(loading: loading)
|
|
147
144
|
}()
|
|
148
145
|
}
|
|
149
146
|
|
|
@@ -69,6 +69,13 @@ class GridTemplate: AutoPlayHeaderProviding {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
if let remoteImage = button.image.remoteImage {
|
|
73
|
+
image = Parser.parseRemoteImage(
|
|
74
|
+
remoteImage: remoteImage,
|
|
75
|
+
traitCollection: traitCollection
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
guard let image = image else { return nil }
|
|
73
80
|
guard let title = Parser.parseText(text: button.title) else { return nil }
|
|
74
81
|
|
|
@@ -50,6 +50,18 @@ class MapTemplate: AutoPlayHeaderProviding,
|
|
|
50
50
|
visibleTravelEstimate = config.visibleTravelEstimate
|
|
51
51
|
|
|
52
52
|
template = CPMapTemplate(id: config.id)
|
|
53
|
+
if let nitroColor = config.defaultGuidanceBackgroundColor,
|
|
54
|
+
let traitCollection = SceneStore.getRootTraitCollection()
|
|
55
|
+
{
|
|
56
|
+
let cardBackgroundColor = Parser.routingManeuverCardBackgroundUIColor(
|
|
57
|
+
color: nitroColor,
|
|
58
|
+
traitCollection: traitCollection
|
|
59
|
+
)
|
|
60
|
+
template.guidanceBackgroundColor = cardBackgroundColor
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
template.guidanceBackgroundColor = .black
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
if let initialProperties = SceneStore.getRootScene()?.initialProperties,
|
|
55
67
|
let windowDict = initialProperties["window"] as? [String: Any],
|
|
@@ -115,6 +127,20 @@ class MapTemplate: AutoPlayHeaderProviding,
|
|
|
115
127
|
button.onPress?()
|
|
116
128
|
}
|
|
117
129
|
}
|
|
130
|
+
if let remoteImage = button.image.remoteImage,
|
|
131
|
+
let icon = Parser.parseRemoteImage(
|
|
132
|
+
remoteImage: remoteImage,
|
|
133
|
+
traitCollection: traitCollection
|
|
134
|
+
)
|
|
135
|
+
{
|
|
136
|
+
return CPMapButton(image: icon) { _ in
|
|
137
|
+
if button.type == .pan {
|
|
138
|
+
self.onPanButtonPress()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
button.onPress?()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
118
144
|
|
|
119
145
|
return CPMapButton { _ in
|
|
120
146
|
if button.type == .pan {
|
|
@@ -607,6 +633,35 @@ class MapTemplate: AutoPlayHeaderProviding,
|
|
|
607
633
|
updateVisibleTravelEstimate(visibleTravelEstimate: nil)
|
|
608
634
|
}
|
|
609
635
|
|
|
636
|
+
func updateManeuversLoading(loading: NitroLoadingManeuver) {
|
|
637
|
+
guard let navigationSession = navigationSession else { return }
|
|
638
|
+
|
|
639
|
+
let description = loading.text
|
|
640
|
+
|
|
641
|
+
guard let traitCollection = SceneStore.getRootTraitCollection() else {
|
|
642
|
+
navigationSession.pauseTrip(for: .loading, description: description)
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let cardBackgroundColor = Parser.routingManeuverCardBackgroundUIColor(
|
|
647
|
+
color: loading.cardBackgroundColor,
|
|
648
|
+
traitCollection: traitCollection
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
template.guidanceBackgroundColor = cardBackgroundColor
|
|
652
|
+
|
|
653
|
+
if #available(iOS 18.0, *) {
|
|
654
|
+
navigationSession.pauseTrip(
|
|
655
|
+
for: .loading,
|
|
656
|
+
description: description,
|
|
657
|
+
turnCardColor: cardBackgroundColor
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
navigationSession.pauseTrip(for: .loading, description: description)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
610
665
|
func updateManeuvers(messageManeuver: NitroMessageManeuver) {
|
|
611
666
|
guard let navigationSession = navigationSession else { return }
|
|
612
667
|
|
|
@@ -74,6 +74,12 @@ class Parser {
|
|
|
74
74
|
traitCollection: traitCollection
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
|
+
if let remoteImage = action.image?.remoteImage {
|
|
78
|
+
image = Parser.parseRemoteImage(
|
|
79
|
+
remoteImage: remoteImage,
|
|
80
|
+
traitCollection: traitCollection
|
|
81
|
+
)
|
|
82
|
+
}
|
|
77
83
|
|
|
78
84
|
var button: CPBarButton
|
|
79
85
|
|
|
@@ -503,6 +509,21 @@ class Parser {
|
|
|
503
509
|
)
|
|
504
510
|
}
|
|
505
511
|
|
|
512
|
+
/// Card background `UIColor` for routing maneuvers and loading pause — same light/dark component pick as `parseManeuver`.
|
|
513
|
+
static func routingManeuverCardBackgroundUIColor(
|
|
514
|
+
color: NitroColor,
|
|
515
|
+
traitCollection: UITraitCollection
|
|
516
|
+
) -> UIColor {
|
|
517
|
+
if #available(iOS 15.4, *) {
|
|
518
|
+
let component =
|
|
519
|
+
traitCollection.userInterfaceStyle == .dark
|
|
520
|
+
? color.darkColor
|
|
521
|
+
: color.lightColor
|
|
522
|
+
return doubleToColor(value: component)
|
|
523
|
+
}
|
|
524
|
+
return parseColor(color: color)
|
|
525
|
+
}
|
|
526
|
+
|
|
506
527
|
static func parseManeuver(
|
|
507
528
|
nitroManeuver: NitroRoutingManeuver,
|
|
508
529
|
traitCollection: UITraitCollection
|
|
@@ -528,13 +549,9 @@ class Parser {
|
|
|
528
549
|
)
|
|
529
550
|
|
|
530
551
|
if #available(iOS 15.4, *) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
: nitroManeuver.cardBackgroundColor.lightColor
|
|
535
|
-
|
|
536
|
-
maneuver.cardBackgroundColor = doubleToColor(
|
|
537
|
-
value: cardBackgroundColor
|
|
552
|
+
maneuver.cardBackgroundColor = routingManeuverCardBackgroundUIColor(
|
|
553
|
+
color: nitroManeuver.cardBackgroundColor,
|
|
554
|
+
traitCollection: traitCollection
|
|
538
555
|
)
|
|
539
556
|
}
|
|
540
557
|
|
|
@@ -760,9 +777,30 @@ class Parser {
|
|
|
760
777
|
)
|
|
761
778
|
}
|
|
762
779
|
|
|
780
|
+
if let remoteImage = image?.remoteImage {
|
|
781
|
+
return Parser.parseRemoteImage(
|
|
782
|
+
remoteImage: remoteImage,
|
|
783
|
+
traitCollection: traitCollection
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
763
787
|
return nil
|
|
764
788
|
}
|
|
765
789
|
|
|
790
|
+
// MARK: - Remote image cache
|
|
791
|
+
private static let remoteImageCache: NSCache<NSString, UIImage> = {
|
|
792
|
+
let cache = NSCache<NSString, UIImage>()
|
|
793
|
+
cache.countLimit = 50
|
|
794
|
+
cache.totalCostLimit = 8 * 1024 * 1024 // 8 MB, matching Android's BitmapCache
|
|
795
|
+
return cache
|
|
796
|
+
}()
|
|
797
|
+
|
|
798
|
+
/// Shared session — long-lived by design; failed tasks don't invalidate it.
|
|
799
|
+
private static let remoteImageSession = URLSession(configuration: .default)
|
|
800
|
+
|
|
801
|
+
/// Default network timeout for remote images when no `timeoutMs` is provided.
|
|
802
|
+
private static let defaultRemoteTimeoutSeconds: TimeInterval = 0.5
|
|
803
|
+
|
|
766
804
|
static func parseAssetImage(
|
|
767
805
|
assetImage: AssetImage,
|
|
768
806
|
traitCollection: UITraitCollection
|
|
@@ -773,17 +811,77 @@ class Parser {
|
|
|
773
811
|
"__packager_asset": assetImage.packager_asset,
|
|
774
812
|
])
|
|
775
813
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
814
|
+
return applyTint(
|
|
815
|
+
uiImage: uiImage,
|
|
816
|
+
color: assetImage.color,
|
|
817
|
+
traitCollection: traitCollection
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
static func parseRemoteImage(
|
|
822
|
+
remoteImage: RemoteImage,
|
|
823
|
+
traitCollection: UITraitCollection
|
|
824
|
+
) -> UIImage? {
|
|
825
|
+
let timeoutSeconds = remoteImage.timeoutMs.map { $0 / 1000.0 } ?? defaultRemoteTimeoutSeconds
|
|
826
|
+
let uiImage = loadRemoteImage(uri: remoteImage.uri, timeoutSeconds: timeoutSeconds)
|
|
827
|
+
|
|
828
|
+
return applyTint(
|
|
829
|
+
uiImage: uiImage,
|
|
830
|
+
color: remoteImage.color,
|
|
831
|
+
traitCollection: traitCollection
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private static func applyTint(
|
|
836
|
+
uiImage: UIImage?,
|
|
837
|
+
color: NitroColor?,
|
|
838
|
+
traitCollection: UITraitCollection
|
|
839
|
+
) -> UIImage? {
|
|
840
|
+
guard let image = uiImage else { return nil }
|
|
841
|
+
guard let color else { return image }
|
|
779
842
|
|
|
780
843
|
return getTintedImageAsset(
|
|
781
844
|
color: color,
|
|
782
|
-
uiImage:
|
|
845
|
+
uiImage: image,
|
|
783
846
|
traitCollection: traitCollection
|
|
784
847
|
)
|
|
785
848
|
}
|
|
786
849
|
|
|
850
|
+
/// Synchronously loads an image from a remote HTTPS URL with in-memory caching.
|
|
851
|
+
/// `parseRemoteImage` is always invoked on a background thread by the Car App rendering pipeline,
|
|
852
|
+
/// so the semaphore wait cannot block the main thread.
|
|
853
|
+
private static func loadRemoteImage(uri: String, timeoutSeconds: TimeInterval) -> UIImage? {
|
|
854
|
+
let cacheKey = uri as NSString
|
|
855
|
+
if let cached = remoteImageCache.object(forKey: cacheKey) {
|
|
856
|
+
return cached
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
guard let url = URL(string: uri) else { return nil }
|
|
860
|
+
|
|
861
|
+
var request = URLRequest(url: url)
|
|
862
|
+
request.timeoutInterval = timeoutSeconds
|
|
863
|
+
|
|
864
|
+
var resultData: Data?
|
|
865
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
866
|
+
let task = remoteImageSession.dataTask(with: request) { data, _, _ in
|
|
867
|
+
resultData = data
|
|
868
|
+
semaphore.signal()
|
|
869
|
+
}
|
|
870
|
+
task.resume()
|
|
871
|
+
if semaphore.wait(timeout: .now() + timeoutSeconds) == .timedOut {
|
|
872
|
+
task.cancel()
|
|
873
|
+
return UIImage(systemName: "exclamationmark.circle")
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
guard let data = resultData, let image = UIImage(data: data) else {
|
|
877
|
+
return UIImage(systemName: "exclamationmark.circle")
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let cost = Int(image.size.width * image.size.height * image.scale * image.scale * 4)
|
|
881
|
+
remoteImageCache.setObject(image, forKey: cacheKey, cost: cost)
|
|
882
|
+
return image
|
|
883
|
+
}
|
|
884
|
+
|
|
787
885
|
static func getTintedImageAsset(
|
|
788
886
|
color: NitroColor,
|
|
789
887
|
uiImage: UIImage,
|