@iternio/react-native-auto-play 0.3.11 → 0.4.0

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 (135) hide show
  1. package/README.md +103 -3
  2. package/ReactNativeAutoPlay.podspec +0 -4
  3. package/android/src/automotive/AndroidManifest.xml +1 -0
  4. package/android/src/main/AndroidManifest.xml +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 +2 -1
  8. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +117 -38
  9. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
  10. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt +29 -30
  11. package/ios/extensions/NitroImageExtensions.swift +10 -1
  12. package/ios/hybrid/HybridAutoPlay.swift +51 -4
  13. package/ios/templates/GridTemplate.swift +7 -0
  14. package/ios/templates/MapTemplate.swift +14 -0
  15. package/ios/templates/Parser.swift +91 -4
  16. package/ios/utils/SymbolFont.swift +44 -44
  17. package/ios/utils/VoiceInputManager.swift +233 -0
  18. package/lib/index.d.ts +1 -0
  19. package/lib/index.js +1 -0
  20. package/lib/specs/AutoPlay.nitro.d.ts +31 -1
  21. package/lib/types/Image.d.ts +46 -4
  22. package/lib/types/Maneuver.d.ts +2 -10
  23. package/lib/utils/NitroImage.d.ts +29 -3
  24. package/lib/utils/NitroImage.js +64 -3
  25. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
  26. package/nitrogen/generated/android/c++/JGlyphImage.hpp +6 -1
  27. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
  28. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
  29. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
  30. package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
  31. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
  32. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
  33. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
  34. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
  35. package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
  36. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
  37. package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
  38. package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
  39. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
  40. package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
  41. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
  42. package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +3 -1
  43. package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
  44. package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
  45. package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
  46. package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
  47. package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
  48. package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
  49. package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
  50. package/nitrogen/generated/android/c++/JNitroImage.hpp +20 -3
  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 +3 -1
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +5 -2
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
  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/NitroMessageManeuver.kt +2 -2
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
  76. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
  77. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
  79. package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
  80. package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +4 -1
  81. package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
  82. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  83. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
  84. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
  85. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
  86. package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
  87. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
  88. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
  89. package/nitrogen/generated/ios/swift/GlyphImage.swift +7 -2
  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/MessageTemplateConfig.swift +16 -11
  94. package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
  95. package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
  96. package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
  97. package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
  98. package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
  99. package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
  100. package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
  101. package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
  102. package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
  103. package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
  104. package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
  105. package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
  106. package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
  107. package/nitrogen/generated/shared/c++/GlyphImage.hpp +6 -1
  108. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
  109. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
  110. package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
  111. package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
  112. package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
  113. package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
  114. package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
  115. package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
  116. package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
  117. package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
  118. package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
  119. package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
  120. package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
  121. package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
  122. package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
  123. package/package.json +2 -3
  124. package/src/index.ts +1 -0
  125. package/src/specs/AutoPlay.nitro.ts +39 -1
  126. package/src/types/Image.ts +65 -16
  127. package/src/types/Maneuver.ts +3 -10
  128. package/src/utils/NitroImage.ts +81 -6
  129. package/android/src/main/res/font/materialsymbolsoutlined_regular.ttf +0 -0
  130. package/ios/Assets/MaterialSymbolsOutlined-Regular.ttf +0 -0
  131. package/lib/types/Glyphmap.d.ts +0 -4105
  132. package/lib/types/Glyphmap.js +0 -4105
  133. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
  134. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
  135. package/src/types/Glyphmap.ts +0 -4107
@@ -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,115 @@ 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 }
524
548
 
525
- val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
526
- val result = DataSources.waitForFinalResult(dataSource)
549
+ val imageRequest = buildImageRequest(remoteImage.uri.toUri())
550
+ val timeoutMs = remoteImage.timeoutMs?.toLong() ?: 500L
551
+ val bitmap = fetchBitmap(context, imageRequest, timeoutMs = timeoutMs) ?: return null
527
552
 
