@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
package/README.md CHANGED
@@ -613,7 +613,7 @@ This section lists the available listeners and lifecycle callbacks so you can wi
613
613
  | --- | --- | --- |
614
614
  | `HybridAutoPlay.addListener(event, cb)` | event: `'didConnect'` `'didDisconnect'` | Connection changes for the head unit. |
615
615
  | `HybridAutoPlay.addListenerRenderState(moduleName, cb)` | `cb(visibility: 'willAppear' \| 'didAppear' \| 'willDisappear' \| 'didDisappear')` | Use `AutoPlayModules.*` or a cluster UUID. |
616
- | `HybridAutoPlay.addListenerVoiceInput(cb)` | `cb(location?, query?)` | Android-only voice input. |
616
+ | `HybridAutoPlay.addListenerVoiceInput(cb)` | `cb(location?, query?)` | Android-only. Fires when the OS triggers a voice input event (e.g. "Hey Google, navigate to…"). For in-app recording use `startVoiceInput` instead. |
617
617
  | `HybridAutoPlay.addSafeAreaInsetsListener(moduleName, cb)` | `cb(insets)` | Safe area inset changes for any module. |
618
618
 
619
619
  ```ts
@@ -760,10 +760,68 @@ new ListTemplate({
760
760
  }).push();
761
761
  ```
762
762
 
763
+ ### Voice Input
764
+
765
+ The library provides a cross-platform in-app voice recording API built on top of the car microphone (when connected) or the device microphone (when no car is connected).
766
+
767
+ #### Permission
768
+
769
+ ```ts
770
+ // Check whether permission is already granted
771
+ const granted = HybridAutoPlay.hasVoiceInputPermission();
772
+
773
+ // Request permission if not yet granted
774
+ const granted = await HybridAutoPlay.requestVoiceInputPermission();
775
+ ```
776
+
777
+ #### Recording
778
+
779
+ ```ts
780
+ // Start recording — resolves with a raw PCM ArrayBuffer (16 kHz, 16-bit, mono)
781
+ // Recording stops automatically on silence or when maxDurationMs is reached
782
+ const pcmBuffer = await HybridAutoPlay.startVoiceInput(
783
+ 1500, // silenceThresholdMs (default 1500)
784
+ 10_000, // maxDurationMs (default 10 000)
785
+ 'Listening...' // text shown on the car screen while recording (iOS CarPlay)
786
+ );
787
+
788
+ // Stop recording early — resolves startVoiceInput with the audio captured so far
789
+ HybridAutoPlay.stopVoiceInput();
790
+ ```
791
+
792
+ On **Android**: uses `CarAudioRecord` when Android Auto is connected, otherwise falls back to standard `AudioRecord`.
793
+
794
+ On **iOS**: presents `CPVoiceControlTemplate` on the car screen when CarPlay is connected, and captures audio via `AVAudioEngine`.
795
+
796
+ #### OS-triggered voice input (Android only)
797
+
798
+ `addListenerVoiceInput` fires when the OS itself initiates a voice action (e.g. "Hey Google, navigate to…"). It is a no-op on iOS — use `startVoiceInput` for in-app recording on both platforms.
799
+
800
+ ```ts
801
+ const cleanup = HybridAutoPlay.addListenerVoiceInput((location, query) => {
802
+ console.log('Voice query:', query, 'near', location);
803
+ });
804
+ ```
805
+
806
+ #### useVoiceInput hook (Android only)
807
+
808
+ A convenience hook that wires up `addListenerVoiceInput` and exposes the latest `location` and `query` values reactively.
809
+
810
+ ```tsx
811
+ import { useVoiceInput } from '@iternio/react-native-auto-play';
812
+
813
+ const MyScreen = () => {
814
+ const { location, query } = useVoiceInput();
815
+ return <Text>{query ?? 'Say something…'}</Text>;
816
+ };
817
+ ```
818
+
819
+ ---
820
+
763
821
  ### Hooks
764
822
 
