@glassly/bluetooth-sdk 0.1.0-dev.26
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 +563 -0
- package/android/build.gradle +450 -0
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +212 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +4 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +50 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/AndroidManifest.xml +42 -0
- package/android/src/main/assets/G2Firmware/README.md +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/app.plugin.js +3 -0
- package/build/BluetoothSdk.types.d.ts +1354 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +41 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/_internal.d.ts +17 -0
- package/build/_internal.d.ts.map +1 -0
- package/build/_internal.js +16 -0
- package/build/_internal.js.map +1 -0
- package/build/_private/BluetoothSdkModule.d.ts +172 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
- package/build/_private/BluetoothSdkModule.js +300 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -0
- package/build/_private/LocalNetworkModule.d.ts +38 -0
- package/build/_private/LocalNetworkModule.d.ts.map +1 -0
- package/build/_private/LocalNetworkModule.js +3 -0
- package/build/_private/LocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +32 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts +6 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +86 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/_private/sdkOtaManifest.d.ts +11 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +15 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +46 -0
- package/build/changelogs.js.map +1 -0
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +9 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +12 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +171 -0
- package/build/index.js.map +1 -0
- package/build/ota-server/OtaServer.types.d.ts +14 -0
- package/build/ota-server/OtaServer.types.d.ts.map +1 -0
- package/build/ota-server/OtaServer.types.js +2 -0
- package/build/ota-server/OtaServer.types.js.map +1 -0
- package/build/ota-server/OtaServerModule.d.ts +21 -0
- package/build/ota-server/OtaServerModule.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.js +3 -0
- package/build/ota-server/OtaServerModule.js.map +1 -0
- package/build/ota-server/OtaServerModule.web.d.ts +10 -0
- package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.web.js +12 -0
- package/build/ota-server/OtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.js +2 -0
- package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.js +3 -0
- package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- package/build/react/index.d.ts +5 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +4 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useBluetooth.d.ts +100 -0
- package/build/react/useBluetooth.d.ts.map +1 -0
- package/build/react/useBluetooth.js +167 -0
- package/build/react/useBluetooth.js.map +1 -0
- package/build/react/useBluetoothEvent.d.ts +6 -0
- package/build/react/useBluetoothEvent.d.ts.map +1 -0
- package/build/react/useBluetoothEvent.js +21 -0
- package/build/react/useBluetoothEvent.js.map +1 -0
- package/build/react/useBluetoothScan.d.ts +22 -0
- package/build/react/useBluetoothScan.d.ts.map +1 -0
- package/build/react/useBluetoothScan.js +135 -0
- package/build/react/useBluetoothScan.js.map +1 -0
- package/build/react/useBluetoothStatus.d.ts +16 -0
- package/build/react/useBluetoothStatus.d.ts.map +1 -0
- package/build/react/useBluetoothStatus.js +137 -0
- package/build/react/useBluetoothStatus.js.map +1 -0
- package/build/react/useGlassesConnection.d.ts +29 -0
- package/build/react/useGlassesConnection.d.ts.map +1 -0
- package/build/react/useGlassesConnection.js +153 -0
- package/build/react/useGlassesConnection.js.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/expo-module.config.json +14 -0
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +1034 -0
- package/ios/GlasslyBluetoothSDK.podspec +106 -0
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/LocalPhotoUploadServer.swift +754 -0
- package/ios/OtaServerModule.swift +147 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/PhotoReceiverModule.swift +129 -0
- package/ios/Source/BluetoothSDK.swift +2303 -0
- package/ios/Source/BluetoothSDKDebug.swift +12 -0
- package/ios/Source/BluetoothSdkDefaults.swift +29 -0
- package/ios/Source/Bridge.swift +708 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +2109 -0
- package/ios/Source/DeviceStore.swift +336 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/ObservableStore.swift +118 -0
- package/ios/Source/OtaManifest.swift +211 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/Resources/G2Firmware/README.md +10 -0
- package/ios/Source/audio/AudioModels.swift +178 -0
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +866 -0
- package/ios/Source/connection/ScanSession.swift +27 -0
- package/ios/Source/controllers/ControllerManager.swift +184 -0
- package/ios/Source/controllers/R1.swift +829 -0
- package/ios/Source/errors/BluetoothSdkError.swift +19 -0
- package/ios/Source/events/BluetoothEvents.swift +342 -0
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/internal/BluetoothAvailability.swift +78 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
- package/ios/Source/internal/ValueParsing.swift +99 -0
- package/ios/Source/requests/DisplayRequests.swift +82 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/Frame.swift +649 -0
- package/ios/Source/sgcs/G1.swift +2554 -0
- package/ios/Source/sgcs/G2.swift +6887 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
- package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
- package/ios/Source/sgcs/G2Flasher.swift +773 -0
- package/ios/Source/sgcs/Mach1.swift +541 -0
- package/ios/Source/sgcs/MentraLive.swift +7011 -0
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/MentraNex.swift +3221 -0
- package/ios/Source/sgcs/Nimo.swift +1886 -0
- package/ios/Source/sgcs/SGCManager.swift +608 -0
- package/ios/Source/sgcs/Simulated.swift +274 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
- package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
- package/ios/Source/status/DeviceStatus.swift +981 -0
- package/ios/Source/status/RuntimeState.swift +359 -0
- package/ios/Source/status/WifiHotspotStatus.swift +345 -0
- package/ios/Source/streaming/StreamModels.swift +693 -0
- package/ios/Source/stt/STTTools.swift +191 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
- package/ios/Source/tts/TTSTools.swift +171 -0
- package/ios/Source/types/DeviceModels.swift +177 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/BinaryCodec.swift +61 -0
- package/ios/Source/utils/BleJsonCompact.swift +405 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/Constants.swift +78 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2096 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
- package/ios/Source/utils/MessageChunker.swift +294 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
- package/ios/Source/utils/TarBz2Extractor.swift +257 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +129 -0
- package/plugin/build/index.d.ts +10 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +3 -0
- package/plugin/build/withAndroid.js +190 -0
- package/plugin/build/withIos.d.ts +5 -0
- package/plugin/build/withIos.js +108 -0
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +45 -0
- package/scripts/write-release-metadata.mjs +152 -0
- package/scripts/write-release-metadata.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +1623 -0
- package/src/_internal.ts +16 -0
- package/src/_private/BluetoothSdkModule.ts +684 -0
- package/src/_private/LocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +34 -0
- package/src/_private/photoRequestPayload.ts +90 -0
- package/src/_private/sdkOtaManifest.ts +18 -0
- package/src/changelogs.ts +46 -0
- package/src/debug.ts +9 -0
- package/src/generated/changelogCatalog.ts +13 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +321 -0
- package/src/ota-server/OtaServer.types.ts +10 -0
- package/src/ota-server/OtaServerModule.ts +23 -0
- package/src/ota-server/OtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
- package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
- package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
- package/src/photo-receiver/index.ts +2 -0
- package/src/react/index.ts +21 -0
- package/src/react/useBluetooth.ts +309 -0
- package/src/react/useBluetoothEvent.ts +35 -0
- package/src/react/useBluetoothScan.ts +173 -0
- package/src/react/useBluetoothStatus.ts +180 -0
- package/src/react/useGlassesConnection.ts +202 -0
- package/src/types/index.ts +12 -0
|
@@ -0,0 +1,2528 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.IntentFilter
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import androidx.core.content.ContextCompat
|
|
13
|
+
import com.glassly.bluetoothsdk.controllers.ControllerManager
|
|
14
|
+
import com.glassly.bluetoothsdk.controllers.R1
|
|
15
|
+
import com.glassly.bluetoothsdk.services.ForegroundService
|
|
16
|
+
import com.glassly.bluetoothsdk.services.PhoneMic
|
|
17
|
+
import com.glassly.bluetoothsdk.sgcs.Ar99
|
|
18
|
+
import com.glassly.bluetoothsdk.sgcs.G1
|
|
19
|
+
import com.glassly.bluetoothsdk.sgcs.G2
|
|
20
|
+
import com.glassly.bluetoothsdk.sgcs.SceneElement
|
|
21
|
+
import com.glassly.bluetoothsdk.sgcs.SceneFrame
|
|
22
|
+
import com.glassly.bluetoothsdk.sgcs.Mach1
|
|
23
|
+
import com.glassly.bluetoothsdk.sgcs.MentraLive
|
|
24
|
+
import com.glassly.bluetoothsdk.sgcs.MentraNex
|
|
25
|
+
import com.glassly.bluetoothsdk.sgcs.Nimo
|
|
26
|
+
import com.glassly.bluetoothsdk.sgcs.SGCManager
|
|
27
|
+
import com.glassly.bluetoothsdk.sgcs.Simulated
|
|
28
|
+
import com.glassly.bluetoothsdk.utils.ControllerTypes
|
|
29
|
+
import com.glassly.bluetoothsdk.utils.DeviceTypes
|
|
30
|
+
import com.glassly.bluetoothsdk.utils.MicMap
|
|
31
|
+
import com.glassly.bluetoothsdk.utils.MicTypes
|
|
32
|
+
import com.glassly.bluetoothsdk.utils.PhoneAudioMonitor
|
|
33
|
+
import com.glassly.lc3Lib.Lc3Cpp
|
|
34
|
+
import com.glassly.bluetoothsdk.speakerid.SpeakerIdEngine
|
|
35
|
+
import com.glassly.bluetoothsdk.stt.SherpaOnnxTranscriber
|
|
36
|
+
import kotlinx.coroutines.CoroutineScope
|
|
37
|
+
import kotlinx.coroutines.Dispatchers
|
|
38
|
+
import kotlinx.coroutines.launch
|
|
39
|
+
import java.text.SimpleDateFormat
|
|
40
|
+
import java.util.*
|
|
41
|
+
import java.util.concurrent.CountDownLatch
|
|
42
|
+
import java.util.concurrent.ExecutorService
|
|
43
|
+
import java.util.concurrent.Executors
|
|
44
|
+
import java.util.concurrent.TimeUnit
|
|
45
|
+
import kotlin.jvm.JvmStatic
|
|
46
|
+
|
|
47
|
+
class DeviceManager {
|
|
48
|
+
companion object {
|
|
49
|
+
|
|
50
|
+
@Volatile
|
|
51
|
+
private var _instance: DeviceManager? = null
|
|
52
|
+
|
|
53
|
+
@JvmStatic
|
|
54
|
+
fun getInstance(): DeviceManager {
|
|
55
|
+
return _instance
|
|
56
|
+
?: synchronized(this) { _instance ?: DeviceManager().also { _instance = it } }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** How stale a SpeakerIdEngine match may be and still label a transcript. */
|
|
60
|
+
private const val SPEAKER_MATCH_WINDOW_MS = 5000L
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Unique (Android)
|
|
64
|
+
private var serviceStarted = false
|
|
65
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
66
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
67
|
+
private var sendStateWorkItem: Runnable? = null
|
|
68
|
+
private var phoneMic: PhoneMic? = null
|
|
69
|
+
|
|
70
|
+
// Track last known permissions
|
|
71
|
+
private var lastHadBluetoothPermission = false
|
|
72
|
+
private var lastHadMicrophonePermission = false
|
|
73
|
+
private var permissionReceiver: BroadcastReceiver? = null
|
|
74
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
75
|
+
private var permissionCheckRunnable: Runnable? = null
|
|
76
|
+
|
|
77
|
+
// Bluetooth adapter state monitoring (detects BT toggle from control center)
|
|
78
|
+
private var bluetoothStateReceiver: BroadcastReceiver? = null
|
|
79
|
+
private var isBluetoothStateReceiverRegistered = false
|
|
80
|
+
|
|
81
|
+
// MARK: - End Unique
|
|
82
|
+
|
|
83
|
+
// MARK: - Properties
|
|
84
|
+
var sgc: SGCManager? = null
|
|
85
|
+
var controller: ControllerManager? = null
|
|
86
|
+
|
|
87
|
+
// settings:
|
|
88
|
+
private var defaultWearable: String
|
|
89
|
+
get() = DeviceStore.store.get("bluetooth", "default_wearable") as? String ?: ""
|
|
90
|
+
set(value) = DeviceStore.apply("bluetooth", "default_wearable", value)
|
|
91
|
+
|
|
92
|
+
private var pendingWearable: String
|
|
93
|
+
get() = DeviceStore.store.get("bluetooth", "pending_wearable") as? String ?: ""
|
|
94
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_wearable", value)
|
|
95
|
+
|
|
96
|
+
private var pendingDeviceName: String
|
|
97
|
+
get() = DeviceStore.store.get("bluetooth", "pending_device_name") as? String ?: ""
|
|
98
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_device_name", value)
|
|
99
|
+
|
|
100
|
+
private var pendingDeviceAddress: String
|
|
101
|
+
get() = DeviceStore.store.get("bluetooth", "pending_device_address") as? String ?: ""
|
|
102
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_device_address", value)
|
|
103
|
+
|
|
104
|
+
public var deviceName: String
|
|
105
|
+
get() = DeviceStore.store.get("bluetooth", "device_name") as? String ?: ""
|
|
106
|
+
set(value) = DeviceStore.apply("bluetooth", "device_name", value)
|
|
107
|
+
|
|
108
|
+
public var deviceAddress: String
|
|
109
|
+
get() = DeviceStore.store.get("bluetooth", "device_address") as? String ?: ""
|
|
110
|
+
set(value) = DeviceStore.apply("bluetooth", "device_address", value)
|
|
111
|
+
|
|
112
|
+
private var defaultController: String
|
|
113
|
+
get() = DeviceStore.store.get("bluetooth", "default_controller") as? String ?: ""
|
|
114
|
+
set(value) = DeviceStore.apply("bluetooth", "default_controller", value)
|
|
115
|
+
|
|
116
|
+
private var pendingController: String
|
|
117
|
+
get() = DeviceStore.store.get("bluetooth", "pending_controller") as? String ?: ""
|
|
118
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_controller", value)
|
|
119
|
+
|
|
120
|
+
private var controllerDeviceName: String
|
|
121
|
+
get() = DeviceStore.store.get("bluetooth", "controller_device_name") as? String ?: ""
|
|
122
|
+
set(value) = DeviceStore.apply("bluetooth", "controller_device_name", value)
|
|
123
|
+
|
|
124
|
+
private var searchingController: Boolean
|
|
125
|
+
get() = DeviceStore.store.get("bluetooth", "searchingController") as? Boolean ?: false
|
|
126
|
+
set(value) = DeviceStore.apply("bluetooth", "searchingController", value)
|
|
127
|
+
|
|
128
|
+
private var screenDisabled: Boolean
|
|
129
|
+
get() = DeviceStore.store.get("bluetooth", "screen_disabled") as? Boolean ?: false
|
|
130
|
+
set(value) = DeviceStore.apply("bluetooth", "screen_disabled", value)
|
|
131
|
+
|
|
132
|
+
private var preferredMic: String
|
|
133
|
+
get() = DeviceStore.store.get("bluetooth", "preferred_mic") as? String ?: "auto"
|
|
134
|
+
set(value) = DeviceStore.apply("bluetooth", "preferred_mic", value)
|
|
135
|
+
|
|
136
|
+
private var autoBrightness: Boolean
|
|
137
|
+
get() = DeviceStore.store.get("bluetooth", "auto_brightness") as? Boolean ?: true
|
|
138
|
+
set(value) = DeviceStore.apply("bluetooth", "auto_brightness", value)
|
|
139
|
+
|
|
140
|
+
private var brightness: Int
|
|
141
|
+
get() = DeviceStore.store.get("bluetooth", "brightness") as? Int ?: 50
|
|
142
|
+
set(value) = DeviceStore.apply("bluetooth", "brightness", value)
|
|
143
|
+
|
|
144
|
+
private var headUpAngle: Int
|
|
145
|
+
get() = DeviceStore.store.get("bluetooth", "head_up_angle") as? Int ?: 30
|
|
146
|
+
set(value) = DeviceStore.apply("bluetooth", "head_up_angle", value)
|
|
147
|
+
|
|
148
|
+
private var sensingEnabled: Boolean
|
|
149
|
+
get() = DeviceStore.store.get("bluetooth", "sensing_enabled") as? Boolean ?: true
|
|
150
|
+
set(value) = DeviceStore.apply("bluetooth", "sensing_enabled", value)
|
|
151
|
+
|
|
152
|
+
public var powerSavingMode: Boolean
|
|
153
|
+
get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
|
|
154
|
+
set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
|
|
155
|
+
|
|
156
|
+
// Phone-side VAD gating switch. Default is OFF (VAD runs) so the
|
|
157
|
+
// coordinator can drive per-utterance offline/online STT switching from
|
|
158
|
+
// `vad_status` events. Set to `true` only as an emergency kill-switch.
|
|
159
|
+
private var bypassVad: Boolean
|
|
160
|
+
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: false
|
|
161
|
+
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
162
|
+
|
|
163
|
+
private var localSttFallbackActive: Boolean
|
|
164
|
+
get() = DeviceStore.store.get("bluetooth", "local_stt_fallback_active") as? Boolean ?: false
|
|
165
|
+
set(value) = DeviceStore.apply("bluetooth", "local_stt_fallback_active", value)
|
|
166
|
+
|
|
167
|
+
private var shouldSendPcm: Boolean
|
|
168
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_pcm") as? Boolean ?: false
|
|
169
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_pcm", value)
|
|
170
|
+
|
|
171
|
+
private var shouldSendLc3: Boolean
|
|
172
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_lc3") as? Boolean ?: false
|
|
173
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_lc3", value)
|
|
174
|
+
|
|
175
|
+
private var shouldSendTranscript: Boolean
|
|
176
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_transcript") as? Boolean ?: false
|
|
177
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_transcript", value)
|
|
178
|
+
|
|
179
|
+
private var contextualDashboard: Boolean
|
|
180
|
+
get() = DeviceStore.store.get("bluetooth", "contextual_dashboard") as? Boolean ?: true
|
|
181
|
+
set(value) = DeviceStore.apply("bluetooth", "contextual_dashboard", value)
|
|
182
|
+
|
|
183
|
+
private var dashboardHeight: Int
|
|
184
|
+
get() = (DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)?.toInt() ?: 4
|
|
185
|
+
set(value) = DeviceStore.apply("bluetooth", "dashboard_height", value)
|
|
186
|
+
|
|
187
|
+
private var dashboardDepth: Int
|
|
188
|
+
get() = (DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)?.toInt() ?: 2
|
|
189
|
+
set(value) = DeviceStore.apply("bluetooth", "dashboard_depth", value)
|
|
190
|
+
|
|
191
|
+
private var galleryMode: Boolean
|
|
192
|
+
get() = DeviceStore.store.get("bluetooth", "gallery_mode") as? Boolean ?: true
|
|
193
|
+
set(value) = DeviceStore.apply("bluetooth", "gallery_mode", value)
|
|
194
|
+
|
|
195
|
+
// state:
|
|
196
|
+
private var searching: Boolean
|
|
197
|
+
get() = DeviceStore.store.get("bluetooth", "searching") as? Boolean ?: false
|
|
198
|
+
set(value) = DeviceStore.apply("bluetooth", "searching", value)
|
|
199
|
+
|
|
200
|
+
private var glassesBluetoothClassicConnected: Boolean
|
|
201
|
+
get() = DeviceStore.store.get("glasses", "bluetoothClassicConnected") as? Boolean ?: false
|
|
202
|
+
set(value) = DeviceStore.apply("glasses", "bluetoothClassicConnected", value)
|
|
203
|
+
|
|
204
|
+
public var micRanking: MutableList<String>
|
|
205
|
+
get() =
|
|
206
|
+
(DeviceStore.store.get("bluetooth", "micRanking") as? List<*>)
|
|
207
|
+
?.mapNotNull { it as? String }
|
|
208
|
+
?.toMutableList()
|
|
209
|
+
?: MicMap.map["auto"]?.toMutableList() ?: mutableListOf()
|
|
210
|
+
set(value) = DeviceStore.apply("bluetooth", "micRanking", value)
|
|
211
|
+
|
|
212
|
+
private var shouldSendBootingMessage: Boolean
|
|
213
|
+
get() = DeviceStore.store.get("bluetooth", "shouldSendBootingMessage") as? Boolean ?: true
|
|
214
|
+
set(value) = DeviceStore.apply("bluetooth", "shouldSendBootingMessage", value)
|
|
215
|
+
|
|
216
|
+
// Guard against duplicate ready callbacks firing back-to-back.
|
|
217
|
+
private var lastReadyHandledAtMs: Long = 0L
|
|
218
|
+
private var lastReadyHandledKey: String = ""
|
|
219
|
+
private var lastSystemTimeSyncConnectionKey: String = ""
|
|
220
|
+
private var pendingSystemTimeSync: Runnable? = null
|
|
221
|
+
|
|
222
|
+
private var systemMicUnavailable: Boolean
|
|
223
|
+
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
224
|
+
set(value) = DeviceStore.apply("bluetooth", "systemMicUnavailable", value)
|
|
225
|
+
|
|
226
|
+
public var headUp: Boolean
|
|
227
|
+
get() = DeviceStore.store.get("glasses", "headUp") as? Boolean ?: false
|
|
228
|
+
set(value) = DeviceStore.apply("glasses", "headUp", value)
|
|
229
|
+
|
|
230
|
+
private var micEnabled: Boolean
|
|
231
|
+
get() = DeviceStore.store.get("bluetooth", "micEnabled") as? Boolean ?: false
|
|
232
|
+
set(value) = DeviceStore.apply("bluetooth", "micEnabled", value)
|
|
233
|
+
|
|
234
|
+
private var currentMic: String
|
|
235
|
+
get() = DeviceStore.store.get("bluetooth", "currentMic") as? String ?: ""
|
|
236
|
+
set(value) = DeviceStore.apply("bluetooth", "currentMic", value)
|
|
237
|
+
|
|
238
|
+
private var searchResults: List<Any>
|
|
239
|
+
get() = DeviceStore.store.get("bluetooth", "searchResults") as? List<Any> ?: emptyList()
|
|
240
|
+
set(value) = DeviceStore.apply("bluetooth", "searchResults", value)
|
|
241
|
+
|
|
242
|
+
private var wifiScanResults: List<Any>
|
|
243
|
+
get() = DeviceStore.store.get("bluetooth", "wifiScanResults") as? List<Any> ?: emptyList()
|
|
244
|
+
set(value) = DeviceStore.apply("bluetooth", "wifiScanResults", value)
|
|
245
|
+
|
|
246
|
+
private var lastLog: MutableList<String>
|
|
247
|
+
get() = DeviceStore.store.get("bluetooth", "lastLog") as? MutableList<String> ?: mutableListOf()
|
|
248
|
+
set(value) = DeviceStore.apply("bluetooth", "lastLog", value)
|
|
249
|
+
|
|
250
|
+
// LC3 Audio Encoding
|
|
251
|
+
// Audio output format enum
|
|
252
|
+
enum class AudioOutputFormat {
|
|
253
|
+
LC3,
|
|
254
|
+
PCM
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
|
|
258
|
+
// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
|
|
259
|
+
private var lc3EncoderPtr: Long = 0
|
|
260
|
+
private var lc3DecoderPtr: Long = 0
|
|
261
|
+
private val lc3Lock = Any()
|
|
262
|
+
|
|
263
|
+
// Audio output format - defaults to LC3 for bandwidth savings
|
|
264
|
+
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
265
|
+
private var lastLc3Event: Long? = null
|
|
266
|
+
private val micHealthLock = Any()
|
|
267
|
+
private var sequenceGapEvents = 0L
|
|
268
|
+
private var decodeFailures = 0L
|
|
269
|
+
private var lastLc3ReceivedAt: Long? = null
|
|
270
|
+
private var lastPcmProducedAt: Long? = null
|
|
271
|
+
private var lastLc3Sequence: Int? = null
|
|
272
|
+
private var micReinitRunnable: Runnable? = null
|
|
273
|
+
private var systemMicAvailabilityRecheckRunnable: Runnable? = null
|
|
274
|
+
|
|
275
|
+
// VAD
|
|
276
|
+
private val vadBuffer = mutableListOf<ByteArray>()
|
|
277
|
+
private var isSpeaking = false
|
|
278
|
+
private var vadPolicy: com.glassly.bluetoothsdk.stt.VadGateSpeechPolicy? = null
|
|
279
|
+
|
|
280
|
+
// STT
|
|
281
|
+
private var transcriber: SherpaOnnxTranscriber? = null
|
|
282
|
+
|
|
283
|
+
// View states
|
|
284
|
+
private val viewStates = mutableListOf<ViewState>()
|
|
285
|
+
|
|
286
|
+
init {
|
|
287
|
+
Bridge.log("DeviceManager: init()")
|
|
288
|
+
initializeViewStates()
|
|
289
|
+
startForegroundService()
|
|
290
|
+
// setupPermissionMonitoring()
|
|
291
|
+
setupBluetoothStateMonitoring()
|
|
292
|
+
phoneMic = PhoneMic.getInstance()
|
|
293
|
+
// Sherpa is intentionally lazy. Initializing it during app startup
|
|
294
|
+
// competes with Cloud V2 captions even when cloud transcription is
|
|
295
|
+
// healthy; start it only when offline/local transcription is requested.
|
|
296
|
+
|
|
297
|
+
// Initialize LC3 encoder/decoder for unified audio encoding
|
|
298
|
+
try {
|
|
299
|
+
Lc3Cpp.init()
|
|
300
|
+
lc3EncoderPtr = Lc3Cpp.initEncoder()
|
|
301
|
+
lc3DecoderPtr = Lc3Cpp.initDecoder()
|
|
302
|
+
Bridge.log("LC3 encoder/decoder initialized successfully")
|
|
303
|
+
} catch (e: Exception) {
|
|
304
|
+
Bridge.log("Failed to initialize LC3 encoder/decoder: ${e.message}")
|
|
305
|
+
lc3EncoderPtr = 0
|
|
306
|
+
lc3DecoderPtr = 0
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Initialize phone-side Silero VAD. Used by handlePcm to gate audio
|
|
310
|
+
// fan-out and to drive per-utterance offline/online STT switching
|
|
311
|
+
// (see LocalSttFallbackCoordinator on the JS side). If a previous
|
|
312
|
+
// policy somehow exists (re-init on hot reload), stop it first to
|
|
313
|
+
// release the ONNX session.
|
|
314
|
+
try {
|
|
315
|
+
vadPolicy?.stop()
|
|
316
|
+
val ctx = Bridge.getContext()
|
|
317
|
+
val policy = com.glassly.bluetoothsdk.stt.VadGateSpeechPolicy(ctx)
|
|
318
|
+
policy.init(blockSizeSamples = 512)
|
|
319
|
+
policy.onSpeechStateChanged = { speaking ->
|
|
320
|
+
isSpeaking = speaking
|
|
321
|
+
Bridge.sendVoiceActivityDetectionStatus(speaking)
|
|
322
|
+
}
|
|
323
|
+
vadPolicy = policy
|
|
324
|
+
Bridge.log("VadGateSpeechPolicy initialized")
|
|
325
|
+
} catch (e: Exception) {
|
|
326
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
327
|
+
vadPolicy = null
|
|
328
|
+
} catch (e: LinkageError) {
|
|
329
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
330
|
+
vadPolicy = null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Mic reinit check every 10 seconds
|
|
334
|
+
val micReinitR =
|
|
335
|
+
object : Runnable {
|
|
336
|
+
override fun run() {
|
|
337
|
+
checkAndReinitGlassesMic()
|
|
338
|
+
mainHandler.postDelayed(this, 10_000)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
micReinitRunnable = micReinitR
|
|
342
|
+
mainHandler.postDelayed(micReinitR, 10_000)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun checkAndReinitGlassesMic() {
|
|
346
|
+
// Re-arm from app INTENT, not glasses STATE: a firmware page kill clears
|
|
347
|
+
// glasses/micEnabled to false while the app still wants the mic, which used to
|
|
348
|
+
// make this watchdog return early and strand the mic. micEnabled + currentMic
|
|
349
|
+
// are DeviceManager-owned intent and survive page kills. If the app wants the
|
|
350
|
+
// glasses mic on and we haven't seen an lc3 event in >5s, reinitialize it.
|
|
351
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
352
|
+
if (!micEnabled || !glassesConnected) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Intent is on but no mic was ever selected — updateMicState() ran at a moment
|
|
357
|
+
// when nothing was available (app backgrounded, glasses mid-reconnect) and set
|
|
358
|
+
// currentMic to "". Retry selection instead of staying stranded.
|
|
359
|
+
if (currentMic.isEmpty()) {
|
|
360
|
+
Bridge.log("MAN: mic intent on but no mic selected — retrying mic selection")
|
|
361
|
+
updateMicState()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (currentMic != MicTypes.GLASSES_CUSTOM) {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (sgc?.isMicSuspendedForAudio == true) {
|
|
370
|
+
Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (PhoneAudioMonitor.getInstance(Bridge.getContext()).isOwnAppAudioPlaying()) {
|
|
375
|
+
Bridge.log("MAN: Glassly audio is playing; skipping glasses mic recovery")
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// When no frame has ever been received, treat elapsed as "forever" so we
|
|
380
|
+
// actually attempt recovery (was 0 before, which made the watchdog a no-op).
|
|
381
|
+
val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
|
|
382
|
+
if (timeSinceLastLc3Event > 5000) {
|
|
383
|
+
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
|
|
384
|
+
sgc?.setMicEnabled(true)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private fun scheduleSystemMicAvailabilityRecheck(reason: String) {
|
|
389
|
+
if (systemMicAvailabilityRecheckRunnable != null) {
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
val recheck =
|
|
394
|
+
object : Runnable {
|
|
395
|
+
override fun run() {
|
|
396
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
397
|
+
|
|
398
|
+
if (!micEnabled || !systemMicUnavailable) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
val stillBlocked = phoneMic?.hasBlockingMicInterruption() ?: true
|
|
403
|
+
if (stillBlocked) {
|
|
404
|
+
scheduleSystemMicAvailabilityRecheck(reason)
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
systemMicUnavailable = false
|
|
409
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
410
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
411
|
+
updateMicState()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
systemMicAvailabilityRecheckRunnable = recheck
|
|
416
|
+
mainHandler.postDelayed(recheck, 2_000)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// MARK: - Unique (Android)
|
|
420
|
+
private fun setupPermissionMonitoring() {
|
|
421
|
+
val context = Bridge.getContext()
|
|
422
|
+
|
|
423
|
+
// Store initial permission state
|
|
424
|
+
lastHadBluetoothPermission = checkBluetoothPermission(context)
|
|
425
|
+
lastHadMicrophonePermission = checkMicrophonePermission(context)
|
|
426
|
+
|
|
427
|
+
Bridge.log(
|
|
428
|
+
"MAN: Initial permissions - BT: $lastHadBluetoothPermission, Mic: $lastHadMicrophonePermission"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
// Create receiver for package changes (fires when permissions change)
|
|
432
|
+
permissionReceiver =
|
|
433
|
+
object : BroadcastReceiver() {
|
|
434
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
435
|
+
if (intent?.action == Intent.ACTION_PACKAGE_CHANGED &&
|
|
436
|
+
intent.data?.schemeSpecificPart == context?.packageName
|
|
437
|
+
) {
|
|
438
|
+
|
|
439
|
+
Bridge.log("MAN: Package changed, checking permissions...")
|
|
440
|
+
checkPermissionChanges()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Register the receiver
|
|
446
|
+
try {
|
|
447
|
+
val filter =
|
|
448
|
+
IntentFilter().apply {
|
|
449
|
+
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
|
450
|
+
addDataScheme("package")
|
|
451
|
+
}
|
|
452
|
+
context.registerReceiver(permissionReceiver, filter)
|
|
453
|
+
Bridge.log("MAN: Permission monitoring started")
|
|
454
|
+
} catch (e: Exception) {
|
|
455
|
+
Bridge.log("MAN: Failed to register permission receiver: ${e.message}")
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Also set up a periodic check as backup (some devices don't fire PACKAGE_CHANGED reliably)
|
|
459
|
+
// startPeriodicPermissionCheck()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private fun startPeriodicPermissionCheck() {
|
|
463
|
+
permissionCheckRunnable =
|
|
464
|
+
object : Runnable {
|
|
465
|
+
override fun run() {
|
|
466
|
+
checkPermissionChanges()
|
|
467
|
+
handler.postDelayed(this, 10000) // Check every 10 seconds
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
handler.postDelayed(permissionCheckRunnable!!, 10000)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private fun checkPermissionChanges() {
|
|
474
|
+
val context = Bridge.getContext()
|
|
475
|
+
|
|
476
|
+
val currentHasBluetoothPermission = checkBluetoothPermission(context)
|
|
477
|
+
val currentHasMicrophonePermission = checkMicrophonePermission(context)
|
|
478
|
+
|
|
479
|
+
var permissionsChanged = false
|
|
480
|
+
|
|
481
|
+
if (currentHasBluetoothPermission != lastHadBluetoothPermission) {
|
|
482
|
+
Bridge.log(
|
|
483
|
+
"MAN: Bluetooth permission changed: $lastHadBluetoothPermission -> $currentHasBluetoothPermission"
|
|
484
|
+
)
|
|
485
|
+
lastHadBluetoothPermission = currentHasBluetoothPermission
|
|
486
|
+
permissionsChanged = true
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (currentHasMicrophonePermission != lastHadMicrophonePermission) {
|
|
490
|
+
Bridge.log(
|
|
491
|
+
"MAN: Microphone permission changed: $lastHadMicrophonePermission -> $currentHasMicrophonePermission"
|
|
492
|
+
)
|
|
493
|
+
lastHadMicrophonePermission = currentHasMicrophonePermission
|
|
494
|
+
permissionsChanged = true
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (permissionsChanged && !currentHasBluetoothPermission) {
|
|
498
|
+
Bridge.log("MAN: Bluetooth permission revoked disconnecting glasses")
|
|
499
|
+
disconnect()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (permissionsChanged && serviceStarted) {
|
|
503
|
+
Bridge.log("MAN: Permissions changed, restarting service")
|
|
504
|
+
restartForegroundService()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private fun checkBluetoothPermission(context: Context): Boolean {
|
|
509
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
510
|
+
val connect = ContextCompat.checkSelfPermission(
|
|
511
|
+
context,
|
|
512
|
+
android.Manifest.permission.BLUETOOTH_CONNECT
|
|
513
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
514
|
+
val scan = ContextCompat.checkSelfPermission(
|
|
515
|
+
context,
|
|
516
|
+
android.Manifest.permission.BLUETOOTH_SCAN
|
|
517
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
518
|
+
connect && scan
|
|
519
|
+
} else {
|
|
520
|
+
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) ==
|
|
521
|
+
PackageManager.PERMISSION_GRANTED
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private fun hasBluetoothPermissions(): Boolean {
|
|
526
|
+
return checkBluetoothPermission(Bridge.getContext())
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private fun checkMicrophonePermission(context: Context): Boolean {
|
|
530
|
+
return ContextCompat.checkSelfPermission(
|
|
531
|
+
context,
|
|
532
|
+
android.Manifest.permission.RECORD_AUDIO
|
|
533
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private fun setupBluetoothStateMonitoring() {
|
|
537
|
+
val context = Bridge.getContext()
|
|
538
|
+
|
|
539
|
+
bluetoothStateReceiver =
|
|
540
|
+
object : BroadcastReceiver() {
|
|
541
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
542
|
+
if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
|
|
543
|
+
|
|
544
|
+
val state =
|
|
545
|
+
intent.getIntExtra(
|
|
546
|
+
BluetoothAdapter.EXTRA_STATE,
|
|
547
|
+
BluetoothAdapter.ERROR
|
|
548
|
+
)
|
|
549
|
+
when (state) {
|
|
550
|
+
BluetoothAdapter.STATE_OFF -> {
|
|
551
|
+
Bridge.log("MAN: Bluetooth turned OFF (control center or settings)")
|
|
552
|
+
disconnect()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
BluetoothAdapter.STATE_TURNING_OFF -> {
|
|
556
|
+
Bridge.log("MAN: Bluetooth turning off...")
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
BluetoothAdapter.STATE_ON -> {
|
|
560
|
+
Bridge.log("MAN: Bluetooth turned ON")
|
|
561
|
+
// Auto-reconnect to last known device if we have one
|
|
562
|
+
if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
|
|
563
|
+
Bridge.log(
|
|
564
|
+
"MAN: Bluetooth restored, attempting reconnect to: $deviceName"
|
|
565
|
+
)
|
|
566
|
+
handler.postDelayed(
|
|
567
|
+
{ connectDefault() },
|
|
568
|
+
2000
|
|
569
|
+
) // Small delay to let BT stack stabilize
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
BluetoothAdapter.STATE_TURNING_ON -> {
|
|
574
|
+
Bridge.log("MAN: Bluetooth turning on...")
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
582
|
+
context.registerReceiver(bluetoothStateReceiver, filter)
|
|
583
|
+
isBluetoothStateReceiverRegistered = true
|
|
584
|
+
Bridge.log("MAN: Bluetooth state monitoring started")
|
|
585
|
+
} catch (e: Exception) {
|
|
586
|
+
Bridge.log("MAN: Failed to register Bluetooth state receiver: ${e.message}")
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private fun stopBluetoothStateMonitoring() {
|
|
591
|
+
if (isBluetoothStateReceiverRegistered && bluetoothStateReceiver != null) {
|
|
592
|
+
try {
|
|
593
|
+
Bridge.getContext().unregisterReceiver(bluetoothStateReceiver)
|
|
594
|
+
} catch (e: Exception) {
|
|
595
|
+
Bridge.log("MAN: Error unregistering Bluetooth state receiver: ${e.message}")
|
|
596
|
+
}
|
|
597
|
+
isBluetoothStateReceiverRegistered = false
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private fun startForegroundService() {
|
|
602
|
+
val context = Bridge.getContext()
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
Bridge.log("MAN: Starting foreground service")
|
|
606
|
+
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
607
|
+
|
|
608
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
609
|
+
context.startForegroundService(serviceIntent)
|
|
610
|
+
} else {
|
|
611
|
+
context.startService(serviceIntent)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
serviceStarted = true
|
|
615
|
+
Bridge.log("MAN: Foreground service started")
|
|
616
|
+
} catch (e: Exception) {
|
|
617
|
+
Bridge.log("MAN: Failed to start service: ${e.message}")
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Re-evaluate the active foreground-service types while the host Activity is visible.
|
|
623
|
+
*
|
|
624
|
+
* Location is a while-in-use permission, so Android 14+ only allows the service to add
|
|
625
|
+
* the location type from the foreground. Sending a command to the existing service
|
|
626
|
+
* updates its type mask without tearing down the glasses connection.
|
|
627
|
+
*/
|
|
628
|
+
fun refreshForegroundServiceTypes() {
|
|
629
|
+
val context = Bridge.getContext()
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
Bridge.log("MAN: Refreshing foreground service types")
|
|
633
|
+
val serviceIntent =
|
|
634
|
+
Intent(context, ForegroundService::class.java).apply {
|
|
635
|
+
action = ForegroundService.ACTION_REFRESH_TYPES
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
639
|
+
context.startForegroundService(serviceIntent)
|
|
640
|
+
} else {
|
|
641
|
+
context.startService(serviceIntent)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
serviceStarted = true
|
|
645
|
+
Bridge.log("MAN: Foreground service types refreshed")
|
|
646
|
+
} catch (e: Exception) {
|
|
647
|
+
Bridge.log("MAN: Failed to refresh foreground service types: ${e.message}")
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private fun restartForegroundService() {
|
|
652
|
+
val context = Bridge.getContext()
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
// Stop the service
|
|
656
|
+
val stopIntent = Intent(context, ForegroundService::class.java)
|
|
657
|
+
context.stopService(stopIntent)
|
|
658
|
+
|
|
659
|
+
// Small delay
|
|
660
|
+
Thread.sleep(100)
|
|
661
|
+
|
|
662
|
+
// Start it again with new permissions
|
|
663
|
+
val startIntent = Intent(context, ForegroundService::class.java)
|
|
664
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
665
|
+
context.startForegroundService(startIntent)
|
|
666
|
+
} else {
|
|
667
|
+
context.startService(startIntent)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
Bridge.log("MAN: Service restarted with updated permissions")
|
|
671
|
+
} catch (e: Exception) {
|
|
672
|
+
Bridge.log("MAN: Failed to restart service: ${e.message}")
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private fun initializeViewStates() {
|
|
677
|
+
viewStates.clear()
|
|
678
|
+
|
|
679
|
+
// Matching Swift's 4 view states exactly
|
|
680
|
+
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
681
|
+
viewStates.add(
|
|
682
|
+
ViewState(
|
|
683
|
+
" ",
|
|
684
|
+
" ",
|
|
685
|
+
" ",
|
|
686
|
+
"text_wall",
|
|
687
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
688
|
+
null,
|
|
689
|
+
null
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
693
|
+
viewStates.add(
|
|
694
|
+
ViewState(
|
|
695
|
+
" ",
|
|
696
|
+
" ",
|
|
697
|
+
" ",
|
|
698
|
+
"text_wall",
|
|
699
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
700
|
+
null,
|
|
701
|
+
null
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private fun statesEqual(s1: ViewState, s2: ViewState): Boolean {
|
|
707
|
+
val state1 =
|
|
708
|
+
"${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
|
|
709
|
+
val state2 =
|
|
710
|
+
"${s2.layoutType}${s2.text}${s2.topText}${s2.bottomText}${s2.title}${s2.data ?: ""}"
|
|
711
|
+
return state1 == state2
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private fun Map<String, Any>.getString(key: String, defaultValue: String): String {
|
|
715
|
+
return (this[key] as? String) ?: defaultValue
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Inner classes
|
|
719
|
+
|
|
720
|
+
data class ViewState(
|
|
721
|
+
var topText: String,
|
|
722
|
+
var bottomText: String,
|
|
723
|
+
var title: String,
|
|
724
|
+
var layoutType: String,
|
|
725
|
+
var text: String,
|
|
726
|
+
var data: String?,
|
|
727
|
+
var animationData: Map<String, Any>?,
|
|
728
|
+
// Optional bitmap_view container position/size (used by G2; ignored by others).
|
|
729
|
+
// Reused by positioned_text for its container rect.
|
|
730
|
+
var bmpX: Int? = null,
|
|
731
|
+
var bmpY: Int? = null,
|
|
732
|
+
var bmpWidth: Int? = null,
|
|
733
|
+
var bmpHeight: Int? = null,
|
|
734
|
+
// Optional positioned_text border (used by G2; ignored by others).
|
|
735
|
+
var borderWidth: Int? = null,
|
|
736
|
+
var borderRadius: Int? = null
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
// Scene slots — one whole SceneFrame per view (main/dashboard), parallel to
|
|
740
|
+
// viewStates. When a slot holds a scene, viewStates carries a "scene"
|
|
741
|
+
// sentinel so sendCurrentState routes here. Holding the WHOLE frame keeps
|
|
742
|
+
// native re-dispatch coherent (dashboard exit re-applies a complete scene,
|
|
743
|
+
// not whatever element happened to arrive last).
|
|
744
|
+
private val sceneStates = arrayOfNulls<SceneFrame>(2)
|
|
745
|
+
// MARK: - End Unique
|
|
746
|
+
|
|
747
|
+
// MARK: - Voice Data Handling
|
|
748
|
+
|
|
749
|
+
private fun convertAndSendMicLc3(pcmData: ByteArray) {
|
|
750
|
+
synchronized(lc3Lock) {
|
|
751
|
+
if (lc3EncoderPtr == 0L) {
|
|
752
|
+
Bridge.log("MAN: ERROR - LC3 encoder not initialized but format is LC3")
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
val lc3FrameSize = (DeviceStore.store.get("bluetooth", "lc3_frame_size") as Number).toInt()
|
|
756
|
+
val lc3Data = Lc3Cpp.encodeLC3(lc3EncoderPtr, pcmData, lc3FrameSize)
|
|
757
|
+
if (lc3Data == null || lc3Data.isEmpty()) {
|
|
758
|
+
Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
Bridge.sendMicLc3(lc3Data)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private fun handleSendingPcm(pcmData: ByteArray) {
|
|
766
|
+
if (shouldSendPcm) {
|
|
767
|
+
Bridge.sendMicPcm(pcmData)
|
|
768
|
+
}
|
|
769
|
+
if (shouldSendLc3) {
|
|
770
|
+
convertAndSendMicLc3(pcmData)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Marks glasses-mic audio as alive for the 10s reinit watchdog. SGCs that decode
|
|
776
|
+
* audio themselves and feed [handlePcm] directly (e.g. Nimo's Opus path) must call
|
|
777
|
+
* this per uplink packet — otherwise the watchdog keeps re-enabling a working mic.
|
|
778
|
+
*/
|
|
779
|
+
fun reportGlassesAudioActivity() {
|
|
780
|
+
val now = System.currentTimeMillis()
|
|
781
|
+
lastLc3Event = now
|
|
782
|
+
synchronized(micHealthLock) {
|
|
783
|
+
lastPcmProducedAt = now
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private fun micHealthSnapshotLocked(): MicHealth =
|
|
788
|
+
MicHealth(
|
|
789
|
+
sequenceGapEvents = sequenceGapEvents,
|
|
790
|
+
decodeFailures = decodeFailures,
|
|
791
|
+
lastLc3ReceivedAt = lastLc3ReceivedAt,
|
|
792
|
+
lastPcmProducedAt = lastPcmProducedAt,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
private fun resetMicHealth() {
|
|
796
|
+
synchronized(micHealthLock) {
|
|
797
|
+
sequenceGapEvents = 0
|
|
798
|
+
decodeFailures = 0
|
|
799
|
+
lastLc3ReceivedAt = null
|
|
800
|
+
lastPcmProducedAt = null
|
|
801
|
+
lastLc3Sequence = null
|
|
802
|
+
}
|
|
803
|
+
lastLc3Event = null
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fun resetMicSequenceBaseline() {
|
|
807
|
+
synchronized(micHealthLock) { lastLc3Sequence = null }
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private fun recordLc3Packet(sequenceNumber: Int?) {
|
|
811
|
+
var gap = false
|
|
812
|
+
val health = synchronized(micHealthLock) {
|
|
813
|
+
val now = System.currentTimeMillis()
|
|
814
|
+
lastLc3ReceivedAt = now
|
|
815
|
+
lastLc3Event = now
|
|
816
|
+
if (sequenceNumber != null) {
|
|
817
|
+
val normalized = sequenceNumber and 0xff
|
|
818
|
+
lastLc3Sequence?.let { previous ->
|
|
819
|
+
val expected = (previous + 1) and 0xff
|
|
820
|
+
if (normalized != expected) {
|
|
821
|
+
gap = true
|
|
822
|
+
sequenceGapEvents += 1
|
|
823
|
+
Bridge.log("MAN: LC3 packet sequence mismatch. Expected: $expected, Got: $normalized")
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
lastLc3Sequence = normalized
|
|
827
|
+
}
|
|
828
|
+
micHealthSnapshotLocked()
|
|
829
|
+
}
|
|
830
|
+
if (gap) Bridge.sendMicHealth(health, "sequence_gap")
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private fun recordMicDecodeFailure() {
|
|
834
|
+
val health = synchronized(micHealthLock) {
|
|
835
|
+
decodeFailures += 1
|
|
836
|
+
micHealthSnapshotLocked()
|
|
837
|
+
}
|
|
838
|
+
Bridge.sendMicHealth(health, "decode_failure")
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private fun recordMicPcmProduced() {
|
|
842
|
+
synchronized(micHealthLock) { lastPcmProducedAt = System.currentTimeMillis() }
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Handle raw LC3 audio data from glasses. Decodes the glasses LC3, then passes to handlePcm for
|
|
847
|
+
* canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
|
|
848
|
+
* phone→cloud encoding.
|
|
849
|
+
*/
|
|
850
|
+
@JvmOverloads
|
|
851
|
+
fun handleGlassesMicData(rawLC3Data: ByteArray, frameSize: Int = 40, sequenceNumber: Int? = null) {
|
|
852
|
+
recordLc3Packet(sequenceNumber)
|
|
853
|
+
val pcmData: ByteArray?
|
|
854
|
+
synchronized(lc3Lock) {
|
|
855
|
+
if (lc3DecoderPtr == 0L) {
|
|
856
|
+
Bridge.log("MAN: LC3 decoder not initialized, cannot process glasses audio")
|
|
857
|
+
recordMicDecodeFailure()
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
// Decode glasses LC3 to PCM (glasses may use different LC3 configs)
|
|
863
|
+
pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, rawLC3Data, frameSize)
|
|
864
|
+
} catch (e: Exception) {
|
|
865
|
+
Bridge.log("MAN: Failed to decode glasses LC3: ${e.message}")
|
|
866
|
+
recordMicDecodeFailure()
|
|
867
|
+
return
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (pcmData != null && pcmData.isNotEmpty()) {
|
|
871
|
+
// Re-encode to canonical LC3 via handlePcm (outside lock to avoid deadlock)
|
|
872
|
+
recordMicPcmProduced()
|
|
873
|
+
handlePcm(pcmData)
|
|
874
|
+
} else {
|
|
875
|
+
Bridge.log("MAN: LC3 decode returned empty data")
|
|
876
|
+
recordMicDecodeFailure()
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
fun handlePcm(pcmData: ByteArray) {
|
|
881
|
+
// Audio always flows. The previous phone-side Silero VAD gate was a
|
|
882
|
+
// bandwidth-saver that ate transcripts when the mic delivered frames
|
|
883
|
+
// not aligned to 512 samples (the case for Android AudioRecord on the
|
|
884
|
+
// phone internal mic) and was never wired up correctly anyway —
|
|
885
|
+
// `bypass_vad_for_debugging` was dead, cloud-side `bypass_vad` was
|
|
886
|
+
// the only knob, and the policy double-VAD'd what the cloud already
|
|
887
|
+
// VADs server-side. VadGateSpeechPolicy is kept around because
|
|
888
|
+
// hardware-side VAD events from the glasses route through the same
|
|
889
|
+
// class to fire `vad_status` (a separate signal the cloud SDK
|
|
890
|
+
// surfaces as `session.audio.isSpeaking`).
|
|
891
|
+
handleSendingPcm(pcmData)
|
|
892
|
+
// Speaker identification / enrollment tap — no-op unless enabled.
|
|
893
|
+
SpeakerIdEngine.acceptAudio(pcmData)
|
|
894
|
+
if (shouldSendTranscript || localSttFallbackActive) {
|
|
895
|
+
if (ensureTranscriberInitialized()) {
|
|
896
|
+
transcriber?.acceptAudio(pcmData)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// turns a single mic on and turns off all other mics:
|
|
902
|
+
private var isUpdatingMicState = false
|
|
903
|
+
private var pendingMicStateUpdate = false
|
|
904
|
+
|
|
905
|
+
internal fun updateMicState() {
|
|
906
|
+
// Guard against re-entrant calls from onRouteChange callbacks
|
|
907
|
+
if (isUpdatingMicState) {
|
|
908
|
+
pendingMicStateUpdate = true
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
isUpdatingMicState = true
|
|
912
|
+
pendingMicStateUpdate = false
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
updateMicStateInternal()
|
|
916
|
+
} finally {
|
|
917
|
+
isUpdatingMicState = false
|
|
918
|
+
// If a re-entrant call was requested, run it now
|
|
919
|
+
if (pendingMicStateUpdate) {
|
|
920
|
+
pendingMicStateUpdate = false
|
|
921
|
+
updateMicState()
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private fun updateMicStateInternal() {
|
|
927
|
+
// go through the micRanking and find the first mic that is available:
|
|
928
|
+
var micUsed: String = ""
|
|
929
|
+
|
|
930
|
+
// allow the sgc to make changes to the micRanking:
|
|
931
|
+
micRanking = sgc?.sortMicRanking(micRanking) ?: micRanking
|
|
932
|
+
Bridge.log("MAN: updateMicState() micRanking: $micRanking")
|
|
933
|
+
|
|
934
|
+
if (micEnabled) {
|
|
935
|
+
|
|
936
|
+
for (micMode in micRanking) {
|
|
937
|
+
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
938
|
+
micMode == MicTypes.BLUETOOTH_CLASSIC ||
|
|
939
|
+
micMode == MicTypes.BLUETOOTH
|
|
940
|
+
) {
|
|
941
|
+
|
|
942
|
+
if (phoneMic?.isRecordingWithMode(micMode) == true) {
|
|
943
|
+
micUsed = micMode
|
|
944
|
+
break
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (systemMicUnavailable) {
|
|
948
|
+
Bridge.log("MAN: systemMicUnavailable, continuing to next mic")
|
|
949
|
+
continue
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// if the phone mic is not recording, start recording:
|
|
953
|
+
val success = phoneMic?.startMode(micMode) ?: false
|
|
954
|
+
Bridge.log("MAN: starting mic mode: $micMode -> $success")
|
|
955
|
+
if (success) {
|
|
956
|
+
micUsed = micMode
|
|
957
|
+
break
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (micMode == MicTypes.GLASSES_CUSTOM) {
|
|
962
|
+
if (sgc?.hasMic == true) {
|
|
963
|
+
// enable the mic if it's not already on:
|
|
964
|
+
if (sgc?.micEnabled == false) {
|
|
965
|
+
sgc?.setMicEnabled(true)
|
|
966
|
+
micUsed = micMode
|
|
967
|
+
break
|
|
968
|
+
} else {
|
|
969
|
+
// the mic is already on, mark it as used and break:
|
|
970
|
+
micUsed = micMode
|
|
971
|
+
break
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// if the glasses doesn't have a mic, continue to the next mic:
|
|
975
|
+
continue
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
currentMic = micUsed
|
|
981
|
+
|
|
982
|
+
if (micUsed == "" && micEnabled) {
|
|
983
|
+
Bridge.log("MAN: No available mic found!")
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// go through and disable all mics after the first used one:
|
|
988
|
+
val allMics = micRanking
|
|
989
|
+
// add any missing mics to the list:
|
|
990
|
+
for (micMode in MicMap.map["auto"]!!) {
|
|
991
|
+
if (!allMics.contains(micMode)) {
|
|
992
|
+
allMics.add(micMode)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (micMode in allMics) {
|
|
996
|
+
if (micMode == micUsed) {
|
|
997
|
+
continue
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
1001
|
+
micMode == MicTypes.BLUETOOTH_CLASSIC ||
|
|
1002
|
+
micMode == MicTypes.BLUETOOTH
|
|
1003
|
+
) {
|
|
1004
|
+
phoneMic?.stopMode(micMode)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true
|
|
1008
|
+
) {
|
|
1009
|
+
sgc?.setMicEnabled(false)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private fun setOnboardMicEnabled(enabled: Boolean) {
|
|
1015
|
+
Bridge.log("MAN: setOnboardMicEnabled(): $enabled")
|
|
1016
|
+
if (enabled) {
|
|
1017
|
+
phoneMic?.startRecording()
|
|
1018
|
+
} else {
|
|
1019
|
+
phoneMic?.stopRecording()
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
fun sendCurrentState() {
|
|
1024
|
+
val hUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
1025
|
+
// Bridge.log("MAN: sendCurrentState(): $isHeadUp")
|
|
1026
|
+
if (screenDisabled) {
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// executor.execute {
|
|
1031
|
+
val currentStateIndex = if (hUp && contextualDashboard) 1 else 0
|
|
1032
|
+
val currentViewState: ViewState = viewStates[currentStateIndex]
|
|
1033
|
+
|
|
1034
|
+
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
|
|
1035
|
+
// dont send the event to glasses that aren't there:
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
var fullyBooted = sgc?.fullyBooted ?: false
|
|
1040
|
+
if (!fullyBooted) {
|
|
1041
|
+
Bridge.log("MAN: DeviceManager.sendCurrentState(): sgc not ready")
|
|
1042
|
+
return
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Cancel any pending clear display work item
|
|
1046
|
+
// sendStateWorkItem?.let { mainHandler.removeCallbacks(it) }
|
|
1047
|
+
|
|
1048
|
+
// Bridge.log("MAN: parsing layoutType: ${currentViewState.layoutType}")
|
|
1049
|
+
// Bridge.log(
|
|
1050
|
+
// "MAN: viewState text: '${currentViewState.text}' (len=${currentViewState.text.length})"
|
|
1051
|
+
// )
|
|
1052
|
+
|
|
1053
|
+
when (currentViewState.layoutType) {
|
|
1054
|
+
"text_wall" -> {
|
|
1055
|
+
sgc?.sendTextWall(parsePlaceholders(currentViewState.text))
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
"double_text_wall" -> {
|
|
1059
|
+
sgc?.sendDoubleTextWall(
|
|
1060
|
+
parsePlaceholders(currentViewState.topText),
|
|
1061
|
+
parsePlaceholders(currentViewState.bottomText)
|
|
1062
|
+
)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
"reference_card" -> {
|
|
1066
|
+
val title = parsePlaceholders(currentViewState.title)
|
|
1067
|
+
val text = parsePlaceholders(currentViewState.text)
|
|
1068
|
+
sgc?.sendTextWall("$title\n\n$text")
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
"bitmap_view" -> {
|
|
1072
|
+
currentViewState.data?.let { data ->
|
|
1073
|
+
sgc?.displayBitmap(
|
|
1074
|
+
data,
|
|
1075
|
+
currentViewState.bmpX,
|
|
1076
|
+
currentViewState.bmpY,
|
|
1077
|
+
currentViewState.bmpWidth,
|
|
1078
|
+
currentViewState.bmpHeight
|
|
1079
|
+
)
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
"positioned_text" -> {
|
|
1084
|
+
sgc?.sendPositionedText(
|
|
1085
|
+
parsePlaceholders(currentViewState.text),
|
|
1086
|
+
currentViewState.bmpX ?: 0,
|
|
1087
|
+
currentViewState.bmpY ?: 0,
|
|
1088
|
+
currentViewState.bmpWidth ?: 576,
|
|
1089
|
+
currentViewState.bmpHeight ?: 288,
|
|
1090
|
+
currentViewState.borderWidth ?: 0,
|
|
1091
|
+
currentViewState.borderRadius ?: 0
|
|
1092
|
+
)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
"scene" -> {
|
|
1096
|
+
sceneStates[currentStateIndex]?.let { sgc?.applySceneFrame(it) }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
"clear_view" -> sgc?.clearDisplay()
|
|
1100
|
+
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
1101
|
+
}
|
|
1102
|
+
// }
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private fun parsePlaceholders(text: String): String {
|
|
1106
|
+
if (!text.contains('$')) return text
|
|
1107
|
+
|
|
1108
|
+
val dateFormatter = SimpleDateFormat("M/dd, h:mm", Locale.getDefault())
|
|
1109
|
+
val formattedDate = dateFormatter.format(Date())
|
|
1110
|
+
|
|
1111
|
+
val time12Format = SimpleDateFormat("hh:mm", Locale.getDefault())
|
|
1112
|
+
val time12 = time12Format.format(Date())
|
|
1113
|
+
|
|
1114
|
+
val time24Format = SimpleDateFormat("HH:mm", Locale.getDefault())
|
|
1115
|
+
val time24 = time24Format.format(Date())
|
|
1116
|
+
|
|
1117
|
+
val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault())
|
|
1118
|
+
val currentDate = dateFormat.format(Date())
|
|
1119
|
+
|
|
1120
|
+
val placeholders =
|
|
1121
|
+
mapOf(
|
|
1122
|
+
"\$no_datetime$" to formattedDate,
|
|
1123
|
+
"\$DATE$" to currentDate,
|
|
1124
|
+
"\$TIME12$" to time12,
|
|
1125
|
+
"\$TIME24$" to time24,
|
|
1126
|
+
"\$GBATT$" to
|
|
1127
|
+
(sgc?.batteryLevel?.let { if (it == -1) "" else "$it%" } ?: ""),
|
|
1128
|
+
"\$CONNECTION_STATUS$" to "Connected"
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
return placeholders.entries.fold(text) { result, (key, value) ->
|
|
1132
|
+
result.replace(key, value)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private fun appendLog(entry: String) {
|
|
1137
|
+
lastLog = (lastLog + entry).takeLast(100).toMutableList()
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
fun onRouteChange(reason: String, availableInputs: List<String>) {
|
|
1141
|
+
Bridge.log("MAN: onRouteChange: reason: $reason")
|
|
1142
|
+
Bridge.log("MAN: onRouteChange: inputs: $availableInputs")
|
|
1143
|
+
|
|
1144
|
+
// Handle external app conflicts - automatically switch to glasses mic if available
|
|
1145
|
+
when (reason) {
|
|
1146
|
+
"external_app_recording" -> {
|
|
1147
|
+
// Another app is using the microphone
|
|
1148
|
+
systemMicUnavailable = true
|
|
1149
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
1150
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
1151
|
+
// scheduleSystemMicAvailabilityRecheck("external_app_recording")
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
"audio_focus_available" -> {
|
|
1155
|
+
// Audio focus is available again
|
|
1156
|
+
systemMicUnavailable = false
|
|
1157
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
1158
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
"external_app_stopped" -> {
|
|
1162
|
+
// External app stopped recording
|
|
1163
|
+
systemMicUnavailable = false
|
|
1164
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
1165
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
"phone_call_interruption" -> {
|
|
1169
|
+
// Phone call started - mark mic as unavailable
|
|
1170
|
+
systemMicUnavailable = true
|
|
1171
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1172
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1173
|
+
// scheduleSystemMicAvailabilityRecheck("phone_call_interruption")
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
"phone_call_ended" -> {
|
|
1177
|
+
// Phone call ended - mark mic as available again
|
|
1178
|
+
systemMicUnavailable = false
|
|
1179
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
1180
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
"phone_call_active" -> {
|
|
1184
|
+
// Tried to start recording while phone call already active
|
|
1185
|
+
systemMicUnavailable = true
|
|
1186
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1187
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
"audio_focus_denied" -> {
|
|
1191
|
+
// Another app has audio focus
|
|
1192
|
+
systemMicUnavailable = true
|
|
1193
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1194
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
"permission_denied" -> {
|
|
1198
|
+
// Microphone permission not granted
|
|
1199
|
+
systemMicUnavailable = true
|
|
1200
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
1201
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
1202
|
+
// Don't trigger fallback - need to request permission from user
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
"audio_route_changed" -> {
|
|
1206
|
+
// Audio route changed
|
|
1207
|
+
// systemMicUnavailable = false
|
|
1208
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
1209
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
"recording_started" -> {
|
|
1213
|
+
// this is an event from the PhoneMic saying we have started recording
|
|
1214
|
+
// Audio recording started
|
|
1215
|
+
// systemMicUnavailable = true
|
|
1216
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
1217
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
"recording_stopped" -> {
|
|
1221
|
+
// this is an event from the PhoneMic saying we have stopped recording
|
|
1222
|
+
// Audio recording stopped
|
|
1223
|
+
// systemMicUnavailable = false
|
|
1224
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1225
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
else -> {
|
|
1229
|
+
// Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
|
|
1230
|
+
// Just log for now - may want to handle these in the future
|
|
1231
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
1232
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
1233
|
+
// systemMicUnavailable = false
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
updateMicState()
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
fun onInterruption(began: Boolean) {
|
|
1241
|
+
Bridge.log("MAN: Interruption: $began")
|
|
1242
|
+
systemMicUnavailable = began
|
|
1243
|
+
updateMicState()
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// MARK: - Auxiliary Commands
|
|
1247
|
+
|
|
1248
|
+
fun initSGC(wearable: String) {
|
|
1249
|
+
Bridge.log("Initializing manager for wearable: $wearable")
|
|
1250
|
+
if (sgc != null && sgc?.type != wearable) {
|
|
1251
|
+
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
1252
|
+
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
1253
|
+
sgc?.cleanup()
|
|
1254
|
+
sgc = null
|
|
1255
|
+
resetSystemTimeSync()
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (sgc != null) {
|
|
1259
|
+
Bridge.log("MAN: SGC already initialized")
|
|
1260
|
+
return
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (wearable.contains(DeviceTypes.SIMULATED)) {
|
|
1264
|
+
sgc = Simulated()
|
|
1265
|
+
} else if (wearable.contains(DeviceTypes.G1)) {
|
|
1266
|
+
sgc = G1()
|
|
1267
|
+
} else if (wearable.contains(DeviceTypes.G2)) {
|
|
1268
|
+
sgc = G2()
|
|
1269
|
+
} else if (wearable.contains(DeviceTypes.LIVE)) {
|
|
1270
|
+
sgc = MentraLive()
|
|
1271
|
+
} else if (wearable.contains(DeviceTypes.NEX)) {
|
|
1272
|
+
sgc = MentraNex()
|
|
1273
|
+
} else if (wearable.contains(DeviceTypes.AR99)) {
|
|
1274
|
+
sgc = Ar99()
|
|
1275
|
+
} else if (wearable.contains(DeviceTypes.MACH1)) {
|
|
1276
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
|
|
1277
|
+
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
1278
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.Z100)
|
|
1279
|
+
} else if (wearable.contains(DeviceTypes.NIMO)) {
|
|
1280
|
+
sgc = Nimo()
|
|
1281
|
+
} else if (wearable.contains(DeviceTypes.FRAME)) {
|
|
1282
|
+
// sgc = FrameManager()
|
|
1283
|
+
}
|
|
1284
|
+
// update device model:
|
|
1285
|
+
DeviceStore.apply("glasses", "deviceModel", sgc?.type ?: "")
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
private fun createOptionalMach1Sgc(deviceType: String): SGCManager? {
|
|
1289
|
+
return try {
|
|
1290
|
+
Mach1().also { sgc ->
|
|
1291
|
+
if (deviceType == DeviceTypes.Z100) {
|
|
1292
|
+
sgc.type = DeviceTypes.Z100
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch (e: LinkageError) {
|
|
1296
|
+
Bridge.log("Failed to initialize $deviceType support: ${e.message}")
|
|
1297
|
+
null
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
fun initController(controllerType: String) {
|
|
1302
|
+
Bridge.log("MAN: Initializing controller: $controllerType")
|
|
1303
|
+
if (controller != null && controller?.type != controllerType) {
|
|
1304
|
+
Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
|
|
1305
|
+
controller?.cleanup()
|
|
1306
|
+
controller = null
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (controller != null) {
|
|
1310
|
+
Bridge.log("MAN: Controller already initialized")
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (controllerType == ControllerTypes.R1) {
|
|
1315
|
+
controller = R1()
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
fun restartTranscriber() {
|
|
1320
|
+
Bridge.log("MAN: Restarting transcriber via command")
|
|
1321
|
+
if (ensureTranscriberInitialized()) {
|
|
1322
|
+
transcriber?.restart()
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// MARK: - Speaker identification
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Enrollment holds the mic open (see setMicState). Set by
|
|
1330
|
+
* startSpeakerEnrollment / cleared when the session ends or is cancelled.
|
|
1331
|
+
*/
|
|
1332
|
+
@Volatile private var speakerEnrollmentActive = false
|
|
1333
|
+
|
|
1334
|
+
fun setSpeakerIdEnabled(enabled: Boolean) {
|
|
1335
|
+
SpeakerIdEngine.setIdentifyEnabled(enabled)
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
fun setSpeakerIdModelPath(path: String) {
|
|
1339
|
+
SpeakerIdEngine.setModelPath(path)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
fun setSpeakerProfiles(profiles: List<Map<String, Any?>>) {
|
|
1343
|
+
val parsed = profiles.mapNotNull { entry ->
|
|
1344
|
+
val id = entry["id"] as? String ?: return@mapNotNull null
|
|
1345
|
+
val name = entry["name"] as? String ?: return@mapNotNull null
|
|
1346
|
+
val base64 = entry["embedding"] as? String ?: return@mapNotNull null
|
|
1347
|
+
val embedding = SpeakerIdEngine.decodeEmbedding(base64) ?: return@mapNotNull null
|
|
1348
|
+
SpeakerIdEngine.Profile(id, name, embedding)
|
|
1349
|
+
}
|
|
1350
|
+
SpeakerIdEngine.setProfiles(parsed)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
fun startSpeakerEnrollment(targetSpeechSeconds: Double) {
|
|
1354
|
+
val source = if (currentMic == MicTypes.GLASSES_CUSTOM) "glasses" else "phone"
|
|
1355
|
+
speakerEnrollmentActive = true
|
|
1356
|
+
setMicState()
|
|
1357
|
+
SpeakerIdEngine.startEnrollment(targetSpeechSeconds, source) {
|
|
1358
|
+
speakerEnrollmentActive = false
|
|
1359
|
+
mainHandler.post { setMicState() }
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
fun cancelSpeakerEnrollment() {
|
|
1364
|
+
SpeakerIdEngine.cancelEnrollment()
|
|
1365
|
+
speakerEnrollmentActive = false
|
|
1366
|
+
setMicState()
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Local transcription envelope, iOS STTTools parity. Native-side join with
|
|
1371
|
+
* SpeakerIdEngine: stamp the most recent identification match onto the
|
|
1372
|
+
* transcript. Fallback stays `0`/unknown when the engine is off or nothing
|
|
1373
|
+
* matched, so downstream schemas are unchanged; the resolved name is
|
|
1374
|
+
* additive.
|
|
1375
|
+
*/
|
|
1376
|
+
private fun buildLocalTranscription(text: String, isFinal: Boolean, language: String): Map<String, Any> {
|
|
1377
|
+
val now = System.currentTimeMillis()
|
|
1378
|
+
val map = HashMap<String, Any>()
|
|
1379
|
+
map["type"] = "local_transcription"
|
|
1380
|
+
map["text"] = text
|
|
1381
|
+
map["isFinal"] = isFinal
|
|
1382
|
+
map["startTime"] = now - if (isFinal) 2000 else 1000
|
|
1383
|
+
map["endTime"] = now
|
|
1384
|
+
map["language"] = language
|
|
1385
|
+
map["transcribeLanguage"] = language
|
|
1386
|
+
map["provider"] = "sherpa-onnx"
|
|
1387
|
+
val match = SpeakerIdEngine.recentMatch(SPEAKER_MATCH_WINDOW_MS)
|
|
1388
|
+
if (match != null) {
|
|
1389
|
+
map["speakerId"] = match.profileId
|
|
1390
|
+
map["resolvedSpeakerId"] = match.profileId
|
|
1391
|
+
map["resolvedSpeakerName"] = match.name
|
|
1392
|
+
if (isFinal) Bridge.log("SpeakerID: local final stamped as ${match.name}")
|
|
1393
|
+
} else {
|
|
1394
|
+
map["speakerId"] = 0
|
|
1395
|
+
}
|
|
1396
|
+
return map
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
private fun ensureTranscriberInitialized(): Boolean {
|
|
1400
|
+
if (transcriber != null) return true
|
|
1401
|
+
|
|
1402
|
+
return try {
|
|
1403
|
+
val context = Bridge.getContext()
|
|
1404
|
+
val nextTranscriber = SherpaOnnxTranscriber(context)
|
|
1405
|
+
nextTranscriber.setTranscriptListener(
|
|
1406
|
+
object : SherpaOnnxTranscriber.TranscriptListener {
|
|
1407
|
+
override fun onPartialResult(text: String, language: String) {
|
|
1408
|
+
Bridge.log("STT: Partial result: $text")
|
|
1409
|
+
// The Sherpa model emits all-caps text; lowercase English
|
|
1410
|
+
// output to match the rest of the pipeline (parity with iOS).
|
|
1411
|
+
val formatted = if (language == "en-US") text.lowercase() else text
|
|
1412
|
+
Bridge.sendLocalTranscription(buildLocalTranscription(formatted, false, language))
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
override fun onFinalResult(text: String, language: String) {
|
|
1416
|
+
Bridge.log("STT: Final result: $text")
|
|
1417
|
+
// The Sherpa model emits all-caps text; lowercase English
|
|
1418
|
+
// output to match the rest of the pipeline (parity with iOS).
|
|
1419
|
+
val formatted = if (language == "en-US") text.lowercase() else text
|
|
1420
|
+
Bridge.sendLocalTranscription(buildLocalTranscription(formatted, true, language))
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
)
|
|
1424
|
+
nextTranscriber.initialize()
|
|
1425
|
+
if (!nextTranscriber.isInitialized()) {
|
|
1426
|
+
Bridge.log("SherpaOnnxTranscriber initialize() returned without becoming ready")
|
|
1427
|
+
return false
|
|
1428
|
+
}
|
|
1429
|
+
transcriber = nextTranscriber
|
|
1430
|
+
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
1431
|
+
true
|
|
1432
|
+
} catch (e: Exception) {
|
|
1433
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
1434
|
+
transcriber = null
|
|
1435
|
+
false
|
|
1436
|
+
} catch (e: LinkageError) {
|
|
1437
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
1438
|
+
transcriber = null
|
|
1439
|
+
false
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// MARK: - connection state management
|
|
1444
|
+
|
|
1445
|
+
fun handleDeviceReady() {
|
|
1446
|
+
if (sgc == null) {
|
|
1447
|
+
Bridge.log("MAN: SGC is null, returning")
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (pendingDeviceName.isNotEmpty()) {
|
|
1452
|
+
deviceName = pendingDeviceName
|
|
1453
|
+
}
|
|
1454
|
+
if (pendingDeviceAddress.isNotEmpty()) {
|
|
1455
|
+
deviceAddress = pendingDeviceAddress
|
|
1456
|
+
}
|
|
1457
|
+
clearPendingConnection()
|
|
1458
|
+
|
|
1459
|
+
val readyKey = "${sgc?.type}:${deviceName}"
|
|
1460
|
+
val now = System.currentTimeMillis()
|
|
1461
|
+
if (readyKey == lastReadyHandledKey && now - lastReadyHandledAtMs < 2000) {
|
|
1462
|
+
Bridge.log("MAN: handleDeviceReady() duplicate suppressed for $readyKey")
|
|
1463
|
+
return
|
|
1464
|
+
}
|
|
1465
|
+
lastReadyHandledKey = readyKey
|
|
1466
|
+
lastReadyHandledAtMs = now
|
|
1467
|
+
|
|
1468
|
+
Bridge.log("MAN: handleDeviceReady() ${sgc?.type}")
|
|
1469
|
+
resetMicHealth()
|
|
1470
|
+
defaultWearable = sgc?.type ?: ""
|
|
1471
|
+
searching = false
|
|
1472
|
+
|
|
1473
|
+
syncSystemTimeOnceForConnection(readyKey)
|
|
1474
|
+
|
|
1475
|
+
// re-apply display height/depth after reconnection
|
|
1476
|
+
mainHandler.postDelayed(
|
|
1477
|
+
{
|
|
1478
|
+
val h =
|
|
1479
|
+
(DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)
|
|
1480
|
+
?.toInt()
|
|
1481
|
+
?: 4
|
|
1482
|
+
val rawDepth =
|
|
1483
|
+
(DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)
|
|
1484
|
+
?.toInt()
|
|
1485
|
+
?: dashboardDepth // canonical default (2), not 1
|
|
1486
|
+
val d = rawDepth.coerceIn(1, 4)
|
|
1487
|
+
sgc?.setDashboardPosition(h, d)
|
|
1488
|
+
},
|
|
1489
|
+
2000
|
|
1490
|
+
)
|
|
1491
|
+
|
|
1492
|
+
// Show welcome message on first connect for all display glasses
|
|
1493
|
+
if (shouldSendBootingMessage) {
|
|
1494
|
+
shouldSendBootingMessage = false
|
|
1495
|
+
executor.execute {
|
|
1496
|
+
// Wait until stock-vs-CFW is known: a banner drawn down the stock
|
|
1497
|
+
// path with the clear routed down the CFW path never leaves the
|
|
1498
|
+
// glass (the clear can't reach the stock text container).
|
|
1499
|
+
sgc?.awaitFirmwareClassification()
|
|
1500
|
+
sgc?.sendTextWall("// Glassly connected")
|
|
1501
|
+
Thread.sleep(3000)
|
|
1502
|
+
sgc?.clearDisplay()
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Call device-specific setup handlers
|
|
1507
|
+
if (defaultWearable.contains(DeviceTypes.G1)) {
|
|
1508
|
+
handleG1Ready()
|
|
1509
|
+
} else if (defaultWearable.contains(DeviceTypes.MACH1)) {
|
|
1510
|
+
handleMach1Ready()
|
|
1511
|
+
} else if (defaultWearable.contains(DeviceTypes.Z100)) {
|
|
1512
|
+
handleMach1Ready() // Z100 uses same initialization as Mach1
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Re-arm the mic from the stored should_send_* flags. disconnect() zeroes the
|
|
1516
|
+
// micEnabled intent, so updateMicState() alone would see stale intent and leave
|
|
1517
|
+
// the mic off; setMicState() recomputes intent from the surviving subscriptions.
|
|
1518
|
+
Bridge.log("MAN: Re-applying microphone settings after reconnection")
|
|
1519
|
+
setMicState()
|
|
1520
|
+
|
|
1521
|
+
// send to the server our battery status:
|
|
1522
|
+
Bridge.sendBatteryStatus(sgc?.batteryLevel ?: -1, false)
|
|
1523
|
+
|
|
1524
|
+
// save the default_wearable now that we're connected:
|
|
1525
|
+
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
1526
|
+
Bridge.saveSetting("device_name", deviceName)
|
|
1527
|
+
Bridge.saveSetting("device_address", deviceAddress)
|
|
1528
|
+
if (defaultWearable.contains(DeviceTypes.AR99)) {
|
|
1529
|
+
val projectName = (DeviceStore.store.get("bluetooth", "project_name") as? String)?.trim().orEmpty()
|
|
1530
|
+
Bridge.saveSetting("project_name", projectName)
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
private fun syncSystemTimeOnceForConnection(connectionKey: String) {
|
|
1535
|
+
val activeSgc = sgc ?: return
|
|
1536
|
+
if (activeSgc.type.contains(DeviceTypes.SIMULATED)) {
|
|
1537
|
+
return
|
|
1538
|
+
}
|
|
1539
|
+
if (connectionKey == lastSystemTimeSyncConnectionKey) {
|
|
1540
|
+
return
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
lastSystemTimeSyncConnectionKey = connectionKey
|
|
1544
|
+
pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
|
|
1545
|
+
val sync =
|
|
1546
|
+
Runnable {
|
|
1547
|
+
pendingSystemTimeSync = null
|
|
1548
|
+
if (lastSystemTimeSyncConnectionKey != connectionKey || sgc !== activeSgc) {
|
|
1549
|
+
return@Runnable
|
|
1550
|
+
}
|
|
1551
|
+
val timestampMs = System.currentTimeMillis()
|
|
1552
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: $timestampMs")
|
|
1553
|
+
activeSgc.sendSetSystemTime(timestampMs)
|
|
1554
|
+
}
|
|
1555
|
+
pendingSystemTimeSync = sync
|
|
1556
|
+
mainHandler.postDelayed(sync, 3000)
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private fun resetSystemTimeSync() {
|
|
1560
|
+
pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
|
|
1561
|
+
pendingSystemTimeSync = null
|
|
1562
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
private fun handleG1Ready() {
|
|
1566
|
+
// G1-specific setup (if any needed in the future)
|
|
1567
|
+
// Note: G1-specific settings like silent mode, battery status,
|
|
1568
|
+
// head up angle, brightness, etc. could be configured here
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private fun handleMach1Ready() {
|
|
1572
|
+
// Mach1-specific setup (if any needed in the future)
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
fun handleDeviceDisconnected() {
|
|
1576
|
+
Bridge.log("MAN: Device disconnected")
|
|
1577
|
+
resetSystemTimeSync()
|
|
1578
|
+
resetMicHealth()
|
|
1579
|
+
DeviceStore.apply("glasses", "headUp", false)
|
|
1580
|
+
DeviceStore.apply(
|
|
1581
|
+
"glasses",
|
|
1582
|
+
"voiceActivityDetectionEnabled",
|
|
1583
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
1584
|
+
)
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
fun handleControllerReady() {
|
|
1588
|
+
val c = controller
|
|
1589
|
+
if (c == null) {
|
|
1590
|
+
Bridge.log("MAN: Controller is nil, returning")
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
Bridge.log("MAN: handleControllerReady(): ${c.type}")
|
|
1594
|
+
|
|
1595
|
+
pendingController = ""
|
|
1596
|
+
defaultController = c.type
|
|
1597
|
+
searching = false
|
|
1598
|
+
|
|
1599
|
+
// save the default_controller now that we're connected:
|
|
1600
|
+
Bridge.saveSetting("default_controller", defaultController)
|
|
1601
|
+
Bridge.saveSetting("controller_device_name", controllerDeviceName)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
fun handleControllerDisconnected() {
|
|
1605
|
+
Bridge.log("MAN: Controller disconnected")
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// MARK: - Network Command handlers
|
|
1609
|
+
|
|
1610
|
+
fun displayText(params: Map<String, Any>) {
|
|
1611
|
+
(params["text"] as? String)?.let { text ->
|
|
1612
|
+
Bridge.log("MAN: Displaying text: $text")
|
|
1613
|
+
sgc?.sendTextWall(text)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
fun clearDisplay() {
|
|
1618
|
+
Bridge.log("MAN: Clearing Display")
|
|
1619
|
+
sgc?.clearDisplay()
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
fun displayEvent(event: Map<String, Any>) {
|
|
1623
|
+
val view = event["view"] as? String
|
|
1624
|
+
if (view == null) {
|
|
1625
|
+
Bridge.log("MAN: Invalid view")
|
|
1626
|
+
return
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
val isDashboard = view == "dashboard"
|
|
1630
|
+
val stateIndex = if (isDashboard) 1 else 0
|
|
1631
|
+
|
|
1632
|
+
// Scene frames (display.render() pipeline) take their own path: the
|
|
1633
|
+
// whole frame is the unit, not a layout, and the host's per-element
|
|
1634
|
+
// annotations make redundant frames self-deduping (all-"unchanged"
|
|
1635
|
+
// frames no-op in the SGC base handler).
|
|
1636
|
+
@Suppress("UNCHECKED_CAST") val sceneMap = event["scene"] as? Map<String, Any>
|
|
1637
|
+
if (sceneMap != null) {
|
|
1638
|
+
handleSceneEvent(stateIndex, sceneMap)
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
@Suppress("UNCHECKED_CAST") val layout = event["layout"] as? Map<String, Any> ?: return
|
|
1643
|
+
|
|
1644
|
+
val layoutType = layout["layoutType"] as? String
|
|
1645
|
+
|
|
1646
|
+
// Scene→legacy handoff: a legacy layout is about to draw over a scene
|
|
1647
|
+
// (e.g. a cloud app taking the view from a miniapp). Sweep the scene's
|
|
1648
|
+
// elements first so they don't linger under the new content; clear_view
|
|
1649
|
+
// wipes everything anyway.
|
|
1650
|
+
sceneStates[stateIndex]?.let { prevFrame ->
|
|
1651
|
+
sceneStates[stateIndex] = null
|
|
1652
|
+
if (layoutType != "clear_view") {
|
|
1653
|
+
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
val text = parsePlaceholders(layout.getString("text", " "))
|
|
1658
|
+
val topText = parsePlaceholders(layout.getString("topText", " "))
|
|
1659
|
+
val bottomText = parsePlaceholders(layout.getString("bottomText", " "))
|
|
1660
|
+
val title = parsePlaceholders(layout.getString("title", " "))
|
|
1661
|
+
val data = layout["data"] as? String
|
|
1662
|
+
|
|
1663
|
+
// Optional container position/size — used by bitmap_view and positioned_text (G2).
|
|
1664
|
+
val bmpX = (layout["x"] as? Number)?.toInt()
|
|
1665
|
+
val bmpY = (layout["y"] as? Number)?.toInt()
|
|
1666
|
+
val bmpWidth = (layout["width"] as? Number)?.toInt()
|
|
1667
|
+
val bmpHeight = (layout["height"] as? Number)?.toInt()
|
|
1668
|
+
|
|
1669
|
+
// Optional positioned_text border (G2).
|
|
1670
|
+
val borderWidth = (layout["borderWidth"] as? Number)?.toInt()
|
|
1671
|
+
val borderRadius = (layout["borderRadius"] as? Number)?.toInt()
|
|
1672
|
+
|
|
1673
|
+
var newViewState =
|
|
1674
|
+
ViewState(
|
|
1675
|
+
topText,
|
|
1676
|
+
bottomText,
|
|
1677
|
+
title,
|
|
1678
|
+
layoutType ?: "",
|
|
1679
|
+
text,
|
|
1680
|
+
data,
|
|
1681
|
+
null,
|
|
1682
|
+
bmpX,
|
|
1683
|
+
bmpY,
|
|
1684
|
+
bmpWidth,
|
|
1685
|
+
bmpHeight,
|
|
1686
|
+
borderWidth,
|
|
1687
|
+
borderRadius
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
val currentState = viewStates[stateIndex]
|
|
1691
|
+
|
|
1692
|
+
if (statesEqual(currentState, newViewState)) {
|
|
1693
|
+
return
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
viewStates[stateIndex] = newViewState
|
|
1697
|
+
val hUp = headUp && contextualDashboard
|
|
1698
|
+
// send the state we just received if the user is currently in that state:
|
|
1699
|
+
if (stateIndex == 0 && !hUp) {
|
|
1700
|
+
sendCurrentState()
|
|
1701
|
+
} else if (stateIndex == 1 && hUp) {
|
|
1702
|
+
sendCurrentState()
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/** Parse + store a scene frame, then dispatch it if its view is visible. */
|
|
1707
|
+
private fun handleSceneEvent(stateIndex: Int, sceneMap: Map<String, Any>) {
|
|
1708
|
+
var frame = parseSceneFrame(sceneMap) ?: return
|
|
1709
|
+
val prevFrame = sceneStates[stateIndex]
|
|
1710
|
+
|
|
1711
|
+
if (prevFrame == null) {
|
|
1712
|
+
// Legacy→scene handoff: stale legacy content (e.g. a cloud app's
|
|
1713
|
+
// text wall) must not linger under the scene's elements.
|
|
1714
|
+
// clearDisplay is the per-device "wipe what's there" (blank-in-place
|
|
1715
|
+
// on G2 — no page rebuild).
|
|
1716
|
+
val prevLegacyType = viewStates[stateIndex].layoutType
|
|
1717
|
+
if (prevLegacyType.isNotEmpty() && prevLegacyType != "clear_view" && prevLegacyType != "scene") {
|
|
1718
|
+
sgc?.clearDisplay()
|
|
1719
|
+
}
|
|
1720
|
+
} else if (prevFrame.appId != frame.appId) {
|
|
1721
|
+
// Cross-app switch: the host's diff baseline is per-app, so the new
|
|
1722
|
+
// app's annotations don't know the old app's elements are on the
|
|
1723
|
+
// glasses. Sweep the old app's elements (SGC registries still map
|
|
1724
|
+
// them), then paint the new frame from scratch. In practice the
|
|
1725
|
+
// boot message interposes between apps, so this isn't visible as a
|
|
1726
|
+
// blank.
|
|
1727
|
+
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1728
|
+
frame = frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Store the REDISPATCH form: any later sendCurrentState (dashboard
|
|
1732
|
+
// exit, head-up return) must repaint the whole frame — the original
|
|
1733
|
+
// annotations are only valid for the first dispatch right now.
|
|
1734
|
+
sceneStates[stateIndex] =
|
|
1735
|
+
frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1736
|
+
viewStates[stateIndex] = ViewState(" ", " ", " ", "scene", " ", null, null)
|
|
1737
|
+
|
|
1738
|
+
val hUp = headUp && contextualDashboard
|
|
1739
|
+
if ((stateIndex == 0 && !hUp) || (stateIndex == 1 && hUp)) {
|
|
1740
|
+
dispatchSceneFrame(frame)
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** Guarded scene dispatch — mirrors sendCurrentState's send conditions. */
|
|
1745
|
+
private fun dispatchSceneFrame(frame: SceneFrame) {
|
|
1746
|
+
if (screenDisabled) return
|
|
1747
|
+
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) return
|
|
1748
|
+
if (sgc?.fullyBooted != true) {
|
|
1749
|
+
Bridge.log("MAN: dispatchSceneFrame(): sgc not ready")
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
sgc?.applySceneFrame(frame)
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
@Suppress("UNCHECKED_CAST")
|
|
1756
|
+
private fun parseSceneFrame(sceneMap: Map<String, Any>): SceneFrame? {
|
|
1757
|
+
val elementsRaw = sceneMap["elements"] as? List<Map<String, Any>> ?: return null
|
|
1758
|
+
val elements =
|
|
1759
|
+
elementsRaw.mapNotNull { el ->
|
|
1760
|
+
val box = el["box"] as? Map<String, Any> ?: return@mapNotNull null
|
|
1761
|
+
val style = el["style"] as? Map<String, Any>
|
|
1762
|
+
SceneElement(
|
|
1763
|
+
id = el["id"] as? String ?: return@mapNotNull null,
|
|
1764
|
+
type = el["type"] as? String ?: return@mapNotNull null,
|
|
1765
|
+
x = (box["x"] as? Number)?.toInt() ?: 0,
|
|
1766
|
+
y = (box["y"] as? Number)?.toInt() ?: 0,
|
|
1767
|
+
w = (box["w"] as? Number)?.toInt() ?: 0,
|
|
1768
|
+
h = (box["h"] as? Number)?.toInt() ?: 0,
|
|
1769
|
+
text = (el["text"] as? String)?.let { parsePlaceholders(it) },
|
|
1770
|
+
data = el["data"] as? String,
|
|
1771
|
+
border = (style?.get("border") as? Number)?.toInt() ?: 0,
|
|
1772
|
+
radius = (style?.get("radius") as? Number)?.toInt() ?: 0,
|
|
1773
|
+
change = el["change"] as? String ?: "created",
|
|
1774
|
+
contentHash = el["contentHash"] as? String ?: "",
|
|
1775
|
+
font = style?.get("font") as? String ?: "default",
|
|
1776
|
+
fontSize = (style?.get("fontSize") as? Number)?.toInt() ?: 0,
|
|
1777
|
+
)
|
|
1778
|
+
}
|
|
1779
|
+
val canvas = sceneMap["canvas"] as? Map<String, Any>
|
|
1780
|
+
return SceneFrame(
|
|
1781
|
+
appId = sceneMap["appId"] as? String ?: "",
|
|
1782
|
+
epoch = (sceneMap["sceneEpoch"] as? Number)?.toInt() ?: 0,
|
|
1783
|
+
replay = sceneMap["replay"] as? Boolean ?: false,
|
|
1784
|
+
elements = elements,
|
|
1785
|
+
removed = (sceneMap["removed"] as? List<*>)?.filterIsInstance<String>() ?: emptyList(),
|
|
1786
|
+
canvasWidth = (canvas?.get("width") as? Number)?.toInt()?.takeIf { it > 0 }
|
|
1787
|
+
?: SceneFrame.DEFAULT_CANVAS_WIDTH,
|
|
1788
|
+
canvasHeight = (canvas?.get("height") as? Number)?.toInt()?.takeIf { it > 0 }
|
|
1789
|
+
?: SceneFrame.DEFAULT_CANVAS_HEIGHT,
|
|
1790
|
+
)
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
fun showDashboard() {
|
|
1794
|
+
sgc?.showDashboard()
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// ---------- Native navigation (glasses-rendered nav UI, e.g. G2 service 0x08) ----------
|
|
1798
|
+
|
|
1799
|
+
fun navigationModeStart() {
|
|
1800
|
+
sgc?.enterNavigationMode()
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Exit the native nav UI. The SGC repaints the retained view state itself once the
|
|
1805
|
+
* firmware hands the screen back (see G2.exitNavigationMode).
|
|
1806
|
+
*/
|
|
1807
|
+
fun navigationModeStop(arrived: Boolean) {
|
|
1808
|
+
sgc?.exitNavigationMode(arrived)
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
fun navigationUpdateInfo(params: Map<String, Any?>) {
|
|
1812
|
+
sgc?.sendNavigationInfo(
|
|
1813
|
+
directionIcon = (params["directionIcon"] as? Number)?.toInt() ?: 0,
|
|
1814
|
+
distance = params["distance"] as? String ?: "",
|
|
1815
|
+
roadName = params["roadName"] as? String ?: "",
|
|
1816
|
+
spendTime = params["spendTime"] as? String ?: "",
|
|
1817
|
+
remainDistance = params["remainDistance"] as? String ?: "",
|
|
1818
|
+
etaTime = params["etaTime"] as? String ?: "",
|
|
1819
|
+
speed = params["speed"] as? String ?: ""
|
|
1820
|
+
)
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
fun navigationMiniMap(params: Map<String, Any?>) {
|
|
1824
|
+
val data = params["data"] as? String
|
|
1825
|
+
if (data == null) {
|
|
1826
|
+
Bridge.log("MAN: navigationMiniMap missing data field")
|
|
1827
|
+
return
|
|
1828
|
+
}
|
|
1829
|
+
sgc?.sendNavigationMiniMap(
|
|
1830
|
+
base64ImageData = data,
|
|
1831
|
+
rotationDeg = (params["rotationDeg"] as? Number)?.toInt() ?: 0,
|
|
1832
|
+
borderEnabled = (params["border"] as? Boolean) ?: true
|
|
1833
|
+
)
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
fun navigationOverviewMap(params: Map<String, Any?>) {
|
|
1837
|
+
val data = params["data"] as? String
|
|
1838
|
+
if (data == null) {
|
|
1839
|
+
Bridge.log("MAN: navigationOverviewMap missing data field")
|
|
1840
|
+
return
|
|
1841
|
+
}
|
|
1842
|
+
sgc?.sendNavigationOverviewMap(data)
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
fun showNotificationsPanel() {
|
|
1846
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
1847
|
+
sgc?.showNotificationsPanel()
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
fun ping() {
|
|
1852
|
+
sgc?.ping()
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
fun dbg1() {
|
|
1856
|
+
Bridge.log("MAN: dbg1()")
|
|
1857
|
+
sgc?.dbg1()
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
fun dbg2() {
|
|
1861
|
+
Bridge.log("MAN: dbg2()")
|
|
1862
|
+
sgc?.dbg2()
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
fun startStream(message: MutableMap<String, Any>) {
|
|
1866
|
+
Bridge.log("MAN: startStream")
|
|
1867
|
+
sgc?.startStream(message)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
fun stopStream() {
|
|
1871
|
+
Bridge.log("MAN: stopStream")
|
|
1872
|
+
sgc?.stopStream()
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
fun keepStreamAlive(message: MutableMap<String, Any>) {
|
|
1876
|
+
Bridge.log("MAN: keepStreamAlive: (message)")
|
|
1877
|
+
sgc?.sendStreamKeepAlive(message)
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
fun requestWifiScan(scanId: String? = null) {
|
|
1881
|
+
Bridge.log("MAN: Requesting wifi scan")
|
|
1882
|
+
DeviceStore.apply("bluetooth", "wifiScanResults", emptyList<Any>())
|
|
1883
|
+
sgc?.requestWifiScan(scanId)
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
|
|
1887
|
+
Bridge.log("MAN: Sending incidentId to glasses for log upload: $incidentId")
|
|
1888
|
+
sgc?.sendIncidentId(incidentId, apiBaseUrl)
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
fun sendWifiCredentials(ssid: String, password: String) {
|
|
1892
|
+
Bridge.log("MAN: Sending wifi credentials: $ssid")
|
|
1893
|
+
sgc?.sendWifiCredentials(ssid, password)
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
fun forgetWifiNetwork(ssid: String) {
|
|
1897
|
+
Bridge.log("MAN: Forgetting wifi network: $ssid")
|
|
1898
|
+
sgc?.forgetWifiNetwork(ssid)
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
fun setHotspotState(enabled: Boolean) {
|
|
1902
|
+
Bridge.log("MAN: Setting glasses hotspot state: $enabled")
|
|
1903
|
+
sgc?.sendHotspotState(enabled)
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
fun setWifiAdbState(enabled: Boolean) {
|
|
1907
|
+
Bridge.log("MAN: Setting glasses Wi-Fi ADB state: $enabled")
|
|
1908
|
+
sgc?.sendWifiAdbState(enabled)
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
fun setSystemTime(timestampMs: Long) {
|
|
1912
|
+
Bridge.log("MAN: Setting glasses system time: $timestampMs")
|
|
1913
|
+
sgc?.sendSetSystemTime(timestampMs)
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
fun queryGalleryStatus() {
|
|
1917
|
+
Bridge.log("MAN: Querying gallery status from glasses")
|
|
1918
|
+
sgc?.queryGalleryStatus()
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Send OTA start command to glasses. Called when user approves an update (onboarding or
|
|
1923
|
+
* background mode). Triggers glasses to begin download and installation.
|
|
1924
|
+
*/
|
|
1925
|
+
fun sendOtaStart(otaVersionUrl: String? = null) {
|
|
1926
|
+
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1927
|
+
(sgc as? MentraLive)?.sendOtaStart(otaVersionUrl)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
fun sendOtaQueryStatus() {
|
|
1931
|
+
Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
|
|
1932
|
+
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
fun startAr99OtaFromFile(path: String): Boolean {
|
|
1936
|
+
val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
|
|
1937
|
+
return ar99.startOtaFromFile(path)
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
fun cancelAr99Ota() {
|
|
1941
|
+
(sgc as? Ar99)?.cancelAr99Ota()
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
fun sendAr99FactoryReset() {
|
|
1945
|
+
val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
|
|
1946
|
+
ar99.sendFactoryReset()
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
fun sendGalleryMode(requestId: String, enabled: Boolean) {
|
|
1950
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1951
|
+
live.sendGalleryMode(requestId, enabled)
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1955
|
+
sendButtonPhotoSettings(requestId, PhotoCaptureDefaults(PhotoSize.fromValue(size)))
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
fun sendButtonPhotoSettings(requestId: String, settings: PhotoCaptureDefaults) {
|
|
1959
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1960
|
+
live.sendButtonPhotoSettings(
|
|
1961
|
+
requestId,
|
|
1962
|
+
settings.size?.value,
|
|
1963
|
+
settings.mfnr,
|
|
1964
|
+
settings.zsl,
|
|
1965
|
+
settings.noiseReduction,
|
|
1966
|
+
settings.edgeEnhancement,
|
|
1967
|
+
settings.ispDigitalGain,
|
|
1968
|
+
settings.ispAnalogGain,
|
|
1969
|
+
settings.aeExposureDivisor,
|
|
1970
|
+
settings.isoCap,
|
|
1971
|
+
settings.compress,
|
|
1972
|
+
settings.sound,
|
|
1973
|
+
settings.resetCaptureTuning == true,
|
|
1974
|
+
)
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
fun sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) {
|
|
1978
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1979
|
+
live.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
fun sendButtonMaxRecordingTime(requestId: String, minutes: Int) {
|
|
1983
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1984
|
+
live.sendButtonMaxRecordingTime(requestId, minutes)
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
fun sendCameraFovSetting(requestId: String, fov: Int, roiPosition: Int) {
|
|
1988
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1989
|
+
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/** Sends the pre-lease FOV command used by ASG clients that do not send an acknowledgement. */
|
|
1993
|
+
fun sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) {
|
|
1994
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
1995
|
+
if (!glassesConnected) throw IllegalStateException("not_connected")
|
|
1996
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1997
|
+
live.sendCameraFovSetting(null, fov, roiPosition)
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/** Replays the persistent base FOV without waiting for an acknowledgement. */
|
|
2001
|
+
fun restoreLegacyCameraFovSetting() {
|
|
2002
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
2003
|
+
if (!glassesConnected) throw IllegalStateException("not_connected")
|
|
2004
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2005
|
+
live.sendCameraFovSetting()
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
fun sendCameraFovOverride(
|
|
2009
|
+
requestId: String,
|
|
2010
|
+
leaseId: String,
|
|
2011
|
+
fov: Int,
|
|
2012
|
+
roiPosition: Int,
|
|
2013
|
+
ttlMs: Int,
|
|
2014
|
+
) {
|
|
2015
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2016
|
+
live.sendCameraFovOverride(requestId, leaseId, fov, roiPosition, ttlMs)
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
fun releaseCameraFovOverride(requestId: String, leaseId: String) {
|
|
2020
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2021
|
+
live.releaseCameraFovOverride(requestId, leaseId)
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
fun sendCameraTuningConfig(requestId: String, anrOn: Boolean, gainOn: Boolean) {
|
|
2025
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2026
|
+
live.sendCameraTuningConfig(requestId, anrOn, gainOn)
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
fun warmUpCamera(
|
|
2030
|
+
requestId: String,
|
|
2031
|
+
size: PhotoSize,
|
|
2032
|
+
mode: PhotoMode = PhotoMode.PHOTO,
|
|
2033
|
+
exposureTimeNs: Long?,
|
|
2034
|
+
durationMs: Int,
|
|
2035
|
+
zsl: Boolean? = null,
|
|
2036
|
+
mfnr: Boolean? = null,
|
|
2037
|
+
) {
|
|
2038
|
+
// Fail fast like other camera commands so the SDK promise rejects immediately instead of
|
|
2039
|
+
// hanging until the request timeout with no camera_status.
|
|
2040
|
+
val live =
|
|
2041
|
+
sgc as? MentraLive
|
|
2042
|
+
?: throw BluetoothSdkException(
|
|
2043
|
+
"unsupported_device",
|
|
2044
|
+
"This command requires Mentra Live glasses.",
|
|
2045
|
+
)
|
|
2046
|
+
live.warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr)
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
fun stopCameraWarmUp(requestId: String) {
|
|
2050
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2051
|
+
live.stopCameraWarmUp(requestId)
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Read glasses media step volume (0—5) via K900 on Mentra Live only. Blocks until response,
|
|
2056
|
+
* error, or timeout (used from JS AsyncFunction on a worker thread).
|
|
2057
|
+
*/
|
|
2058
|
+
fun getGlassesMediaVolumeBlocking(): Map<String, Any> {
|
|
2059
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2060
|
+
val latch = CountDownLatch(1)
|
|
2061
|
+
var result: Map<String, Any>? = null
|
|
2062
|
+
var error: String? = null
|
|
2063
|
+
live.getGlassesMediaVolume(
|
|
2064
|
+
{ m ->
|
|
2065
|
+
result = m
|
|
2066
|
+
latch.countDown()
|
|
2067
|
+
},
|
|
2068
|
+
{ e ->
|
|
2069
|
+
error = e
|
|
2070
|
+
latch.countDown()
|
|
2071
|
+
}
|
|
2072
|
+
)
|
|
2073
|
+
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
2074
|
+
if (!completed) {
|
|
2075
|
+
throw IllegalStateException("glasses_volume_timeout")
|
|
2076
|
+
}
|
|
2077
|
+
error?.let { throw IllegalStateException(it) }
|
|
2078
|
+
return result ?: throw IllegalStateException("glasses_volume_empty")
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/** Set glasses media step volume (0—5) via K900 on Mentra Live only. */
|
|
2082
|
+
fun setGlassesMediaVolumeBlocking(level: Int): Map<String, Any> {
|
|
2083
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
2084
|
+
val latch = CountDownLatch(1)
|
|
2085
|
+
var result: Map<String, Any>? = null
|
|
2086
|
+
var error: String? = null
|
|
2087
|
+
live.setGlassesMediaVolume(
|
|
2088
|
+
level,
|
|
2089
|
+
{ m ->
|
|
2090
|
+
result = m
|
|
2091
|
+
latch.countDown()
|
|
2092
|
+
},
|
|
2093
|
+
{ e ->
|
|
2094
|
+
error = e
|
|
2095
|
+
latch.countDown()
|
|
2096
|
+
}
|
|
2097
|
+
)
|
|
2098
|
+
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
2099
|
+
if (!completed) {
|
|
2100
|
+
throw IllegalStateException("glasses_volume_timeout")
|
|
2101
|
+
}
|
|
2102
|
+
error?.let { throw IllegalStateException(it) }
|
|
2103
|
+
return result ?: throw IllegalStateException("glasses_volume_empty")
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* Request version info from glasses. Glasses will respond with version_info message containing
|
|
2108
|
+
* build number, firmware version, etc.
|
|
2109
|
+
*/
|
|
2110
|
+
fun requestVersionInfo() {
|
|
2111
|
+
Bridge.log("MAN: 📱 Requesting version info from glasses")
|
|
2112
|
+
sgc?.requestVersionInfo()
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
fun availableG2FirmwareArtifacts(): List<Map<String, Any>> {
|
|
2116
|
+
val g2 = sgc as? G2 ?: throw IllegalStateException("The connected device is not a G2.")
|
|
2117
|
+
return g2.availableFirmwareArtifacts()
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
suspend fun startG2FirmwareFlash(target: String): Map<String, Any> {
|
|
2121
|
+
val g2 = sgc as? G2 ?: throw IllegalStateException("The connected device is not a G2.")
|
|
2122
|
+
return g2.startFirmwareFlash(target)
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/** Send shutdown command to glasses. This will initiate a graceful shutdown of the device. */
|
|
2126
|
+
fun sendShutdown() {
|
|
2127
|
+
Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
|
|
2128
|
+
sgc?.sendShutdown()
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/** Send reboot command to glasses. This will initiate a reboot of the device. */
|
|
2132
|
+
fun sendReboot() {
|
|
2133
|
+
Bridge.log("MAN: 🔄 Sending reboot command to glasses")
|
|
2134
|
+
sgc?.sendReboot()
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
fun startVideoRecording(
|
|
2138
|
+
requestId: String,
|
|
2139
|
+
save: Boolean,
|
|
2140
|
+
sound: Boolean,
|
|
2141
|
+
width: Int = 0,
|
|
2142
|
+
height: Int = 0,
|
|
2143
|
+
fps: Int = 0,
|
|
2144
|
+
maxRecordingTimeMinutes: Int = 0,
|
|
2145
|
+
) {
|
|
2146
|
+
Bridge.log(
|
|
2147
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, sound=$sound, " +
|
|
2148
|
+
"resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
|
|
2149
|
+
)
|
|
2150
|
+
sgc?.startVideoRecording(
|
|
2151
|
+
requestId, save, sound, width, height, fps, maxRecordingTimeMinutes
|
|
2152
|
+
)
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
fun queryVideoRecordingStatus(requestId: String) {
|
|
2156
|
+
Bridge.log("MAN: Querying video recording status: requestId=$requestId")
|
|
2157
|
+
sgc?.queryVideoRecordingStatus(requestId)
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
2161
|
+
Bridge.log(
|
|
2162
|
+
"MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
|
|
2163
|
+
if (webhookUrl.isNullOrEmpty()) "none" else "set"
|
|
2164
|
+
)
|
|
2165
|
+
sgc?.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
fun setMicState() {
|
|
2169
|
+
val willSendPcm = shouldSendPcm || shouldSendLc3
|
|
2170
|
+
val willSendTranscript = shouldSendTranscript || localSttFallbackActive
|
|
2171
|
+
// Speaker enrollment records through whichever mic is currently
|
|
2172
|
+
// ranked active, so it holds the mic open like any other consumer.
|
|
2173
|
+
val nextEnabled = willSendPcm || willSendTranscript || speakerEnrollmentActive
|
|
2174
|
+
// Tell VAD when the mic is shutting down so it doesn't get stuck in
|
|
2175
|
+
// a stale "speaking" state and keep emitting vad_status=true after
|
|
2176
|
+
// audio stops flowing.
|
|
2177
|
+
if (micEnabled && !nextEnabled) {
|
|
2178
|
+
vadPolicy?.microphoneStateChanged(false)
|
|
2179
|
+
}
|
|
2180
|
+
micEnabled = nextEnabled
|
|
2181
|
+
vadBuffer.clear()
|
|
2182
|
+
updateMicState()
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
fun requestPhoto(request: PhotoRequest) {
|
|
2186
|
+
val exposureNs: Long? =
|
|
2187
|
+
request.exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
2188
|
+
when {
|
|
2189
|
+
v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
2190
|
+
else -> v.toLong()
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
val manualIso = if (exposureNs != null) request.iso?.takeIf { it > 0 } else null
|
|
2194
|
+
val routed =
|
|
2195
|
+
request.copy(
|
|
2196
|
+
exposureTimeNs = exposureNs?.toDouble(),
|
|
2197
|
+
iso = manualIso,
|
|
2198
|
+
)
|
|
2199
|
+
Bridge.log(
|
|
2200
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} size=${routed.size.value} mode=${routed.mode.value} compress=${routed.compress.value} save=${routed.save} sound=${routed.sound} exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} aeDivisor=${routed.aeExposureDivisor} isoCap=${routed.isoCap} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
2201
|
+
)
|
|
2202
|
+
val activeSgc = sgc
|
|
2203
|
+
if (activeSgc == null) {
|
|
2204
|
+
Bridge.log(
|
|
2205
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
|
|
2206
|
+
)
|
|
2207
|
+
return
|
|
2208
|
+
}
|
|
2209
|
+
activeSgc.requestPhoto(routed)
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
fun rgbLedControl(
|
|
2213
|
+
requestId: String,
|
|
2214
|
+
packageName: String?,
|
|
2215
|
+
action: String,
|
|
2216
|
+
color: String?,
|
|
2217
|
+
onDurationMs: Int,
|
|
2218
|
+
offDurationMs: Int,
|
|
2219
|
+
count: Int
|
|
2220
|
+
) {
|
|
2221
|
+
Bridge.log("MAN: RGB LED control: action=$action, color=$color, requestId=$requestId")
|
|
2222
|
+
sgc?.sendRgbLedControl(requestId, packageName, action, color, onDurationMs, offDurationMs, count)
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
fun connectDefault() {
|
|
2226
|
+
if (defaultWearable.isEmpty()) {
|
|
2227
|
+
Bridge.log("MAN: No default wearable, returning")
|
|
2228
|
+
return
|
|
2229
|
+
}
|
|
2230
|
+
val reconnectTarget =
|
|
2231
|
+
if (defaultWearable.contains(DeviceTypes.AR99) && deviceAddress.isNotBlank()) {
|
|
2232
|
+
deviceAddress
|
|
2233
|
+
} else {
|
|
2234
|
+
deviceName
|
|
2235
|
+
}
|
|
2236
|
+
if (reconnectTarget.isEmpty()) {
|
|
2237
|
+
Bridge.log("MAN: No reconnect target, returning")
|
|
2238
|
+
return
|
|
2239
|
+
}
|
|
2240
|
+
if (!hasBluetoothPermissions()) {
|
|
2241
|
+
// Auto-reconnect paths (boot, BT toggle, app launch before perm flow)
|
|
2242
|
+
// may fire before user has granted runtime Bluetooth permissions on Android 12+.
|
|
2243
|
+
// Bail out instead of crashing with SecurityException on startScan / getRemoteName.
|
|
2244
|
+
Bridge.log("MAN: connectDefault skipped — bluetooth runtime permissions not granted")
|
|
2245
|
+
return
|
|
2246
|
+
}
|
|
2247
|
+
initSGC(defaultWearable)
|
|
2248
|
+
val live = sgc as? MentraLive
|
|
2249
|
+
if (live?.isPairingYieldActive() == true) {
|
|
2250
|
+
Bridge.log("MAN: connectDefault skipped — Mentra Live pairing yield active")
|
|
2251
|
+
return
|
|
2252
|
+
}
|
|
2253
|
+
searching = true
|
|
2254
|
+
sgc?.connectById(reconnectTarget)
|
|
2255
|
+
connectDefaultController()
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
fun connectDefaultController() {
|
|
2259
|
+
if (defaultController.isEmpty()) {
|
|
2260
|
+
Bridge.log("MAN: No default controller, returning")
|
|
2261
|
+
return
|
|
2262
|
+
}
|
|
2263
|
+
if (controllerDeviceName.isEmpty()) {
|
|
2264
|
+
Bridge.log("MAN: No controller device name, returning")
|
|
2265
|
+
return
|
|
2266
|
+
}
|
|
2267
|
+
if (!hasBluetoothPermissions()) {
|
|
2268
|
+
Bridge.log("MAN: connectDefaultController skipped — bluetooth runtime permissions not granted")
|
|
2269
|
+
return
|
|
2270
|
+
}
|
|
2271
|
+
initController(defaultController)
|
|
2272
|
+
searchingController = true
|
|
2273
|
+
controller?.connectById(controllerDeviceName)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
fun connectByName(dName: String) {
|
|
2277
|
+
Bridge.log("MAN: Connecting to wearable: $dName")
|
|
2278
|
+
|
|
2279
|
+
var name = dName
|
|
2280
|
+
|
|
2281
|
+
// use stored device name if available:
|
|
2282
|
+
if (dName.isEmpty() && !deviceName.isEmpty()) {
|
|
2283
|
+
name = deviceName
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (pendingWearable.isEmpty() && defaultWearable.isEmpty()) {
|
|
2287
|
+
Bridge.log("MAN: No pending or default wearable, returning")
|
|
2288
|
+
return
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (pendingWearable.isEmpty() && !defaultWearable.isEmpty()) {
|
|
2292
|
+
Bridge.log("MAN: No pending wearable, using default wearable")
|
|
2293
|
+
pendingWearable = defaultWearable
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// if the pending wearable is a controller, don't disconnect the glasses;
|
|
2297
|
+
// route through the controller manager instead
|
|
2298
|
+
if (ControllerTypes.ALL.contains(pendingWearable)) {
|
|
2299
|
+
controller?.disconnect()
|
|
2300
|
+
initController(pendingWearable)
|
|
2301
|
+
controller?.connectById(name)
|
|
2302
|
+
return
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
disconnect(clearPendingIdentity = false)
|
|
2306
|
+
Thread.sleep(100)
|
|
2307
|
+
searching = true
|
|
2308
|
+
pendingDeviceName = name
|
|
2309
|
+
|
|
2310
|
+
initSGC(pendingWearable)
|
|
2311
|
+
sgc?.connectById(name)
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
fun connectDevice(deviceModel: String, deviceName: String) {
|
|
2315
|
+
Bridge.log("MAN: Connecting to device: $deviceModel $deviceName")
|
|
2316
|
+
if (DeviceTypes.ALL.contains(deviceModel)) {
|
|
2317
|
+
pendingWearable = deviceModel
|
|
2318
|
+
initSGC(pendingWearable)
|
|
2319
|
+
sgc?.connectById(deviceName)
|
|
2320
|
+
return
|
|
2321
|
+
}
|
|
2322
|
+
if (ControllerTypes.ALL.contains(deviceModel)) {
|
|
2323
|
+
pendingWearable = deviceModel
|
|
2324
|
+
initController(deviceModel)
|
|
2325
|
+
controller?.connectById(deviceName)
|
|
2326
|
+
return
|
|
2327
|
+
}
|
|
2328
|
+
Bridge.log("MAN: No compatible device model, returning")
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
fun connectSimulated() {
|
|
2332
|
+
defaultWearable = DeviceTypes.SIMULATED
|
|
2333
|
+
deviceName = DeviceTypes.SIMULATED
|
|
2334
|
+
initSGC(defaultWearable)
|
|
2335
|
+
handleDeviceReady()
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
fun disconnect(clearPendingIdentity: Boolean = true) {
|
|
2339
|
+
sgc?.clearDisplay()
|
|
2340
|
+
sgc?.disconnect()
|
|
2341
|
+
sgc = null // Clear the SGC reference after disconnect
|
|
2342
|
+
resetSystemTimeSync()
|
|
2343
|
+
resetMicHealth()
|
|
2344
|
+
searching = false
|
|
2345
|
+
micEnabled = false
|
|
2346
|
+
updateMicState()
|
|
2347
|
+
shouldSendBootingMessage = true // Reset for next first connect
|
|
2348
|
+
// clear glasses properties:
|
|
2349
|
+
DeviceStore.apply("glasses", "deviceModel", "")
|
|
2350
|
+
// Device identifiers are session-bound. Clear them on every disconnect so a
|
|
2351
|
+
// previously connected pair can never be reported for the next connection.
|
|
2352
|
+
DeviceStore.apply("glasses", "serialNumber", "")
|
|
2353
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
2354
|
+
DeviceStore.apply("glasses", "wifiMacAddress", "")
|
|
2355
|
+
DeviceStore.apply("glasses", "leftMacAddress", "")
|
|
2356
|
+
DeviceStore.apply("glasses", "rightMacAddress", "")
|
|
2357
|
+
DeviceStore.apply("glasses", "macAddress", "")
|
|
2358
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2359
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
2360
|
+
DeviceStore.apply(
|
|
2361
|
+
"glasses",
|
|
2362
|
+
"voiceActivityDetectionEnabled",
|
|
2363
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
2364
|
+
)
|
|
2365
|
+
// disconnect the controller as well:
|
|
2366
|
+
searchingController = false
|
|
2367
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2368
|
+
controller?.disconnect()
|
|
2369
|
+
controller = null
|
|
2370
|
+
if (clearPendingIdentity) {
|
|
2371
|
+
clearPendingConnection()
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
private fun clearPendingConnection() {
|
|
2376
|
+
pendingDeviceName = ""
|
|
2377
|
+
pendingDeviceAddress = ""
|
|
2378
|
+
pendingWearable = ""
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
fun disconnectController() {
|
|
2382
|
+
searchingController = false
|
|
2383
|
+
// disconnect the controller from the glasses if applicable:
|
|
2384
|
+
sgc?.disconnectController()
|
|
2385
|
+
controller?.disconnect()
|
|
2386
|
+
controller = null
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
fun forget() {
|
|
2390
|
+
Bridge.log("MAN: Forgetting smart glasses")
|
|
2391
|
+
|
|
2392
|
+
val live = sgc as? MentraLive
|
|
2393
|
+
if (live != null) {
|
|
2394
|
+
// MentraLive.forget() disconnects GATT then removeBond. Do not refuse
|
|
2395
|
+
// during CTKD bonding — Unpair is an explicit user request to drop the pair.
|
|
2396
|
+
live.forget()
|
|
2397
|
+
// MentraLive.forget() already destroyed the SGC. Clear the manager reference and
|
|
2398
|
+
// session state without calling disconnect() again (that would hit a dead instance
|
|
2399
|
+
// and leave a destroyed MentraLive retained for the next scan).
|
|
2400
|
+
sgc = null
|
|
2401
|
+
resetSystemTimeSync()
|
|
2402
|
+
searching = false
|
|
2403
|
+
micEnabled = false
|
|
2404
|
+
updateMicState()
|
|
2405
|
+
shouldSendBootingMessage = true
|
|
2406
|
+
DeviceStore.apply("glasses", "deviceModel", "")
|
|
2407
|
+
DeviceStore.apply("glasses", "serialNumber", "")
|
|
2408
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
2409
|
+
DeviceStore.apply("glasses", "leftMacAddress", "")
|
|
2410
|
+
DeviceStore.apply("glasses", "rightMacAddress", "")
|
|
2411
|
+
DeviceStore.apply("glasses", "macAddress", "")
|
|
2412
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2413
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
2414
|
+
DeviceStore.apply(
|
|
2415
|
+
"glasses",
|
|
2416
|
+
"voiceActivityDetectionEnabled",
|
|
2417
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
2418
|
+
)
|
|
2419
|
+
searchingController = false
|
|
2420
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2421
|
+
controller?.disconnect()
|
|
2422
|
+
controller = null
|
|
2423
|
+
} else {
|
|
2424
|
+
// Typical abandonAttempt path: disconnect() already destroyed SGC and nulled
|
|
2425
|
+
// this.sgc, so MentraLive.forget() never ran and Classic bonds stayed up —
|
|
2426
|
+
// glasses keep IBRT ACL and stop BLE advertising. Only tear Live bonds when
|
|
2427
|
+
// the selected/pending wearable is Mentra Live — forgetting G1/G2 must not
|
|
2428
|
+
// unpair an unrelated Live unit.
|
|
2429
|
+
val liveTarget =
|
|
2430
|
+
defaultWearable == DeviceTypes.LIVE || pendingWearable == DeviceTypes.LIVE
|
|
2431
|
+
if (liveTarget) {
|
|
2432
|
+
val extraAddress =
|
|
2433
|
+
when {
|
|
2434
|
+
defaultWearable == DeviceTypes.LIVE ->
|
|
2435
|
+
deviceAddress.takeIf { it.isNotEmpty() }
|
|
2436
|
+
pendingWearable == DeviceTypes.LIVE ->
|
|
2437
|
+
pendingDeviceAddress.takeIf { it.isNotEmpty() }
|
|
2438
|
+
else -> null
|
|
2439
|
+
}
|
|
2440
|
+
MentraLive.unbondBondedMentraLiveDevices(
|
|
2441
|
+
Bridge.getContext(),
|
|
2442
|
+
extraAddress,
|
|
2443
|
+
)
|
|
2444
|
+
}
|
|
2445
|
+
sgc?.forget()
|
|
2446
|
+
disconnect()
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// Clear state
|
|
2450
|
+
defaultWearable = ""
|
|
2451
|
+
deviceName = ""
|
|
2452
|
+
deviceAddress = ""
|
|
2453
|
+
clearPendingConnection()
|
|
2454
|
+
Bridge.saveSetting("default_wearable", "")
|
|
2455
|
+
Bridge.saveSetting("device_name", "")
|
|
2456
|
+
Bridge.saveSetting("device_address", "")
|
|
2457
|
+
Bridge.saveSetting("project_name", "")
|
|
2458
|
+
Bridge.saveSetting("pending_wearable", "")
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
fun forgetController() {
|
|
2462
|
+
Bridge.log("MAN: Forgetting controller")
|
|
2463
|
+
controller?.forget()
|
|
2464
|
+
disconnectController()
|
|
2465
|
+
// Clear state
|
|
2466
|
+
defaultController = ""
|
|
2467
|
+
controllerDeviceName = ""
|
|
2468
|
+
Bridge.saveSetting("controller_device_name", "")
|
|
2469
|
+
Bridge.saveSetting("default_controller", "")
|
|
2470
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
fun findCompatibleDevices(deviceModel: String) {
|
|
2474
|
+
Bridge.log("MAN: Searching for compatible device names for: $deviceModel")
|
|
2475
|
+
|
|
2476
|
+
// reset the search results:
|
|
2477
|
+
searchResults = emptyList()
|
|
2478
|
+
|
|
2479
|
+
if (DeviceTypes.ALL.contains(deviceModel)) {
|
|
2480
|
+
pendingWearable = deviceModel
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
if (ControllerTypes.ALL.contains(deviceModel)) {
|
|
2484
|
+
pendingWearable = deviceModel
|
|
2485
|
+
initController(deviceModel)
|
|
2486
|
+
controller?.findCompatibleDevices()
|
|
2487
|
+
return
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
initSGC(pendingWearable)
|
|
2491
|
+
Bridge.log("MAN: sgc initialized, calling findCompatibleDevices")
|
|
2492
|
+
sgc?.findCompatibleDevices()
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
fun stopScan() {
|
|
2496
|
+
controller?.stopScan()
|
|
2497
|
+
sgc?.stopScan()
|
|
2498
|
+
DeviceStore.apply("bluetooth", "searching", false)
|
|
2499
|
+
DeviceStore.apply("bluetooth", "searchingController", false)
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// MARK: Cleanup
|
|
2503
|
+
fun cleanup() {
|
|
2504
|
+
stopBluetoothStateMonitoring()
|
|
2505
|
+
|
|
2506
|
+
micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
2507
|
+
micReinitRunnable = null
|
|
2508
|
+
systemMicAvailabilityRecheckRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
2509
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
2510
|
+
|
|
2511
|
+
// Clean up transcriber resources
|
|
2512
|
+
transcriber?.shutdown()
|
|
2513
|
+
transcriber = null
|
|
2514
|
+
|
|
2515
|
+
// Clean up LC3 encoder/decoder (synchronized to prevent use-after-free
|
|
2516
|
+
// if the recording thread is mid-encode/decode)
|
|
2517
|
+
synchronized(lc3Lock) {
|
|
2518
|
+
if (lc3EncoderPtr != 0L) {
|
|
2519
|
+
Lc3Cpp.freeEncoder(lc3EncoderPtr)
|
|
2520
|
+
lc3EncoderPtr = 0
|
|
2521
|
+
}
|
|
2522
|
+
if (lc3DecoderPtr != 0L) {
|
|
2523
|
+
Lc3Cpp.freeDecoder(lc3DecoderPtr)
|
|
2524
|
+
lc3DecoderPtr = 0
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|