553
+ return applyTintAndCache(
554
+ context = context,
555
+ color = remoteImage.color,
556
+ bitmap = bitmap,
557
+ cache = { result -> BitmapCache.put(context, remoteImage, result) }
558
+ )
559
+ }
560
+
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
+ }
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
+ // underlyingBitmap can be null when Fresco decodes to a CloseableBitmap
602
+ // whose backing bitmap has already been recycled or failed to allocate;
603
+ // copy() can also throw (e.g., OOM on very large remote images). Either
604
+ // way we return null so the caller falls back to a placeholder icon.
605
+ return image.underlyingBitmap?.copy(Bitmap.Config.ARGB_8888, false)
532
606
  } else if (image is CloseableXml) {
533
607
  val drawable = image.buildDrawable()
534
- bitmap = drawable?.toBitmap(
535
- width = image.width, height = image.height, Bitmap.Config.ARGB_8888
536
- )
608
+ return drawable?.toBitmap(width = image.width, height = image.height, Bitmap.Config.ARGB_8888)
537
609
  }
610
+ } catch (_: Exception) {
611
+ // Any decode/copy failure (OOM, recycled bitmap, invalid config, …) should
612
+ // not crash the car app — the image is optional decoration and the caller
613
+ // handles a null return with CarIcon.ALERT.
614
+ return null
538
615
  } finally {
539
616
  image?.close()
540
617
  result?.close()
541
618
  dataSource.close()
542
619
  }
620
+ return null
621
+ }
543
622
 
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)
623
+ private fun applyTintAndCache(
624
+ context: CarContext,
625
+ color: NitroColor?,
626
+ bitmap: Bitmap,
627
+ cache: (Bitmap) -> Unit
628
+ ): Bitmap {
629
+ color?.get(context)?.let { tint ->
630
+ val tinted = createBitmap(bitmap.width, bitmap.height)
631
+ val canvas = Canvas(tinted)
551
632
  val paint = Paint()
552
633
 
553
- paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
634
+ paint.colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN)
554
635
  canvas.drawBitmap(bitmap, 0f, 0f, paint)
555
636
 
556
- BitmapCache.put(context, assetImage, result)
557
-
558
- return result
637
+ cache(tinted)
638
+ return tinted
559
639
  }
560
640
 
561
- BitmapCache.put(context, assetImage, bitmap)
562
-
641
+ cache(bitmap)
563
642
  return bitmap
564
643
  }
565
644
 
@@ -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"
@@ -10,39 +10,36 @@ import android.graphics.Typeface
10
10
  import androidx.car.app.CarContext
11
11
  import androidx.core.content.res.ResourcesCompat
12
12
  import androidx.core.graphics.createBitmap
13
- import androidx.core.graphics.drawable.IconCompat
14
- import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
15
- import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroImage
16
13
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.BuildConfig
17
- import com.margelo.nitro.swe.iternio.reactnativeautoplay.R
18
- import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.Parser
14
+ import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
19
15
 
