@iternio/react-native-auto-play 0.4.8 → 0.4.11

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 (165) hide show
  1. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +93 -0
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +20 -286
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/ThreadUtil.kt +13 -6
  4. package/ios/hybrid/HybridAutoPlay.swift +51 -2
  5. package/ios/utils/VoiceInputManager.swift +40 -144
  6. package/lib/index.d.ts +1 -3
  7. package/lib/index.js +1 -2
  8. package/lib/scenes/CarPlayDashboardScene.d.ts +1 -0
  9. package/lib/scenes/CarPlayDashboardScene.js +13 -7
  10. package/lib/specs/AutoPlay.nitro.d.ts +35 -0
  11. package/nitro.json +0 -10
  12. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +0 -2
  13. package/nitrogen/generated/android/ReactNativeAutoPlayOnLoad.cpp +0 -18
  14. package/nitrogen/generated/android/c++/JActiveCarUxRestrictions.hpp +8 -8
  15. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +16 -16
  16. package/nitrogen/generated/android/c++/JHybridAndroidAutoTelemetrySpec.cpp +4 -4
  17. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +52 -4
  18. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +5 -0
  19. package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -4
  20. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -4
  21. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +4 -4
  22. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +15 -15
  23. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +16 -16
  24. package/nitrogen/generated/android/c++/JLaneGuidance.hpp +16 -16
  25. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +16 -16
  26. package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +16 -16
  27. package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +16 -16
  28. package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +8 -8
  29. package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +16 -16
  30. package/nitrogen/generated/android/c++/JNitroManeuver.cpp +4 -4
  31. package/nitrogen/generated/android/c++/JNitroManeuver.hpp +4 -4
  32. package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +16 -16
  33. package/nitrogen/generated/android/c++/JNitroSection.hpp +8 -8
  34. package/nitrogen/generated/android/c++/JPermissionRequestResult.hpp +16 -16
  35. package/nitrogen/generated/android/c++/JRouteChoice.hpp +32 -32
  36. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +8 -8
  37. package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +16 -16
  38. package/nitrogen/generated/android/c++/JTripsConfig.hpp +8 -8
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ActiveCarUxRestrictions.kt +19 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AppFocusState.kt +15 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AssetImage.kt +23 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AutoText.kt +17 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/BooleanTelemetryItem.kt +15 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Distance.kt +15 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/DurationWithTimeZone.kt +15 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +21 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GoogleSignIn.kt +19 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GoogleSignInAccount.kt +27 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +33 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +21 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ImageLane.kt +15 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/InformationTemplateConfig.kt +35 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/InputSignIn.kt +27 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/LaneGuidance.kt +15 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListTemplateConfig.kt +33 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Location.kt +15 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MapTemplateConfig.kt +51 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +37 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NavigationAlertAction.kt +17 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +27 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAttributedString.kt +15 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAttributedStringImage.kt +15 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroBaseMapTemplateConfig.kt +29 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroColor.kt +15 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroGridButton.kt +17 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +18 -8
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroLoadingManeuver.kt +17 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroManeuver.kt +18 -8
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMapButton.kt +17 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +19 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +31 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +49 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +27 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroSection.kt +17 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NumericTelemetryItem.kt +15 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PermissionRequestResult.kt +15 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PinSignIn.kt +15 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Point.kt +15 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PreferredImageLane.kt +19 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/QrSignIn.kt +15 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +17 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RouteChoice.kt +21 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SafeAreaInsets.kt +21 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SearchTemplateConfig.kt +39 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SignInTemplateConfig.kt +37 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/StringTelemetryItem.kt +15 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Telemetry.kt +35 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TravelEstimates.kt +19 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripConfig.kt +15 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripPoint.kt +19 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripPreviewTextConfiguration.kt +19 -0
  92. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripSelectorCallback.kt +13 -0
  93. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripsConfig.kt +15 -0
  94. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_GlyphImage_AssetImage_RemoteImage.kt +18 -8
  95. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_PreferredImageLane_ImageLane.kt +16 -7
  96. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_QrSignIn_PinSignIn_InputSignIn_GoogleSignIn.kt +20 -9
  97. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VehicleTelemetryItem.kt +19 -0
  98. package/nitrogen/generated/ios/ReactNativeAutoPlay+autolinking.rb +2 -0
  99. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -41
  100. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +126 -201
  101. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +0 -11
  102. package/nitrogen/generated/ios/ReactNativeAutoPlayAutolinking.mm +0 -8
  103. package/nitrogen/generated/ios/ReactNativeAutoPlayAutolinking.swift +0 -12
  104. package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +42 -0
  105. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  106. package/nitrogen/generated/ios/swift/{Func_void_VoiceInputResult.swift → Func_void_std__shared_ptr_ArrayBuffer_.swift} +10 -10
  107. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +5 -0
  108. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +94 -0
  109. package/nitrogen/generated/ios/swift/NitroImage.swift +13 -0
  110. package/nitrogen/generated/ios/swift/NitroManeuver.swift +13 -0
  111. package/nitrogen/generated/ios/swift/Variant_GlyphImage_AssetImage_RemoteImage.swift +13 -0
  112. package/nitrogen/generated/ios/swift/Variant_PreferredImageLane_ImageLane.swift +12 -0
  113. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +5 -0
  114. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +6 -0
  115. package/package.json +2 -2
  116. package/src/index.ts +1 -3
  117. package/src/scenes/CarPlayDashboardScene.ts +18 -11
  118. package/src/specs/AutoPlay.nitro.ts +44 -0
  119. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoice.kt +0 -97
  120. package/ios/hybrid/HybridVoice.swift +0 -65
  121. package/lib/HybridAutoPlay.d.ts +0 -2
  122. package/lib/HybridAutoPlay.js +0 -2
  123. package/lib/components/OnAppearedChildRenderer.d.ts +0 -10
  124. package/lib/components/OnAppearedChildRenderer.js +0 -26
  125. package/lib/hooks/useIsAutoPlayFocused.d.ts +0 -7
  126. package/lib/hooks/useIsAutoPlayFocused.js +0 -20
  127. package/lib/hybrid/HybridVoice.d.ts +0 -52
  128. package/lib/hybrid/HybridVoice.js +0 -52
  129. package/lib/hybrid.d.ts +0 -2
  130. package/lib/hybrid.js +0 -2
  131. package/lib/specs/AutomotivePermissionRequestTemplate.d.ts +0 -11
  132. package/lib/specs/AutomotivePermissionRequestTemplate.js +0 -1
  133. package/lib/specs/AutomotivePermissionRequestTemplate.nitro.d.ts +0 -11
  134. package/lib/specs/AutomotivePermissionRequestTemplate.nitro.js +0 -1
  135. package/lib/specs/Voice.nitro.d.ts +0 -11
  136. package/lib/specs/Voice.nitro.js +0 -1
  137. package/lib/templates/AutomotivePermissionRequestTemplate.d.ts +0 -23
  138. package/lib/templates/AutomotivePermissionRequestTemplate.js +0 -18
  139. package/lib/types/Glyphmap.d.ts +0 -4105
  140. package/lib/types/Glyphmap.js +0 -4105
  141. package/lib/types/Voice.d.ts +0 -16
  142. package/lib/types/Voice.js +0 -1
  143. package/nitrogen/generated/android/c++/JFunc_void_VoiceInputChunk.hpp +0 -81
  144. package/nitrogen/generated/android/c++/JHybridVoiceSpec.cpp +0 -104
  145. package/nitrogen/generated/android/c++/JHybridVoiceSpec.hpp +0 -66
  146. package/nitrogen/generated/android/c++/JVoiceInputChunk.hpp +0 -64
  147. package/nitrogen/generated/android/c++/JVoiceInputResult.hpp +0 -64
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_VoiceInputChunk.kt +0 -80
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoiceSpec.kt +0 -72
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputChunk.kt +0 -41
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputResult.kt +0 -41
  152. package/nitrogen/generated/ios/c++/HybridVoiceSpecSwift.cpp +0 -11
  153. package/nitrogen/generated/ios/c++/HybridVoiceSpecSwift.hpp +0 -116
  154. package/nitrogen/generated/ios/swift/Func_void_VoiceInputChunk.swift +0 -46
  155. package/nitrogen/generated/ios/swift/HybridVoiceSpec.swift +0 -58
  156. package/nitrogen/generated/ios/swift/HybridVoiceSpec_cxx.swift +0 -234
  157. package/nitrogen/generated/ios/swift/VoiceInputChunk.swift +0 -60
  158. package/nitrogen/generated/ios/swift/VoiceInputResult.swift +0 -60
  159. package/nitrogen/generated/shared/c++/HybridVoiceSpec.cpp +0 -24
  160. package/nitrogen/generated/shared/c++/HybridVoiceSpec.hpp +0 -73
  161. package/nitrogen/generated/shared/c++/VoiceInputChunk.hpp +0 -89
  162. package/nitrogen/generated/shared/c++/VoiceInputResult.hpp +0 -89
  163. package/src/hybrid/HybridVoice.ts +0 -79
  164. package/src/specs/Voice.nitro.ts +0 -16
  165. package/src/types/Voice.ts +0 -18
