@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.
- package/README.md +60 -2
- package/android/src/automotive/AndroidManifest.xml +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +8 -1
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +108 -38
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
- package/ios/extensions/NitroImageExtensions.swift +10 -1
- package/ios/hybrid/HybridAutoPlay.swift +51 -4
- package/ios/hybrid/HybridMapTemplate.swift +2 -5
- package/ios/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +55 -0
- package/ios/templates/Parser.swift +109 -11
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/templates/MapTemplate.d.ts +7 -1
- package/lib/templates/MapTemplate.js +10 -2
- package/lib/types/Image.d.ts +13 -0
- package/lib/types/Maneuver.d.ts +6 -0
- package/lib/utils/NitroImage.d.ts +6 -1
- package/lib/utils/NitroImage.js +7 -0
- package/lib/utils/NitroManeuver.d.ts +2 -0
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +8 -2
- package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
- package/nitrogen/generated/android/c++/JNitroImage.hpp +19 -2
- package/nitrogen/generated/android/c++/JNitroLoadingManeuver.hpp +15 -4
- package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
- package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
- package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MapTemplateConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroLoadingManeuver.kt +9 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/MapTemplateConfig.swift +12 -1
- package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
- package/nitrogen/generated/ios/swift/NitroLoadingManeuver.swift +25 -2
- package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
- package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
- package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
- package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/MapTemplateConfig.hpp +8 -1
- package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroLoadingManeuver.hpp +15 -4
- package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
- package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
- package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
- package/package.json +1 -1
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/templates/MapTemplate.ts +23 -2
- package/src/types/Image.ts +14 -0
- package/src/types/Maneuver.ts +6 -0
- package/src/utils/NitroImage.ts +15 -1
- package/src/utils/NitroManeuver.ts +2 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
- 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()`:
|
|
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" />
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt
CHANGED
|
@@ -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
|
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt
ADDED
|
@@ -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()
|