20
16
  object SymbolFont {
21
- const val TAG = "SymbolFont"
22
-
23
- private var typeface: Typeface? = null
24
-
25
- private fun loadFont(context: Context) {
26
- if (typeface != null) {
27
- return
28
- }
17
+ private var cachedFontName: String? = null
18
+ private var cachedTypeface: Typeface? = null
29
19
 
30
- typeface = ResourcesCompat.getFont(context, R.font.materialsymbolsoutlined_regular)
20
+ private fun loadTypeface(context: Context, fontName: String): Typeface? {
21
+ if (fontName == cachedFontName) return cachedTypeface
22
+ val id = context.resources.getIdentifier(
23
+ fontName.lowercase(), "font", context.packageName
24
+ )
25
+ if (id == 0) return null
26
+ val tf = ResourcesCompat.getFont(context, id) ?: return null
27
+ cachedFontName = fontName
28
+ cachedTypeface = tf
29
+ return tf
31
30
  }
32
31
 
33
32
  private fun imageFromGlyph(
34
33
  context: Context,
35
- glyph: Double,
34
+ glyphImage: GlyphImage,
36
35
  color: Int,
37
36
  backgroundColor: Int,
38
37
  cornerRadius: Float = 8f, //TODO: make accessible and add it to GlyphImage.cacheKey
39
- fontScale: Float,
40
38
  ): Bitmap? {
41
- loadFont(context)
42
-
43
- val font = typeface ?: run {
44
- return null
45
- }
39
+ val font =
40
+ loadTypeface(context, glyphImage.fontName) ?: run {
41
+ return null
42
+ }
46
43
 
47
44
  val virtualScreenDensity = context.resources.displayMetrics.density
48
45
  val scale = BuildConfig.SCALE_FACTOR * virtualScreenDensity
@@ -59,6 +56,8 @@ object SymbolFont {
59
56
  }
60
57
  canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint)
61
58
 
59
+ val fontScale = (glyphImage.fontScale ?: 1.0).toFloat()
60
+
62
61
  // Setup text paint
63
62
  paint.reset()
64
63
  paint = Paint().apply {
@@ -70,7 +69,7 @@ object SymbolFont {
70
69
  }
71
70
 
72
71
  // Get the character from codepoint
73
- val codepoint = glyph.toInt()
72
+ val codepoint = glyphImage.glyph.toInt()
74
73
  val text = String(Character.toChars(codepoint))
75
74
 
76
75
  // Measure text
@@ -94,13 +93,13 @@ object SymbolFont {
94
93
  return bitmap
95
94
  }
96
95
 
97
- bitmap = imageFromGlyph(
98
- context = context,
99
- glyph = image.glyph,
100
- color = image.color.get(context),
101
- backgroundColor = image.backgroundColor.get(context),
102
- fontScale = (image.fontScale ?: 1.0).toFloat()
103
- )
96
+ bitmap =
97
+ imageFromGlyph(
98
+ context = context,
99
+ glyphImage = image,
100
+ color = image.color.get(context),
101
+ backgroundColor = image.backgroundColor.get(context),
102
+ )
104
103
 
105
104
  bitmap?.let {
106
105
  BitmapCache.put(context, image, it)
@@ -108,4 +107,4 @@ object SymbolFont {
108
107
 
109
108
  return bitmap
110
109
  }
111
- }
110
+ }
@@ -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
@@ -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
 
@@ -127,6 +127,20 @@ class MapTemplate: AutoPlayHeaderProviding,
127
127
  button.onPress?()
128
128
  }
129
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
+ }
130
144
 
131
145
  return CPMapButton { _ in
132
146
  if button.type == .pan {
@@ -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
 
@@ -771,9 +777,30 @@ class Parser {
771
777
  )
772
778
  }
773
779
 
780
+ if let remoteImage = image?.remoteImage {
781
+ return Parser.parseRemoteImage(
782
+ remoteImage: remoteImage,
783
+ traitCollection: traitCollection
784
+ )
785
+ }
786
+
774
787
  return nil
775
788
  }
776
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
+
777
804
  static func parseAssetImage(
778
805
  assetImage: AssetImage,
779
806
  traitCollection: UITraitCollection
@@ -784,17 +811,77 @@ class Parser {
784
811
  "__packager_asset": assetImage.packager_asset,
785
812
  ])
786
813
 
787
- guard let color = assetImage.color else {
788
- return uiImage
789
- }
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 }
790
842
 
791
843
  return getTintedImageAsset(
792
844
  color: color,
793
- uiImage: uiImage,
845
+ uiImage: image,
794
846
  traitCollection: traitCollection
795
847
  )
796
848
  }
797
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
+
798
885
  static func getTintedImageAsset(
799
886
  color: NitroColor,
800
887
  uiImage: UIImage,