765
823
  - `useMapTemplate()`: Get a reference to the parent `MapTemplate` instance.
766
- - `useVoiceInput()`: Access voice input functionality - Android Auto only.
824
+ - `useVoiceInput()`: Reactively exposes the latest OS-triggered voice input (`location`, `query`). Android only — for in-app recording use `startVoiceInput` / `stopVoiceInput` directly.
767
825
  - `useSafeAreaInsets()`: Get safe area insets for any root component.
768
826
  - `useFocusedEffect()`: A useEffect alternative that executes when the specified component is visible to the user - use any of the `AutoPlayModules` enum or a cluster uuid to sepcify the component the effect should listen for.
769
827
  - `useAndroidAutoTelemetry()`: Access to car telemetry data on Android Auto and Android Automotive.
@@ -19,6 +19,7 @@
19
19
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
20
20
 
21
21
  <uses-permission android:name="android.permission.WAKE_LOCK" />
22
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
22
23
 
23
24
 
24
25
  <!-- Android Automotive specific permissions -->
@@ -12,6 +12,7 @@
12
12
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
13
13
 
14
14
  <uses-permission android:name="android.permission.WAKE_LOCK" />
15
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
15
16
 
16
17
  <!-- Android Auto specific permissions -->
17
18
  <uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
@@ -55,6 +55,7 @@ class AndroidAutoSession(sessionInfo: SessionInfo) :
55
55
  null,
56
56
  arrayOf(action),
57
57
  null,
58
+ null,
58
59
  null
59
60
  )
60
61
 
@@ -1,12 +1,22 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
2
 
3
+ import android.content.pm.PackageManager
4
+ import android.os.Build
5
+ import androidx.core.content.ContextCompat
3
6
  import com.facebook.react.bridge.UiThreadUtil
7
+ import com.facebook.react.modules.core.PermissionAwareActivity
8
+ import com.facebook.react.modules.core.PermissionListener
9
+ import com.margelo.nitro.NitroModules
10
+ import com.margelo.nitro.core.ArrayBuffer
4
11
  import com.margelo.nitro.core.Promise
5
12
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.AndroidAutoTemplate
6
13
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.MessageTemplate
7
14
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.ThreadUtil
15
+ import kotlinx.coroutines.suspendCancellableCoroutine
16
+ import java.nio.ByteBuffer
8
17
  import java.util.concurrent.ConcurrentHashMap
9
18
  import java.util.concurrent.CopyOnWriteArrayList
19
+ import kotlin.coroutines.resume
10
20
 
11
21
  class HybridAutoPlay : HybridAutoPlaySpec() {
12
22
  init {
@@ -241,6 +251,84 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
241
251
  }
242
252
  }
243
253
 
