@iternio/react-native-auto-play 0.3.11 → 0.3.13
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 +103 -3
- package/ReactNativeAutoPlay.podspec +0 -4
- 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/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 +2 -5
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +117 -38
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt +29 -30
- package/ios/extensions/NitroImageExtensions.swift +10 -1
- package/ios/hybrid/HybridAutoPlay.swift +51 -4
- package/ios/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +14 -0
- package/ios/templates/Parser.swift +91 -4
- package/ios/utils/SymbolFont.swift +44 -44
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/templates/MapTemplate.d.ts +4 -1
- package/lib/types/Image.d.ts +46 -4
- package/lib/types/Maneuver.d.ts +2 -10
- package/lib/utils/NitroImage.d.ts +29 -3
- package/lib/utils/NitroImage.js +64 -3
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JGlyphImage.hpp +6 -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 +3 -1
- 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 +20 -3
- 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 +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +5 -2
- 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/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/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 +4 -1
- 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/GlyphImage.swift +7 -2
- 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/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/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++/GlyphImage.hpp +6 -1
- 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++/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++/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 +2 -3
- package/src/index.ts +1 -0
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/templates/MapTemplate.ts +4 -1
- package/src/types/Image.ts +65 -16
- package/src/types/Maneuver.ts +3 -10
- package/src/utils/NitroImage.ts +81 -6
- package/android/src/main/res/font/materialsymbolsoutlined_regular.ttf +0 -0
- package/ios/Assets/MaterialSymbolsOutlined-Regular.ttf +0 -0
- package/lib/types/Glyphmap.d.ts +0 -4105
- package/lib/types/Glyphmap.js +0 -4105
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
- 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
|
|
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()`:
|
|
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" />
|
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,10 +66,6 @@ class MapTemplate(
|
|
|
66
66
|
navigationManager.setNavigationManagerCallback(navigationManagerCallback)
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
config.defaultGuidanceBackgroundColor?.let { nitroColor ->
|
|
71
|
-
MapTemplate.cardBackgroundColor = Parser.parseColor(nitroColor)
|
|
72
|
-
}
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
override fun parse(): Template {
|
|
@@ -377,7 +373,8 @@ class MapTemplate(
|
|
|
377
373
|
val notificationIcon = Parser.parseImageToBitmap(
|
|
378
374
|
context,
|
|
379
375
|
current.symbolImage.asFirstOrNull(),
|
|
380
|
-
current.symbolImage.asSecondOrNull()
|
|
376
|
+
current.symbolImage.asSecondOrNull(),
|
|
377
|
+
current.symbolImage.asThirdOrNull()
|
|
381
378
|
)
|
|
382
379
|
|
|
383
380
|
val notificationText = currentStep.cue?.toString()
|