@iternio/react-native-auto-play 0.3.11 → 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/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 -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/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +14 -0
- package/ios/templates/Parser.swift +91 -4
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/types/Image.d.ts +13 -0
- package/lib/utils/NitroImage.d.ts +6 -1
- package/lib/utils/NitroImage.js +7 -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 +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 +19 -2
- 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/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 +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/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++/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 +1 -1
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/types/Image.ts +14 -0
- package/src/utils/NitroImage.ts +15 -1
- 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
|
+
}
|
|
@@ -377,7 +377,8 @@ class MapTemplate(
|
|
|
377
377
|
val notificationIcon = Parser.parseImageToBitmap(
|
|
378
378
|
context,
|
|
379
379
|
current.symbolImage.asFirstOrNull(),
|
|
380
|
-
current.symbolImage.asSecondOrNull()
|
|
380
|
+
current.symbolImage.asSecondOrNull(),
|
|
381
|
+
current.symbolImage.asThirdOrNull()
|
|
381
382
|
)
|
|
382
383
|
|
|
383
384
|
val notificationText = currentStep.cue?.toString()
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt
CHANGED
|
@@ -40,6 +40,7 @@ import com.facebook.datasource.DataSources
|
|
|
40
40
|
import com.facebook.drawee.backends.pipeline.Fresco
|
|
41
41
|
import com.facebook.imagepipeline.image.CloseableBitmap
|
|
42
42
|
import com.facebook.imagepipeline.image.CloseableXml
|
|
43
|
+
import com.facebook.imagepipeline.request.ImageRequest
|
|
43
44
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
|
44
45
|
import com.facebook.react.views.imagehelper.ImageSource
|
|
45
46
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.AndroidAutoScreen
|
|
@@ -67,10 +68,11 @@ import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroRow
|
|
|
67
68
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroSectionType
|
|
68
69
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OffRampType
|
|
69
70
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OnRampType
|
|
71
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.RemoteImage
|
|
70
72
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TrafficSide
|
|
71
73
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TravelEstimates
|
|
72
74
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TurnType
|
|
73
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.
|
|
75
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.Variant_GlyphImage_AssetImage_RemoteImage
|
|
74
76
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.BitmapCache
|
|
75
77
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.SymbolFont
|
|
76
78
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.get
|
|
@@ -79,6 +81,7 @@ import java.util.Calendar
|
|
|
79
81
|
import java.util.Locale
|
|
80
82
|
import java.util.TimeZone
|
|
81
83
|
import kotlin.math.abs
|
|
84
|
+
import androidx.core.net.toUri
|
|
82
85
|
|
|
83
86
|
object Parser {
|
|
84
87
|
const val TAG = "Parser"
|
|
@@ -210,27 +213,35 @@ object Parser {
|
|
|
210
213
|
}.build()
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
fun parseImage(context: CarContext, image:
|
|
214
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
216
|
+
fun parseImage(context: CarContext, image: Variant_GlyphImage_AssetImage_RemoteImage): CarIcon {
|
|
217
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
fun parseImage(context: CarContext, image: NitroImage): CarIcon {
|
|
218
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
221
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
fun parseImage(
|
|
222
|
-
|
|
224
|
+
fun parseImage(
|
|
225
|
+
context: CarContext,
|
|
226
|
+
glyphImage: GlyphImage?,
|
|
227
|
+
assetImage: AssetImage?,
|
|
228
|
+
remoteImage: RemoteImage?
|
|
229
|
+
): CarIcon {
|
|
230
|
+
val bitmap = parseImageToBitmap(context, glyphImage, assetImage, remoteImage)
|
|
223
231
|
|
|
224
232
|
bitmap?.let {
|
|
225
233
|
return CarIcon.Builder(IconCompat.createWithBitmap(it)).build()
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
//
|
|
229
|
-
return CarIcon.
|
|
236
|
+
// remote images might fail to load so we provide some placeholder then
|
|
237
|
+
return CarIcon.ALERT
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
fun parseImageToBitmap(
|
|
233
|
-
context: CarContext,
|
|
241
|
+
context: CarContext,
|
|
242
|
+
glyphImage: GlyphImage?,
|
|
243
|
+
assetImage: AssetImage?,
|
|
244
|
+
remoteImage: RemoteImage?
|
|
234
245
|
): Bitmap? {
|
|
235
246
|
glyphImage?.let {
|
|
236
247
|
return SymbolFont.imageFromNitroImage(
|
|
@@ -240,6 +251,9 @@ object Parser {
|
|
|
240
251
|
assetImage?.let {
|
|
241
252
|
return parseAssetImage(context, it)
|
|
242
253
|
}
|
|
254
|
+
remoteImage?.let {
|
|
255
|
+
return parseRemoteImage(context, it)
|
|
256
|
+
}
|
|
243
257
|
|
|
244
258
|
return null
|
|
245
259
|
}
|
|
@@ -251,10 +265,14 @@ object Parser {
|
|
|
251
265
|
fun imageFromNitroImages(
|
|
252
266
|
context: CarContext, images: List<NitroImage>
|
|
253
267
|
): IconCompat {
|
|
254
|
-
val bitmaps = images.
|
|
268
|
+
val bitmaps = images.mapNotNull {
|
|
255
269
|
parseImageToBitmap(
|
|
256
|
-
context, it.asFirstOrNull(), it.asSecondOrNull()
|
|
257
|
-
)
|
|
270
|
+
context, it.asFirstOrNull(), it.asSecondOrNull(), it.asThirdOrNull()
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (bitmaps.isEmpty()) {
|
|
275
|
+
return IconCompat.createWithBitmap(createBitmap(1, 1))
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
val height = bitmaps.maxOf { it.height }
|
|
@@ -512,54 +530,106 @@ object Parser {
|
|
|
512
530
|
}
|
|
513
531
|
|
|
514
532
|
fun parseAssetImage(context: CarContext, assetImage: AssetImage): Bitmap? {
|
|
515
|
-
|
|
533
|
+
BitmapCache.get(context, assetImage)?.let { return it }
|
|
516
534
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
535
|
+
val imageRequest = buildImageRequest(ImageSource(context, assetImage.uri).uri)
|
|
536
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = null) ?: return null
|
|
520
537
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
.
|
|
538
|
+
return applyTintAndCache(
|
|
539
|
+
context = context,
|
|
540
|
+
color = assetImage.color,
|
|
541
|
+
bitmap = bitmap,
|
|
542
|
+
cache = { result -> BitmapCache.put(context, assetImage, result) }
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fun parseRemoteImage(context: CarContext, remoteImage: RemoteImage): Bitmap? {
|
|
547
|
+
BitmapCache.get(context, remoteImage)?.let { return it }
|
|
548
|
+
|
|
549
|
+
val imageRequest = buildImageRequest(remoteImage.uri.toUri())
|
|
550
|
+
val timeoutMs = remoteImage.timeoutMs?.toLong() ?: 500L
|
|
551
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = timeoutMs) ?: return null
|
|
552
|
+
|
|
553
|
+
return applyTintAndCache(
|
|
554
|
+
context = context,
|
|
555
|
+
color = remoteImage.color,
|
|
556
|
+
bitmap = bitmap,
|
|
557
|
+
cache = { result -> BitmapCache.put(context, remoteImage, result) }
|
|
558
|
+
)
|
|
559
|
+
}
|
|
524
560
|
|
|
525
|
-
|
|
526
|
-
|
|
561
|
+
// Disable Fresco's own caching — BitmapCache handles all caching uniformly
|
|
562
|
+
private fun buildImageRequest(uri: android.net.Uri): ImageRequest =
|
|
563
|
+
ImageRequestBuilder.newBuilderWithSource(uri)
|
|
564
|
+
.disableDiskCache().disableMemoryCache().build()
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Fetches a bitmap via Fresco's image pipeline.
|
|
568
|
+
* When [timeoutMs] is provided, the fetch runs on a background thread and is abandoned on timeout.
|
|
569
|
+
* When `null`, the fetch runs synchronously (bundled assets, no network I/O).
|
|
570
|
+
*/
|
|
571
|
+
private fun fetchBitmap(
|
|
572
|
+
context: CarContext,
|
|
573
|
+
imageRequest: ImageRequest,
|
|
574
|
+
timeoutMs: Long?
|
|
575
|
+
): Bitmap? {
|
|
576
|
+
if (timeoutMs == null) return fetchSync(context, imageRequest)
|
|
577
|
+
|
|
578
|
+
var fetched: Bitmap? = null
|
|
579
|
+
val thread = Thread { fetched = fetchSync(context, imageRequest) }
|
|
580
|
+
thread.start()
|
|
581
|
+
thread.join(timeoutMs)
|
|
582
|
+
if (thread.isAlive) {
|
|
583
|
+
// Fresco's waitForFinalResult is not interruptible, but setting the flag
|
|
584
|
+
// lets any subsequent interruptible call exit and signals intent to exit.
|
|
585
|
+
thread.interrupt()
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
return fetched
|
|
589
|
+
}
|
|
527
590
|
|
|
591
|
+
private fun fetchSync(context: CarContext, imageRequest: ImageRequest): Bitmap? {
|
|
592
|
+
val dataSource = try {
|
|
593
|
+
Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
|
|
594
|
+
} catch (_: Exception) { return null }
|
|
595
|
+
val result = try {
|
|
596
|
+
DataSources.waitForFinalResult(dataSource)
|
|
597
|
+
} catch (_: Exception) { dataSource.close(); return null }
|
|
528
598
|
val image = result?.get()
|
|
529
599
|
try {
|
|
530
600
|
if (image is CloseableBitmap) {
|
|
531
|
-
|
|
601
|
+
return image.underlyingBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
|
532
602
|
} else if (image is CloseableXml) {
|
|
533
603
|
val drawable = image.buildDrawable()
|
|
534
|
-
|
|
535
|
-
width = image.width, height = image.height, Bitmap.Config.ARGB_8888
|
|
536
|
-
)
|
|
604
|
+
return drawable?.toBitmap(width = image.width, height = image.height, Bitmap.Config.ARGB_8888)
|
|
537
605
|
}
|
|
538
606
|
} finally {
|
|
539
607
|
image?.close()
|
|
540
608
|
result?.close()
|
|
541
609
|
dataSource.close()
|
|
542
610
|
}
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
543
613
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
614
|
+
private fun applyTintAndCache(
|
|
615
|
+
context: CarContext,
|
|
616
|
+
color: NitroColor?,
|
|
617
|
+
bitmap: Bitmap,
|
|
618
|
+
cache: (Bitmap) -> Unit
|
|
619
|
+
): Bitmap {
|
|
620
|
+
color?.get(context)?.let { tint ->
|
|
621
|
+
val tinted = createBitmap(bitmap.width, bitmap.height)
|
|
622
|
+
val canvas = Canvas(tinted)
|
|
551
623
|
val paint = Paint()
|
|
552
624
|
|
|
553
|
-
paint.colorFilter = PorterDuffColorFilter(
|
|
625
|
+
paint.colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN)
|
|
554
626
|
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
|
555
627
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return result
|
|
628
|
+
cache(tinted)
|
|
629
|
+
return tinted
|
|
559
630
|
}
|
|
560
631
|
|
|
561
|
-
|
|
562
|
-
|
|
632
|
+
cache(bitmap)
|
|
563
633
|
return bitmap
|
|
564
634
|
}
|
|
565
635
|
|