@@ -1,11 +1,19 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
2
 
3
+ import android.content.pm.PackageManager
3
4
  import android.os.Build
5
+ import androidx.core.content.ContextCompat
4
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
5
11
  import com.margelo.nitro.core.Promise
6
12
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.AndroidAutoTemplate
7
13
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.MessageTemplate
8
14
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.ThreadUtil
15
+ import kotlinx.coroutines.suspendCancellableCoroutine
16
+ import java.nio.ByteBuffer
9
17
  import java.util.concurrent.ConcurrentHashMap
10
18
  import java.util.concurrent.CopyOnWriteArrayList
11
19
  import kotlin.coroutines.resume
@@ -37,6 +45,10 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
37
45
  return AndroidAutoSession.getIsConnected()
38
46
  }
39
47
 
48
+ override fun isCarServiceRunning(): Boolean {
49
+ return AndroidAutoService.instance != null
50
+ }
51
+
40
52
  override fun addListenerRenderState(
41
53
  moduleName: String, callback: (VisibilityState) -> Unit
42
54
  ): () -> Unit {
@@ -243,6 +255,84 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
243
255
  }
244
256
  }
245
257
 
258
+ override fun hasVoiceInputPermission(): Boolean {
259
+ val context = NitroModules.applicationContext ?: return false
260
+ return ContextCompat.checkSelfPermission(
261
+ context, android.Manifest.permission.RECORD_AUDIO
262
+ ) == PackageManager.PERMISSION_GRANTED
263
+ }
264
+
265
+ override fun requestVoiceInputPermission(): Promise<Boolean> {
266
+ return Promise.async {
267
+ if (hasVoiceInputPermission()) {
268
+ return@async true
269
+ }
270
+
271
+ val carContext = AndroidAutoSession.getRootContext()
272
+
273
+ if (carContext != null) {
274
+ suspendCancellableCoroutine {
275
+ carContext.requestPermissions(listOf(android.Manifest.permission.RECORD_AUDIO)) { approved, _ ->
276
+ it.resume(approved.contains(android.Manifest.permission.RECORD_AUDIO))
277
+ }
278
+ }
279
+ } else {
280
+ val context = NitroModules.applicationContext ?: return@async false
281
+ val activity =
282
+ context.currentActivity as? PermissionAwareActivity ?: return@async false
283
+ val code = (Math.random() * 10000).toInt()
284
+
285
+ suspendCancellableCoroutine {
286
+ activity.requestPermissions(
287
+ arrayOf(android.Manifest.permission.RECORD_AUDIO),
288
+ code,
289
+ PermissionListener { requestCode, _, grantResults ->
290
+ if (requestCode != code) {
291
+ return@PermissionListener false
292
+ }
293
+
294
+ val granted =
295
+ grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED
296
+
297
+ it.resume(granted)
298
+
299
+ return@PermissionListener true
300
+ })
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ override fun startVoiceInput(
307
+ silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?
308
+ ): Promise<ArrayBuffer> {
309
+ return Promise.async {
310
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
311
+ throw UnsupportedOperationException("startVoiceInput requires at least API level ${Build.VERSION_CODES.O}")
312
+ }
313
+
314
+ val manager = VoiceInputManager(AndroidAutoSession.getRootContext())
315
+ voiceInputManager = manager
316
+
317
+ try {
318
+ val pcmBytes = manager.start(
319
+ silenceThresholdMs = silenceThresholdMs?.toLong() ?: 1_500L,
320
+ maxDurationMs = maxDurationMs?.toLong() ?: 10_000L,
321
+ )
322
+ val directBuffer =
323
+ ByteBuffer.allocateDirect(pcmBytes.size).put(pcmBytes).rewind() as ByteBuffer
324
+ ArrayBuffer.wrap(directBuffer)
325
+ } finally {
326
+ voiceInputManager = null
327
+ manager.dispose()
328
+ }
329
+ }
330
+ }
331
+
332
+ override fun stopVoiceInput() {
333
+ voiceInputManager?.stop()
334
+ }
335
+
246
336
  companion object {
247
337
  const val TAG = "HybridAutoPlay"
248
338
 
@@ -253,6 +343,9 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
253
343
 
254
344
  private val voiceInputListeners = CopyOnWriteArrayList<(Location?, String?) -> Unit>()
255
345
 
346
+ @Volatile
347
+ private var voiceInputManager: VoiceInputManager? = null
348
+
256
349
  private val safeAreaInsetsListeners =
257
350
  ConcurrentHashMap<String, CopyOnWriteArrayList<(SafeAreaInsets) -> Unit>>()
258
351
 
@@ -1,7 +1,5 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
2
 
3
- import android.annotation.SuppressLint
4
- import android.content.Intent
5
3
  import android.content.pm.PackageManager
6
4
  import android.media.AudioAttributes
7
5
  import android.media.AudioFocusRequest
@@ -10,28 +8,18 @@ import android.media.AudioManager
10
8
  import android.media.AudioRecord
11
9
  import android.media.MediaRecorder
12
10
  import android.os.Build
13
- import android.os.Bundle
14
- import android.os.ParcelFileDescriptor
15
- import android.speech.RecognitionListener
16
- import android.speech.RecognizerIntent
17
- import android.speech.SpeechRecognizer
18
11
  import androidx.annotation.RequiresApi
19
12
  import androidx.car.app.CarContext
20
13
  import androidx.car.app.media.CarAudioRecord
21
14
  import androidx.core.content.ContextCompat
22
- import com.facebook.react.bridge.UiThreadUtil
23
15
  import com.margelo.nitro.NitroModules
24
- import com.margelo.nitro.core.ArrayBuffer
25
- import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.ThreadUtil
26
16
  import kotlinx.coroutines.CoroutineScope
27
17
  import kotlinx.coroutines.Dispatchers
28
18
  import kotlinx.coroutines.Job
29
- import kotlinx.coroutines.async
30
19
  import kotlinx.coroutines.cancel
31
20
  import kotlinx.coroutines.launch
32
21
  import kotlinx.coroutines.suspendCancellableCoroutine
33
22
  import java.io.ByteArrayOutputStream
34
- import java.nio.ByteBuffer
35
23
  import kotlin.coroutines.Continuation
36
24
  import kotlin.coroutines.resume
37
25
  import kotlin.coroutines.resumeWithException
@@ -39,274 +27,43 @@ import kotlin.math.abs
39
27
 
40
28
  /**
41
29
  * Captures 16-bit PCM audio (16 kHz, mono).
42
- * When [carContext] is provided uses CarAudioRecord (Android Auto/Automotive),
43
- * otherwise falls back to standard AudioRecord.
44
- *
45
- * When preferSpeechToText is true and SpeechRecognizer is available, it owns
46
- * the microphone and streams partial results; the PCM path is not used.
47
- * When SpeechRecognizer is unavailable the manager falls back to PCM recording.
30
+ * When [carContext] is provided uses CarAudioRecord (Android Auto/Automotive).
31
+ * When [carContext] is null falls back to standard AudioRecord (phone-only).
48
32
  */
49
33
  class VoiceInputManager(
50
34
  private val carContext: CarContext?,
51
35
  ) {
52
- // PCM recording state
53
36
  private var carAudioRecord: CarAudioRecord? = null
54
37
  private var audioRecord: AudioRecord? = null
55
38
  private var audioFocusRequest: AudioFocusRequest? = null
56
39
  private var recordingJob: Job? = null
57
- private var pcmContinuation: Continuation<ByteArray>? = null
40
+ private var continuation: Continuation<ByteArray>? = null
58
41
  private val scope = CoroutineScope(Dispatchers.IO)
59
42
 
60
43
  @Volatile
61
44
  private var isRecording = false
62
45
 
63
- // STT state — only set when SpeechRecognizer owns the mic
64
- @Volatile
65
- private var activeSpeechRecognizer: SpeechRecognizer? = null
66
-
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
+ */
67
51
  @RequiresApi(Build.VERSION_CODES.O)
68
52
  suspend fun start(
69
53
  silenceThresholdMs: Long = 1_500,
70
54
  maxDurationMs: Long = 10_000,
71
- preferSpeechToText: Boolean = false,
72
- onChunk: ((chunk: VoiceInputChunk) -> Unit)? = null,
73
- language: String? = null
74
- ): VoiceInputResult {
75
- if (preferSpeechToText) {
76
- val context = NitroModules.applicationContext ?: throw IllegalArgumentException()
77
- if (SpeechRecognizer.isRecognitionAvailable(context)) {
78
- if (carContext != null) {
79
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
80
- return startSTTFromCarAudio(silenceThresholdMs, maxDurationMs, onChunk, language)
81
- }
82
- // Car connected but API < 33: EXTRA_AUDIO_SOURCE unavailable, fall back to PCM
83
- return startPCM(silenceThresholdMs, maxDurationMs, onChunk)
84
- }
85
- return ThreadUtil.postOnUiAndAwait { startSTT(context, onChunk, language) }.getOrThrow()
86
- }
87
- }
88
- return startPCM(silenceThresholdMs, maxDurationMs, onChunk)
89
- }
90
-
91
- // MARK: - STT path (SpeechRecognizer owns the mic)
92
-
93
- private suspend fun startSTT(
94
- context: android.content.Context,
95
- onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
96
- language: String?
97
- ): VoiceInputResult = suspendCancellableCoroutine { cont ->
98
- val recognizer = SpeechRecognizer.createSpeechRecognizer(context)
99
- activeSpeechRecognizer = recognizer
100
-
101
- recognizer.setRecognitionListener(object : RecognitionListener {
102
- override fun onResults(results: Bundle?) {
103
- activeSpeechRecognizer = null
104
- recognizer.destroy()
105
- val text =
106
- results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull()
107
- cont.resume(VoiceInputResult(transcription = text, audio = null))
108
- }
109
-
110
- override fun onError(error: Int) {
111
- activeSpeechRecognizer = null
112
- recognizer.destroy()
113
- // Return empty transcription — caller sees null audio and null transcription
114
- cont.resume(VoiceInputResult(transcription = null, audio = null))
115
- }
116
-
117
- override fun onPartialResults(partialResults: Bundle?) {
118
- val text = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
119
- ?.firstOrNull()
120
- if (!text.isNullOrEmpty()) {
121
- onChunk?.invoke(VoiceInputChunk(partial = text, audio = null))
122
- }
123
- }
124
-
125
- override fun onReadyForSpeech(params: Bundle?) {}
126
- override fun onBeginningOfSpeech() {}
127
- override fun onRmsChanged(rmsdB: Float) {}
128
- override fun onBufferReceived(buffer: ByteArray?) {}
129
- override fun onEndOfSpeech() {}
130
- override fun onEvent(eventType: Int, params: Bundle?) {}
131
- })
132
-
133
- val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
134
- putExtra(
135
- RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
136
- )
137
- putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
138
- putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
139
- language?.let {
140
- putExtra(RecognizerIntent.EXTRA_LANGUAGE, it)
141
- }
142
- }
143
-
144
- recognizer.startListening(intent)
145
-
146
- cont.invokeOnCancellation {
147
- activeSpeechRecognizer = null
148
- recognizer.destroy()
149
- }
150
- }
151
-
152
- // MARK: - STT path fed from CarAudioRecord via a pipe (API 33+)
153
- @SuppressLint("MissingPermission")
154
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
155
- private suspend fun startSTTFromCarAudio(
156
- silenceThresholdMs: Long,
157
- maxDurationMs: Long,
158
- onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
159
- language: String?
160
- ): VoiceInputResult {
161
- if (!hasVoiceInputPermission()) {
162
- throw SecurityException("RECORD_AUDIO permission not granted")
163
- }
164
-
165
- val appContext = NitroModules.applicationContext ?: throw IllegalArgumentException()
166
- val pipes = ParcelFileDescriptor.createPipe()
167
- val readFd = pipes[0]
168
- val pipeOut = ParcelFileDescriptor.AutoCloseOutputStream(pipes[1])
169
-
170
- val sttDeferred = scope.async {
171
- ThreadUtil.postOnUiAndAwait {
172
- startSTTWithSource(appContext, readFd, silenceThresholdMs, onChunk, language)
173
- }.getOrThrow()
174
- }
175
-
176
- try {
177
- recordPCM(silenceThresholdMs, maxDurationMs) { chunk ->
178
- chunk.audio?.let { ab ->
179
- try {
180
- pipeOut.write(ab.toByteArray())
181
- } catch (_: Exception) {
182
- }
183
- }
184
- }
185
- } finally {
186
- try {
187
- pipeOut.close()
188
- } catch (_: Exception) {
189
- }
190
- try {
191
- readFd.close()
192
- } catch (_: Exception) {
193
- }
194
- }
195
-
196
- return sttDeferred.await()
197
- }
198
-
199
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
200
- private suspend fun startSTTWithSource(
201
- context: android.content.Context,
202
- audioSource: ParcelFileDescriptor,
203
- silenceThresholdMs: Long,
204
- onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
205
- language: String?
206
- ): VoiceInputResult = suspendCancellableCoroutine { cont ->
207
- val recognizer = SpeechRecognizer.createSpeechRecognizer(context)
208
- activeSpeechRecognizer = recognizer
209
- // When EXTRA_AUDIO_SOURCE is used, onResults always returns an empty list — the actual
210
- // transcription only arrives via onPartialResults. Track the last partial here.
211
- var lastPartial: String? = null
212
-
213
- recognizer.setRecognitionListener(object : RecognitionListener {
214
- override fun onResults(results: Bundle?) {
215
- activeSpeechRecognizer = null
216
- recognizer.destroy()
217
- val text =
218
- results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull()
219
- ?: lastPartial
220
- cont.resume(VoiceInputResult(transcription = text, audio = null))
221
- }
222
-
223
- override fun onError(error: Int) {
224
- activeSpeechRecognizer = null
225
- recognizer.destroy()
226
- cont.resume(VoiceInputResult(transcription = lastPartial, audio = null))
227
- }
228
-
229
- override fun onPartialResults(partialResults: Bundle?) {
230
- val text = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
231
- ?.firstOrNull()
232
- if (!text.isNullOrEmpty()) {
233
- lastPartial = text
234
- onChunk?.invoke(VoiceInputChunk(partial = text, audio = null))
235
- }
236
- }
237
-
238
- override fun onReadyForSpeech(params: Bundle?) {}
239
- override fun onBeginningOfSpeech() {}
240
- override fun onRmsChanged(rmsdB: Float) {}
241
- override fun onBufferReceived(buffer: ByteArray?) {}
242
- override fun onEndOfSpeech() {}
243
- override fun onEvent(eventType: Int, params: Bundle?) {}
244
- })
245
-
246
- val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
247
- putExtra(
248
- RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
249
- )
250
- putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
251
- putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
252
- language?.let {
253
- putExtra(RecognizerIntent.EXTRA_LANGUAGE, it)
254
- }
255
- putExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE, audioSource)
256
- putExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE_CHANNEL_COUNT, 1)
257
- putExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
258
- putExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE_SAMPLING_RATE, SAMPLE_RATE)
259
- putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS, WARMUP_MS)
260
- putExtra(
261
- RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
262
- silenceThresholdMs
263
- )
264
- putExtra(
265
- RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
266
- silenceThresholdMs / 2,
267
- )
268
- }
269
-
270
- recognizer.startListening(intent)
271
-
272
- cont.invokeOnCancellation {
273
- activeSpeechRecognizer = null
274
- recognizer.destroy()
275
- }
276
- }
277
-
278
- // MARK: - PCM path
279
-
280
- @RequiresApi(Build.VERSION_CODES.O)
281
- private suspend fun startPCM(
282
- silenceThresholdMs: Long,
283
- maxDurationMs: Long,
284
- onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
285
- ): VoiceInputResult {
286
- val pcmBytes = recordPCM(silenceThresholdMs, maxDurationMs, onChunk)
287
- val directBuffer =
288
- ByteBuffer.allocateDirect(pcmBytes.size).put(pcmBytes).rewind() as ByteBuffer
289
- return VoiceInputResult(transcription = null, audio = ArrayBuffer.wrap(directBuffer))
290
- }
291
-
292
- @SuppressLint("MissingPermission")
293
- @RequiresApi(Build.VERSION_CODES.O)
294
- private suspend fun recordPCM(
295
- silenceThresholdMs: Long,
296
- maxDurationMs: Long,
297
- onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
298
55
  ): ByteArray = suspendCancellableCoroutine { cont ->
299
- if (!hasVoiceInputPermission()) {
56
+ val appContext = NitroModules.applicationContext
57
+ if (appContext == null || ContextCompat.checkSelfPermission(
58
+ appContext,
59
+ android.Manifest.permission.RECORD_AUDIO,
60
+ ) != PackageManager.PERMISSION_GRANTED
61
+ ) {
300
62
  cont.resumeWithException(SecurityException("RECORD_AUDIO permission not granted"))
301
63
  return@suspendCancellableCoroutine
302
64
  }
303
65
 
304
- val appContext = NitroModules.applicationContext ?: run {
305
- cont.resumeWithException(SecurityException("Missing application context"))
306
- return@suspendCancellableCoroutine
307
- }
308
-
309
- pcmContinuation = cont
66
+ continuation = cont
310
67
 
311
68
  val audioManager = appContext.getSystemService(AudioManager::class.java)
312
69
 
@@ -323,7 +80,7 @@ class VoiceInputManager(
323
80
  }.build()
324
81
 
325
82
  if (audioManager.requestAudioFocus(focusRequest) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
326
- pcmContinuation = null
83
+ continuation = null
327
84
  cont.resumeWithException(IllegalStateException("Audio focus request denied"))
328
85
  return@suspendCancellableCoroutine
329
86
  }
@@ -379,13 +136,6 @@ class VoiceInputManager(
379
136
  if (read > 0) {
380
137
  outputStream.write(buffer, 0, read)
381
138
 
382
- onChunk?.let { cb ->
383
- val chunk = ByteArray(read) { buffer[it] }
384
- val direct =
385
- ByteBuffer.allocateDirect(read).put(chunk).rewind() as ByteBuffer
386
- cb(VoiceInputChunk(partial = null, audio = ArrayBuffer.wrap(direct)))
387
- }
388
-
389
139
  val now = System.currentTimeMillis()
390
140
  val elapsedMs = now - recordingStart
391
141
 
@@ -401,9 +151,7 @@ class VoiceInputManager(
401
151
  val sample =
402
152
  (buffer[i].toInt() and 0xFF) or (buffer[i + 1].toInt() shl 8)
403
153
  val absSample = abs(sample.toShort().toInt())
404
- if (absSample > peak) {
405
- peak = absSample
406
- }
154
+ if (absSample > peak) peak = absSample
407
155
  i += 2
408
156
  }
409
157
 
@@ -422,21 +170,14 @@ class VoiceInputManager(
422
170
  }
423
171
  } finally {
424
172
  releaseResources()
425
- val captured = pcmContinuation
426
- pcmContinuation = null
427
- captured?.resume(outputStream.toByteArray())
173
+ val capturedContinuation = continuation
174
+ continuation = null
175
+ capturedContinuation?.resume(outputStream.toByteArray())
428
176
  }
429
177
  }
430
178
  }
431
179
 
432
180
  fun stop() {
433
- // STT path: stopListening() triggers onResults/onError which resolves the continuation
434
- activeSpeechRecognizer?.let { recognizer ->
435
- UiThreadUtil.runOnUiThread {
436
- recognizer.stopListening()
437
- }
438
- }
439
- // PCM path and car-audio STT pump
440
181
  isRecording = false
441
182
  carAudioRecord?.stopRecording()
442
183
  audioRecord?.stop()
@@ -469,12 +210,5 @@ class VoiceInputManager(
469
210
  private const val WARMUP_MS = 500L
470
211
  private const val SAMPLE_RATE = 16_000
471
212
  private const val PHONE_BUFFER_SIZE = 3_200 // ~100ms at 16kHz/16-bit/mono
472
-
473
- fun hasVoiceInputPermission(): Boolean {
474
- val context = NitroModules.applicationContext ?: return false
475
- return ContextCompat.checkSelfPermission(
476
- context, android.Manifest.permission.RECORD_AUDIO
477
- ) == PackageManager.PERMISSION_GRANTED
478
- }
479
213
  }
480
214
  }
@@ -1,12 +1,19 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay.utils
2
2
 
3
- import kotlinx.coroutines.Dispatchers
4
- import kotlinx.coroutines.withContext
3
+ import com.facebook.react.bridge.UiThreadUtil
4
+ import kotlinx.coroutines.suspendCancellableCoroutine
5
+ import kotlin.coroutines.resume
5
6
 
6
7
  object ThreadUtil {
7
- suspend fun <T> postOnUiAndAwait(block: suspend () -> T): Result<T> = runCatching {
8
- withContext(Dispatchers.Main) {
9
- block()
8
+ suspend fun <T> postOnUiAndAwait(block: () -> T): Result<T> =
9
+ suspendCancellableCoroutine { cont ->
10
+ UiThreadUtil.runOnUiThread {
11
+ try {
12
+ val result = block()
13
+ cont.resume(Result.success(result))
14
+ } catch (e: Exception) {
15
+ cont.resume(Result.failure(e))
16
+ }
17
+ }
10
18
  }
11
- }
12
19
  }
@@ -21,7 +21,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
21
21
  private static var listeners = [EventName: [StateListener]]()
22
22
  private static var renderStateListeners = [String: [RenderStateListener]]()
23
23
  private static var safeAreaInsetsListeners = [String: [SafeAreaListener]]()
24
-
24
+ private static var voiceInputManager: VoiceInputManager?
25
25
 
26
26
  override init() {
27
27
  HybridAutoPlay.listeners.removeAll()
@@ -83,6 +83,10 @@ class HybridAutoPlay: HybridAutoPlaySpec {
83
83
  return SceneStore.isRootModuleConnected()
84
84
  }
85
85
 
86
+ func isCarServiceRunning() throws -> Bool {
87
+ return SceneStore.isRootModuleConnected()
88
+ }
89
+
86
90
  func addSafeAreaInsetsListener(
87
91
  moduleName: String,
88
92
  callback: @escaping (SafeAreaInsets) -> Void
@@ -119,10 +123,55 @@ class HybridAutoPlay: HybridAutoPlaySpec {
119
123
  func addListenerVoiceInput(
120
124
  callback: @escaping (Location?, String?) -> Void
121
125
  ) throws -> () -> Void {
122
- // iOS does not use the OS-triggered voice input path — use HybridVoice instead.
126
+ // iOS does not use the OS-triggered voice input path — use startVoiceInput() instead.
123
127
  return {}
124
128
  }
125
129
 
130
+ func hasVoiceInputPermission() throws -> Bool {
131
+ return AVAudioSession.sharedInstance().recordPermission == .granted
132
+ }
133
+
134
+ func requestVoiceInputPermission() throws -> Promise<Bool> {
135
+ return Promise.async {
136
+ return await withCheckedContinuation { cont in
137
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
138
+ cont.resume(returning: granted)
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ func startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?) throws -> Promise<
145
+ ArrayBuffer
146
+ > {
147
+ return Promise.async {
148
+ let interfaceController = try? await RootModule.withInterfaceController { $0 }
149
+
150
+ let manager = VoiceInputManager()
151
+ HybridAutoPlay.voiceInputManager = manager
152
+
153
+ defer {
154
+ HybridAutoPlay.voiceInputManager = nil
155
+ }
156
+
157
+ let data = try await manager.start(
158
+ interfaceController: interfaceController,
159
+ silenceThresholdMs: silenceThresholdMs ?? 1_500,
160
+ maxDurationMs: maxDurationMs ?? 10_000,
161
+ listeningText: listeningText ?? "Listening..."
162
+ )
163
+
164
+ return try ArrayBuffer.copy(data: data)
165
+ }
166
+ }
167
+
168
+ func stopVoiceInput() throws {
169
+ Task { @MainActor in
170
+ let interfaceController = try? await RootModule.withInterfaceController { $0 }
171
+ HybridAutoPlay.voiceInputManager?.stop(interfaceController: interfaceController)
172
+ }
173
+ }
174
+
126
175
  // MARK: set/push/pop templates
127
176
  func setRootTemplate(templateId: String) throws -> Promise<Void> {
128
177
  return Promise.async {