@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
package/README.md CHANGED
@@ -304,7 +304,49 @@ When using build variants, Android Studio may not be aware of the selected varia
304
304
  To work around this and allow for debugging or enhancing the Android Automotive-specific implementation, you can temporarily set the automotive flags in your `gradle.properties` file or your default `.env` file before running a Gradle sync.
305
305
 
306
306
  ## Icons
307
- The library is using [Material Symbols](https://fonts.google.com/icons) for iconography. The font is bundled with the library, so no extra setup is required. You can use these icons on both Android Auto and CarPlay.
307
+ The library does **not** bundle any icon font the consuming app must provide one.
308
+
309
+ ### Setup
310
+
311
+ 1. Add a `.ttf` font file to your native projects:
312
+ - **iOS** — add `<name>.ttf` to your app bundle (no `UIAppFonts` entry needed — the library registers it via CoreText automatically).
313
+ - **Android** — place `<name>.ttf` in `res/font/`.
314
+
315
+ For cross-platform compatibility use **lowercase names with underscores only** (e.g. `material_symbols`).
316
+
317
+ 2. Register the font and an optional glyph map at startup:
318
+
319
+ ```ts
320
+ import { setIconFont } from '@iternio/react-native-auto-play';
321
+ import { glyphMap } from './assets/Glyphmap';
322
+
323
+ setIconFont('material_symbols', glyphMap);
324
+ ```
325
+
326
+ 3. Use glyph images by name or code point:
327
+
328
+ ```ts
329
+ { type: 'glyph', name: 'directions_car' }
330
+ { type: 'glyph', codepoint: 0xe531 }
331
+ ```
332
+
333
+ `setIconFont` must be called once before the first glyph is used (subsequent calls are ignored). If no font is registered, the library throws an error when a glyph image is rendered.
334
+
335
+ ### Type-safe glyph names
336
+
337
+ To get autocompletion and type checking for glyph names, create a declaration file in your app (e.g. `autoplay-glyphs.d.ts`):
338
+
339
+ ```ts
340
+ import type { GlyphName } from './assets/Glyphmap';
341
+
342
+ declare module '@iternio/react-native-auto-play' {
343
+ interface AutoPlayGlyphMap extends Record<GlyphName, number> {}
344
+ }
345
+ ```
346
+
347
+ Without this augmentation, `name` accepts any `string`. With it, only keys from your glyph map are allowed and you get full autocompletion.
348
+
349
+ The example app uses [Material Symbols](https://fonts.google.com/icons). See `apps/example/assets/symbolFont/` for the glyph map generation script.
308
350
 
309
351
  It is also possible to use custom bundled images (e.g. PNG, WEBP or Vector Drawables). Make sure to add them to your native projects.
310
352
  - iOS: Add to your `Images.xcassets`
@@ -613,7 +655,7 @@ This section lists the available listeners and lifecycle callbacks so you can wi
613
655
  | --- | --- | --- |
614
656
  | `HybridAutoPlay.addListener(event, cb)` | event: `'didConnect'` `'didDisconnect'` | Connection changes for the head unit. |
615
657
  | `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. |
658
+ | `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
659
  | `HybridAutoPlay.addSafeAreaInsetsListener(moduleName, cb)` | `cb(insets)` | Safe area inset changes for any module. |
618
660
 
619
661
  ```ts
@@ -760,10 +802,68 @@ new ListTemplate({
760
802
  }).push();
761
803
  ```
762
804
 
805
+ ### Voice Input
806
+
807
+ 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).
808
+
809
+ #### Permission
810
+
811
+ ```ts
812
+ // Check whether permission is already granted
813
+ const granted = HybridAutoPlay.hasVoiceInputPermission();
814
+
815
+ // Request permission if not yet granted
816
+ const granted = await HybridAutoPlay.requestVoiceInputPermission();
817
+ ```
818
+
819
+ #### Recording
820
+
821
+ ```ts
822
+ // Start recording — resolves with a raw PCM ArrayBuffer (16 kHz, 16-bit, mono)
823
+ // Recording stops automatically on silence or when maxDurationMs is reached
824
+ const pcmBuffer = await HybridAutoPlay.startVoiceInput(
825
+ 1500, // silenceThresholdMs (default 1500)
826
+ 10_000, // maxDurationMs (default 10 000)
827
+ 'Listening...' // text shown on the car screen while recording (iOS CarPlay)
828
+ );
829
+
830
+ // Stop recording early — resolves startVoiceInput with the audio captured so far
831
+ HybridAutoPlay.stopVoiceInput();
832
+ ```
833
+
834
+ On **Android**: uses `CarAudioRecord` when Android Auto is connected, otherwise falls back to standard `AudioRecord`.
835
+
836
+ On **iOS**: presents `CPVoiceControlTemplate` on the car screen when CarPlay is connected, and captures audio via `AVAudioEngine`.
837
+
838
+ #### OS-triggered voice input (Android only)
839
+
840
+ `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.
841
+
842
+ ```ts
843
+ const cleanup = HybridAutoPlay.addListenerVoiceInput((location, query) => {
844
+ console.log('Voice query:', query, 'near', location);
845
+ });
846
+ ```
847
+
848
+ #### useVoiceInput hook (Android only)
849
+
850
+ A convenience hook that wires up `addListenerVoiceInput` and exposes the latest `location` and `query` values reactively.
851
+
852
+ ```tsx
853
+ import { useVoiceInput } from '@iternio/react-native-auto-play';
854
+
855
+ const MyScreen = () => {
856
+ const { location, query } = useVoiceInput();
857
+ return <Text>{query ?? 'Say something…'}</Text>;
858
+ };
859
+ ```
860
+
861
+ ---
862
+
763
863
  ### Hooks
764
864
 
765
865
  - `useMapTemplate()`: Get a reference to the parent `MapTemplate` instance.
766
- - `useVoiceInput()`: Access voice input functionality - Android Auto only.
866
+ - `useVoiceInput()`: Reactively exposes the latest OS-triggered voice input (`location`, `query`). Android only — for in-app recording use `startVoiceInput` / `stopVoiceInput` directly.
767
867
  - `useSafeAreaInsets()`: Get safe area insets for any root component.
768
868
  - `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
869
  - `useAndroidAutoTelemetry()`: Access to car telemetry data on Android Auto and Android Automotive.
@@ -25,10 +25,6 @@ Pod::Spec.new do |s|
25
25
  # react helpers like RCTConvert
26
26
  s.public_header_files = Array(s.attributes_hash['public_header_files']) + ["ios/ReactHelpers/*.h"]
27
27
 
28
- s.resource_bundles = {
29
- "ReactNativeAutoPlay" => ['ios/Assets/**/*.ttf']
30
- }
31
-
32
28
  s.pod_target_xcconfig = {
33
29
  # C++ compiler flags, mainly for folly.
34
30
  "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES"
@@ -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" />
@@ -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
+ }
@@ -377,7 +377,8 @@ class MapTemplate(
377
377
  val notificationIcon = Parser.parseImageToBitmap(
378
378
  context,
379
379
  current.symbolImage.asFirstOrNull(),
380
- current.symbolImage.asSecondOrNull()
380
+ current.symbolImage.asSecondOrNull(),
381
+ current.symbolImage.asThirdOrNull()
381
382
  )
382
383
 
383
384
  val notificationText = currentStep.cue?.toString()