@iternio/react-native-auto-play 0.4.9 → 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.
- package/README.md +0 -26
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +89 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +20 -286
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/ThreadUtil.kt +13 -6
- package/ios/hybrid/HybridAutoPlay.swift +47 -2
- package/ios/utils/VoiceInputManager.swift +40 -144
- package/lib/index.d.ts +1 -3
- package/lib/index.js +1 -2
- package/lib/specs/AutoPlay.nitro.d.ts +29 -0
- package/nitro.json +0 -10
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +0 -2
- package/nitrogen/generated/android/ReactNativeAutoPlayOnLoad.cpp +0 -18
- package/nitrogen/generated/android/c++/JActiveCarUxRestrictions.hpp +8 -8
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JHybridAndroidAutoTelemetrySpec.cpp +4 -4
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +47 -4
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -4
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -4
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +4 -4
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +15 -15
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JLaneGuidance.hpp +16 -16
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +8 -8
- package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JNitroManeuver.cpp +4 -4
- package/nitrogen/generated/android/c++/JNitroManeuver.hpp +4 -4
- package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +16 -16
- package/nitrogen/generated/android/c++/JNitroSection.hpp +8 -8
- package/nitrogen/generated/android/c++/JPermissionRequestResult.hpp +16 -16
- package/nitrogen/generated/android/c++/JRouteChoice.hpp +32 -32
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +8 -8
- package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +16 -16
- package/nitrogen/generated/android/c++/JTripsConfig.hpp +8 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ActiveCarUxRestrictions.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AppFocusState.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AssetImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/AutoText.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/BooleanTelemetryItem.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Distance.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/DurationWithTimeZone.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GoogleSignIn.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GoogleSignInAccount.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +33 -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/ImageLane.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/InformationTemplateConfig.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/InputSignIn.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/LaneGuidance.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListTemplateConfig.kt +33 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Location.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MapTemplateConfig.kt +51 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NavigationAlertAction.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAttributedString.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAttributedStringImage.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroBaseMapTemplateConfig.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroColor.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroGridButton.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroLoadingManeuver.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroManeuver.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMapButton.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroSection.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NumericTelemetryItem.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PermissionRequestResult.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PinSignIn.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Point.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/PreferredImageLane.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/QrSignIn.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RouteChoice.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SafeAreaInsets.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SearchTemplateConfig.kt +39 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/SignInTemplateConfig.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/StringTelemetryItem.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Telemetry.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TravelEstimates.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripConfig.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripPoint.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripPreviewTextConfiguration.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripSelectorCallback.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/TripsConfig.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_GlyphImage_AssetImage_RemoteImage.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_PreferredImageLane_ImageLane.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Variant_QrSignIn_PinSignIn_InputSignIn_GoogleSignIn.kt +20 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VehicleTelemetryItem.kt +19 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay+autolinking.rb +2 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -41
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +126 -201
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +0 -11
- package/nitrogen/generated/ios/ReactNativeAutoPlayAutolinking.mm +0 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlayAutolinking.swift +0 -12
- package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +34 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/{Func_void_VoiceInputResult.swift → Func_void_std__shared_ptr_ArrayBuffer_.swift} +10 -10
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/NitroImage.swift +13 -0
- package/nitrogen/generated/ios/swift/NitroManeuver.swift +13 -0
- package/nitrogen/generated/ios/swift/Variant_GlyphImage_AssetImage_RemoteImage.swift +13 -0
- package/nitrogen/generated/ios/swift/Variant_PreferredImageLane_ImageLane.swift +12 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
- package/package.json +2 -2
- package/src/index.ts +1 -3
- package/src/specs/AutoPlay.nitro.ts +37 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoice.kt +0 -97
- package/ios/hybrid/HybridVoice.swift +0 -65
- package/lib/HybridAutoPlay.d.ts +0 -2
- package/lib/HybridAutoPlay.js +0 -2
- package/lib/components/OnAppearedChildRenderer.d.ts +0 -10
- package/lib/components/OnAppearedChildRenderer.js +0 -26
- package/lib/hooks/useIsAutoPlayFocused.d.ts +0 -7
- package/lib/hooks/useIsAutoPlayFocused.js +0 -20
- package/lib/hybrid/HybridVoice.d.ts +0 -52
- package/lib/hybrid/HybridVoice.js +0 -52
- package/lib/hybrid.d.ts +0 -2
- package/lib/hybrid.js +0 -2
- package/lib/specs/AutomotivePermissionRequestTemplate.d.ts +0 -11
- package/lib/specs/AutomotivePermissionRequestTemplate.js +0 -1
- package/lib/specs/AutomotivePermissionRequestTemplate.nitro.d.ts +0 -11
- package/lib/specs/AutomotivePermissionRequestTemplate.nitro.js +0 -1
- package/lib/specs/Voice.nitro.d.ts +0 -11
- package/lib/specs/Voice.nitro.js +0 -1
- package/lib/templates/AutomotivePermissionRequestTemplate.d.ts +0 -23
- package/lib/templates/AutomotivePermissionRequestTemplate.js +0 -18
- package/lib/types/Glyphmap.d.ts +0 -4105
- package/lib/types/Glyphmap.js +0 -4105
- package/lib/types/Voice.d.ts +0 -16
- package/lib/types/Voice.js +0 -1
- package/nitrogen/generated/android/c++/JFunc_void_VoiceInputChunk.hpp +0 -81
- package/nitrogen/generated/android/c++/JHybridVoiceSpec.cpp +0 -104
- package/nitrogen/generated/android/c++/JHybridVoiceSpec.hpp +0 -66
- package/nitrogen/generated/android/c++/JVoiceInputChunk.hpp +0 -64
- package/nitrogen/generated/android/c++/JVoiceInputResult.hpp +0 -64
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_VoiceInputChunk.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoiceSpec.kt +0 -72
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputChunk.kt +0 -41
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputResult.kt +0 -41
- package/nitrogen/generated/ios/c++/HybridVoiceSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridVoiceSpecSwift.hpp +0 -116
- package/nitrogen/generated/ios/swift/Func_void_VoiceInputChunk.swift +0 -46
- package/nitrogen/generated/ios/swift/HybridVoiceSpec.swift +0 -58
- package/nitrogen/generated/ios/swift/HybridVoiceSpec_cxx.swift +0 -234
- package/nitrogen/generated/ios/swift/VoiceInputChunk.swift +0 -60
- package/nitrogen/generated/ios/swift/VoiceInputResult.swift +0 -60
- package/nitrogen/generated/shared/c++/HybridVoiceSpec.cpp +0 -24
- package/nitrogen/generated/shared/c++/HybridVoiceSpec.hpp +0 -73
- package/nitrogen/generated/shared/c++/VoiceInputChunk.hpp +0 -89
- package/nitrogen/generated/shared/c++/VoiceInputResult.hpp +0 -89
- package/src/hybrid/HybridVoice.ts +0 -79
- package/src/specs/Voice.nitro.ts +0 -16
- package/src/types/Voice.ts +0 -18
package/README.md
CHANGED
|
@@ -314,32 +314,6 @@ The library does **not** bundle any icon font — the consuming app must provide
|
|
|
314
314
|
|
|
315
315
|
For cross-platform compatibility use **lowercase names with underscores only** (e.g. `material_symbols`).
|
|
316
316
|
|
|
317
|
-
**or**
|
|
318
|
-
|
|
319
|
-
1. use expo-font
|
|
320
|
-
```js
|
|
321
|
-
[
|
|
322
|
-
'expo-font',
|
|
323
|
-
{
|
|
324
|
-
android: {
|
|
325
|
-
fonts: [
|
|
326
|
-
{
|
|
327
|
-
fontFamily: 'MaterialSymbols',
|
|
328
|
-
fontDefinitions: [
|
|
329
|
-
{
|
|
330
|
-
path: './assets/fonts/material_symbols.ttf',
|
|
331
|
-
weight: 800,
|
|
332
|
-
},
|
|
333
|
-
],
|
|
334
|
-
},
|
|
335
|
-
],
|
|
336
|
-
},
|
|
337
|
-
ios: ['./assets/fonts/material_symbols.ttf'],
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
```
|
|
341
|
-
For cross-platform compatibility use **lowercase names with underscores only** (e.g. `material_symbols`).
|
|
342
|
-
|
|
343
317
|
2. Register the font and an optional glyph map at startup:
|
|
344
318
|
|
|
345
319
|
```ts
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt
CHANGED
|
@@ -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
|
|
@@ -247,6 +255,84 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
|
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
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
|
+
|
|
250
336
|
companion object {
|
|
251
337
|
const val TAG = "HybridAutoPlay"
|
|
252
338
|
|
|
@@ -257,6 +343,9 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
|
|
|
257
343
|
|
|
258
344
|
private val voiceInputListeners = CopyOnWriteArrayList<(Location?, String?) -> Unit>()
|
|
259
345
|
|
|
346
|
+
@Volatile
|
|
347
|
+
private var voiceInputManager: VoiceInputManager? = null
|
|
348
|
+
|
|
260
349
|
private val safeAreaInsetsListeners =
|
|
261
350
|
ConcurrentHashMap<String, CopyOnWriteArrayList<(SafeAreaInsets) -> Unit>>()
|
|
262
351
|
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
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
|
}
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/ThreadUtil.kt
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
package com.margelo.nitro.swe.iternio.reactnativeautoplay.utils
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import kotlinx.coroutines.
|
|
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:
|
|
8
|
-
|
|
9
|
-
|
|
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()
|
|
@@ -123,10 +123,55 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
123
123
|
func addListenerVoiceInput(
|
|
124
124
|
callback: @escaping (Location?, String?) -> Void
|
|
125
125
|
) throws -> () -> Void {
|
|
126
|
-
// iOS does not use the OS-triggered voice input path — use
|
|
126
|
+
// iOS does not use the OS-triggered voice input path — use startVoiceInput() instead.
|
|
127
127
|
return {}
|
|
128
128
|
}
|
|
129
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
|
+
|
|
130
175
|
// MARK: set/push/pop templates
|
|
131
176
|
func setRootTemplate(templateId: String) throws -> Promise<Void> {
|
|
132
177
|
return Promise.async {
|