254
+ override fun hasVoiceInputPermission(): Boolean {
255
+ val context = NitroModules.applicationContext ?: return false
256
+ return ContextCompat.checkSelfPermission(
257
+ context, android.Manifest.permission.RECORD_AUDIO
258
+ ) == PackageManager.PERMISSION_GRANTED
259
+ }
260
+
261
+ override fun requestVoiceInputPermission(): Promise<Boolean> {
262
+ return Promise.async {
263
+ if (hasVoiceInputPermission()) {
264
+ return@async true
265
+ }
266
+
267
+ val carContext = AndroidAutoSession.getRootContext()
268
+
269
+ if (carContext != null) {
270
+ suspendCancellableCoroutine {
271
+ carContext.requestPermissions(listOf(android.Manifest.permission.RECORD_AUDIO)) { approved, _ ->
272
+ it.resume(approved.contains(android.Manifest.permission.RECORD_AUDIO))
273
+ }
274
+ }
275
+ } else {
276
+ val context = NitroModules.applicationContext ?: return@async false
277
+ val activity =
278
+ context.currentActivity as? PermissionAwareActivity ?: return@async false
279
+ val code = (Math.random() * 10000).toInt()
280
+
281
+ suspendCancellableCoroutine {
282
+ activity.requestPermissions(
283
+ arrayOf(android.Manifest.permission.RECORD_AUDIO),
284
+ code,
285
+ PermissionListener { requestCode, _, grantResults ->
286
+ if (requestCode != code) {
287
+ return@PermissionListener false
288
+ }
289
+
290
+ val granted =
291
+ grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED
292
+
293
+ it.resume(granted)
294
+
295
+ return@PermissionListener true
296
+ })
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ override fun startVoiceInput(
303
+ silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?
304
+ ): Promise<ArrayBuffer> {
305
+ return Promise.async {
306
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
307
+ throw UnsupportedOperationException("startVoiceInput requires at least API level ${Build.VERSION_CODES.O}")
308
+ }
309
+
310
+ val manager = VoiceInputManager(AndroidAutoSession.getRootContext())
311
+ voiceInputManager = manager
312
+
313
+ try {
314
+ val pcmBytes = manager.start(
315
+ silenceThresholdMs = silenceThresholdMs?.toLong() ?: 1_500L,
316
+ maxDurationMs = maxDurationMs?.toLong() ?: 10_000L,
317
+ )
318
+ val directBuffer =
319
+ ByteBuffer.allocateDirect(pcmBytes.size).put(pcmBytes).rewind() as ByteBuffer
320
+ ArrayBuffer.wrap(directBuffer)
321
+ } finally {
322
+ voiceInputManager = null
323
+ manager.dispose()
324
+ }
325
+ }
326
+ }
327
+
328
+ override fun stopVoiceInput() {
329
+ voiceInputManager?.stop()
330
+ }
331
+
244
332
  companion object {
245
333
  const val TAG = "HybridAutoPlay"
246
334
 
@@ -251,6 +339,9 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
251
339
 
252
340
  private val voiceInputListeners = CopyOnWriteArrayList<(Location?, String?) -> Unit>()
253
341
 
342
+ @Volatile
343
+ private var voiceInputManager: VoiceInputManager? = null
344
+
254
345
  private val safeAreaInsetsListeners =
255
346
  ConcurrentHashMap<String, CopyOnWriteArrayList<(SafeAreaInsets) -> Unit>>()
256
347
 
@@ -0,0 +1,214 @@
1
+ package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
+
3
+ import android.content.pm.PackageManager
4
+ import android.media.AudioAttributes
5
+ import android.media.AudioFocusRequest
6
+ import android.media.AudioFormat
7
+ import android.media.AudioManager
8
+ import android.media.AudioRecord
9
+ import android.media.MediaRecorder
10
+ import android.os.Build
11
+ import androidx.annotation.RequiresApi
12
+ import androidx.car.app.CarContext
13
+ import androidx.car.app.media.CarAudioRecord
14
+ import androidx.core.content.ContextCompat
15
+ import com.margelo.nitro.NitroModules
16
+ import kotlinx.coroutines.CoroutineScope
17
+ import kotlinx.coroutines.Dispatchers
18
+ import kotlinx.coroutines.Job
19
+ import kotlinx.coroutines.cancel
20
+ import kotlinx.coroutines.launch
21
+ import kotlinx.coroutines.suspendCancellableCoroutine
22
+ import java.io.ByteArrayOutputStream
23
+ import kotlin.coroutines.Continuation
24
+ import kotlin.coroutines.resume
25
+ import kotlin.coroutines.resumeWithException
26
+ import kotlin.math.abs
27
+
28
+ /**
29
+ * Captures 16-bit PCM audio (16 kHz, mono).
30
+ * When [carContext] is provided uses CarAudioRecord (Android Auto/Automotive).
31
+ * When [carContext] is null falls back to standard AudioRecord (phone-only).
32
+ */
33
+ class VoiceInputManager(
34
+ private val carContext: CarContext?,
35
+ ) {
36
+ private var carAudioRecord: CarAudioRecord? = null
37
+ private var audioRecord: AudioRecord? = null
38
+ private var audioFocusRequest: AudioFocusRequest? = null
39
+ private var recordingJob: Job? = null
40
+ private var continuation: Continuation<ByteArray>? = null
41
+ private val scope = CoroutineScope(Dispatchers.IO)
42
+
43
+ @Volatile
44
+ private var isRecording = false
45
+
46
+ /**
47
+ * Acquires audio focus, starts recording, and suspends until stopped.
48
+ * Stops automatically after [silenceThresholdMs] of silence or [maxDurationMs] total.
49
+ * Returns the complete raw PCM buffer (Int16 LE, 16 kHz, mono).
50
+ */
51
+ @RequiresApi(Build.VERSION_CODES.O)
52
+ suspend fun start(
53
+ silenceThresholdMs: Long = 1_500,
54
+ maxDurationMs: Long = 10_000,
55
+ ): ByteArray = suspendCancellableCoroutine { cont ->
56
+ val appContext = NitroModules.applicationContext
57
+ if (appContext == null || ContextCompat.checkSelfPermission(
58
+ appContext,
59
+ android.Manifest.permission.RECORD_AUDIO,
60
+ ) != PackageManager.PERMISSION_GRANTED
61
+ ) {
62
+ cont.resumeWithException(SecurityException("RECORD_AUDIO permission not granted"))
63
+ return@suspendCancellableCoroutine
64
+ }
65
+
66
+ continuation = cont
67
+
68
+ val audioManager = appContext.getSystemService(AudioManager::class.java)
69
+
70
+ val audioAttributes =
71
+ AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
72
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE).build()
73
+
74
+ val focusRequest =
75
+ AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
76
+ .setAudioAttributes(audioAttributes).setOnAudioFocusChangeListener { state ->
77
+ if (state == AudioManager.AUDIOFOCUS_LOSS) {
78
+ stop()
79
+ }
80
+ }.build()
81
+
82
+ if (audioManager.requestAudioFocus(focusRequest) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
83
+ continuation = null
84
+ cont.resumeWithException(IllegalStateException("Audio focus request denied"))
85
+ return@suspendCancellableCoroutine
86
+ }
87
+
88
+ audioFocusRequest = focusRequest
89
+
90
+ val bufferSize: Int
91
+
92
+ if (carContext != null) {
93
+ val record = CarAudioRecord.create(carContext)
94
+ carAudioRecord = record
95
+ bufferSize = CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE
96
+ isRecording = true
97
+ record.startRecording()
98
+ } else {
99
+ val minBuffer = AudioRecord.getMinBufferSize(
100
+ SAMPLE_RATE,
101
+ AudioFormat.CHANNEL_IN_MONO,
102
+ AudioFormat.ENCODING_PCM_16BIT,
103
+ )
104
+ bufferSize = maxOf(minBuffer, PHONE_BUFFER_SIZE)
105
+ val record = AudioRecord(
106
+ MediaRecorder.AudioSource.MIC,
107
+ SAMPLE_RATE,
108
+ AudioFormat.CHANNEL_IN_MONO,
109
+ AudioFormat.ENCODING_PCM_16BIT,
110
+ bufferSize,
111
+ )
112
+ audioRecord = record
113
+ isRecording = true
114
+ record.startRecording()
115
+ }
116
+
117
+ val outputStream = ByteArrayOutputStream()
118
+
119
+ recordingJob = scope.launch {
120
+ val buffer = ByteArray(bufferSize)
121
+ val recordingStart = System.currentTimeMillis()
122
+ var silenceStart: Long? = null
123
+
124
+ try {
125
+ while (isRecording) {
126
+ val read = carAudioRecord?.read(buffer, 0, bufferSize) ?: audioRecord?.read(
127
+ buffer,
128
+ 0,
129
+ bufferSize,
130
+ ) ?: -1
131
+
132
+ if (read < 0) {
133
+ break
134
+ }
135
+
136
+ if (read > 0) {
137
+ outputStream.write(buffer, 0, read)
138
+
139
+ val now = System.currentTimeMillis()
140
+ val elapsedMs = now - recordingStart
141
+
142
+ if (elapsedMs >= maxDurationMs) {
143
+ break
144
+ }
145
+
146
+ // Silence detection — skip during warm-up
147
+ if (elapsedMs >= WARMUP_MS) {
148
+ var peak = 0
149
+ var i = 0
150
+ while (i < read - 1) {
151
+ val sample =
152
+ (buffer[i].toInt() and 0xFF) or (buffer[i + 1].toInt() shl 8)
153
+ val absSample = abs(sample.toShort().toInt())
154
+ if (absSample > peak) peak = absSample
155
+ i += 2
156
+ }
157
+
158
+ if (peak < SILENCE_AMPLITUDE_THRESHOLD) {
159
+ if (silenceStart == null) {
160
+ silenceStart = now
161
+ }
162
+ if (now - silenceStart >= silenceThresholdMs) {
163
+ break
164
+ }
165
+ } else {
166
+ silenceStart = null
167
+ }
168
+ }
169
+ }
170
+ }
171
+ } finally {
172
+ releaseResources()
173
+ val capturedContinuation = continuation
174
+ continuation = null
175
+ capturedContinuation?.resume(outputStream.toByteArray())
176
+ }
177
+ }
178
+ }
179
+
180
+ fun stop() {
181
+ isRecording = false
182
+ carAudioRecord?.stopRecording()
183
+ audioRecord?.stop()
184
+ }
185
+
186
+ @RequiresApi(Build.VERSION_CODES.O)
187
+ private fun releaseResources() {
188
+ carAudioRecord?.stopRecording()
189
+ carAudioRecord = null
190
+ audioRecord?.stop()
191
+ audioRecord?.release()
192
+ audioRecord = null
193
+ recordingJob = null
194
+ audioFocusRequest?.let {
195
+ val audioManager = (NitroModules.applicationContext ?: carContext)?.getSystemService(
196
+ AudioManager::class.java,
197
+ )
198
+ audioManager?.abandonAudioFocusRequest(it)
199
+ }
200
+ audioFocusRequest = null
201
+ }
202
+
203
+ fun dispose() {
204
+ stop()
205
+ scope.cancel()
206
+ }
207
+
208
+ companion object {
209
+ private const val SILENCE_AMPLITUDE_THRESHOLD = 500
210
+ private const val WARMUP_MS = 500L
211
+ private const val SAMPLE_RATE = 16_000
212
+ private const val PHONE_BUFFER_SIZE = 3_200 // ~100ms at 16kHz/16-bit/mono
213
+ }
214
+ }
@@ -66,6 +66,10 @@ class MapTemplate(
66
66
  navigationManager.setNavigationManagerCallback(navigationManagerCallback)
67
67
  }
68
68
  }
69
+
70
+ config.defaultGuidanceBackgroundColor?.let { nitroColor ->
71
+ MapTemplate.cardBackgroundColor = Parser.parseColor(nitroColor)
72
+ }
69
73
  }
70
74
 
71
75
  override fun parse(): Template {
@@ -329,7 +333,9 @@ class MapTemplate(
329
333
  }
330
334
 
331
335
  if (loadingInfo != null) {
336
+ cardBackgroundColor = Parser.parseColor(loadingInfo.cardBackgroundColor)
332
337
  navigationInfo = RoutingInfo.Builder().setLoading(true).build()
338
+ AndroidAutoScreen.invalidateSurfaceScreens()
333
339
  return
334
340
  }
335
341
 
@@ -371,7 +377,8 @@ class MapTemplate(
371
377
  val notificationIcon = Parser.parseImageToBitmap(
372
378
  context,
373
379
  current.symbolImage.asFirstOrNull(),
374
- current.symbolImage.asSecondOrNull()
380
+ current.symbolImage.asSecondOrNull(),
381
+ current.symbolImage.asThirdOrNull()
375
382
  )
376
383
 
377
384
  val notificationText = currentStep.cue?.toString()