@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.
Files changed (134) hide show
  1. package/README.md +60 -2
  2. package/android/src/automotive/AndroidManifest.xml +1 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +1 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +8 -1
  8. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +108 -38
  9. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
  10. package/ios/extensions/NitroImageExtensions.swift +10 -1
  11. package/ios/hybrid/HybridAutoPlay.swift +51 -4
  12. package/ios/hybrid/HybridMapTemplate.swift +2 -5
  13. package/ios/templates/GridTemplate.swift +7 -0
  14. package/ios/templates/MapTemplate.swift +55 -0
  15. package/ios/templates/Parser.swift +109 -11
  16. package/ios/utils/VoiceInputManager.swift +233 -0
  17. package/lib/specs/AutoPlay.nitro.d.ts +31 -1
  18. package/lib/templates/MapTemplate.d.ts +7 -1
  19. package/lib/templates/MapTemplate.js +10 -2
  20. package/lib/types/Image.d.ts +13 -0
  21. package/lib/types/Maneuver.d.ts +6 -0
  22. package/lib/utils/NitroImage.d.ts +6 -1
  23. package/lib/utils/NitroImage.js +7 -0
  24. package/lib/utils/NitroManeuver.d.ts +2 -0
  25. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
  26. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
  27. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
  28. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
  29. package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
  30. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
  31. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
  32. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
  33. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
  34. package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
  35. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
  36. package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
  37. package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
  38. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
  39. package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
  40. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
  41. package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +8 -2
  42. package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
  43. package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
  44. package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
  45. package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
  46. package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
  47. package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
  48. package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
  49. package/nitrogen/generated/android/c++/JNitroImage.hpp +19 -2
  50. package/nitrogen/generated/android/c++/JNitroLoadingManeuver.hpp +15 -4
  51. package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
  52. package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
  53. package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
  54. package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
  55. package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
  56. package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
  57. package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
  58. package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
  59. package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
  60. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
  61. package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
  62. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
  63. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
  64. package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +2 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MapTemplateConfig.kt +7 -4
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroLoadingManeuver.kt +9 -3
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
  77. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
  79. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
  80. package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
  81. package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +3 -0
  82. package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
  83. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  84. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
  85. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
  86. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
  87. package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
  88. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
  89. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
  90. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
  91. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
  92. package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
  93. package/nitrogen/generated/ios/swift/MapTemplateConfig.swift +12 -1
  94. package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
  95. package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
  96. package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
  97. package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
  98. package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
  99. package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
  100. package/nitrogen/generated/ios/swift/NitroLoadingManeuver.swift +25 -2
  101. package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
  102. package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
  103. package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
  104. package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
  105. package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
  106. package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
  107. package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
  108. package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
  109. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
  110. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
  111. package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
  112. package/nitrogen/generated/shared/c++/MapTemplateConfig.hpp +8 -1
  113. package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
  114. package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
  115. package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
  116. package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
  117. package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
  118. package/nitrogen/generated/shared/c++/NitroLoadingManeuver.hpp +15 -4
  119. package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
  120. package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
  121. package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
  122. package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
  123. package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
  124. package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
  125. package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
  126. package/package.json +1 -1
  127. package/src/specs/AutoPlay.nitro.ts +39 -1
  128. package/src/templates/MapTemplate.ts +23 -2
  129. package/src/types/Image.ts +14 -0
  130. package/src/types/Maneuver.ts +6 -0
  131. package/src/utils/NitroImage.ts +15 -1
  132. package/src/utils/NitroManeuver.ts +2 -0
  133. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
  134. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
@@ -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.Variant_GlyphImage_AssetImage
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: Variant_GlyphImage_AssetImage): CarIcon {
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(context: CarContext, glyphImage: GlyphImage?, assetImage: AssetImage?): CarIcon {
222
- val bitmap = parseImageToBitmap(context, glyphImage, assetImage)
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
- // this should not be possible, we just wanna satisfy kotlin
229
- return CarIcon.APP_ICON
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, glyphImage: GlyphImage?, assetImage: AssetImage?
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.map {
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
- var bitmap = BitmapCache.get(context, assetImage)
533
+ BitmapCache.get(context, assetImage)?.let { return it }
516
534
 
517
- if (bitmap != null) {
518
- return bitmap
519
- }
535
+ val imageRequest = buildImageRequest(ImageSource(context, assetImage.uri).uri)
536
+ val bitmap = fetchBitmap(context, imageRequest, timeoutMs = null) ?: return null
520
537
 
521
- val source = ImageSource(context, assetImage.uri)
522
- val imageRequest = ImageRequestBuilder.newBuilderWithSource(source.uri).disableDiskCache()
523
- .disableMemoryCache().build()
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
- val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
526
- val result = DataSources.waitForFinalResult(dataSource)
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
- bitmap = image.underlyingBitmap.copy(Bitmap.Config.ARGB_8888, false)
601
+ return image.underlyingBitmap.copy(Bitmap.Config.ARGB_8888, false)
532
602
  } else if (image is CloseableXml) {
533
603
  val drawable = image.buildDrawable()
534
- bitmap = drawable?.toBitmap(
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
- if (bitmap == null) {
545
- return null
546
- }
547
-
548
- assetImage.color?.get(context)?.let { color ->
549
- val result = createBitmap(bitmap.width, bitmap.height)
550
- val canvas = Canvas(result)
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(color, PorterDuff.Mode.SRC_IN)
625
+ paint.colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN)
554
626
  canvas.drawBitmap(bitmap, 0f, 0f, paint)
555
627
 
556
- BitmapCache.put(context, assetImage, result)
557
-
558
- return result
628
+ cache(tinted)
629
+ return tinted
559
630
  }
560
631
 
561
- BitmapCache.put(context, assetImage, bitmap)
562
-
632
+ cache(bitmap)
563
633
  return bitmap
564
634
  }
565
635
 
@@ -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 Variant_GlyphImage_AssetImage: ImageProtocol {
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
- // TODO: Inplement voice input
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
- -> NitroModules.Promise<Void>
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 -> NitroModules.Promise<Void> {
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 -> NitroModules.Promise<Void> {
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 loadingManeuver):
141
+ case .third(let loading):
142
142
  {
143
- template.navigationSession?.pauseTrip(
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
- let cardBackgroundColor =
532
- traitCollection.userInterfaceStyle == .dark
533
- ? nitroManeuver.cardBackgroundColor.darkColor
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
- guard let color = assetImage.color else {
777
- return uiImage
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: 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,