@mentra/bluetooth-sdk 0.1.0
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 +278 -0
- package/android/build.gradle +167 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +109 -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/mentra/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 +1 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/app.plugin.js +1 -0
- package/build/BluetoothSdk.types.d.ts +498 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +23 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/BluetoothSdkModule.d.ts +71 -0
- package/build/BluetoothSdkModule.d.ts.map +1 -0
- package/build/BluetoothSdkModule.js +106 -0
- package/build/BluetoothSdkModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BluetoothSdkModule.swift +665 -0
- package/ios/MentraBluetoothSDK.podspec +77 -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/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 +1659 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
- package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
- package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
- package/ios/Packages/VAD/Data/Configuration.swift +68 -0
- package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
- package/ios/Packages/VAD/Data/Record.swift +13 -0
- package/ios/Packages/VAD/Data/Result.swift +22 -0
- package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
- package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/Source/Bridge.swift +353 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +1478 -0
- package/ios/Source/DeviceStore.swift +295 -0
- package/ios/Source/MentraBluetoothSDK.swift +2439 -0
- package/ios/Source/ObservableStore.swift +88 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/controllers/ControllerManager.swift +187 -0
- package/ios/Source/controllers/R1.swift +827 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Frame.swift +651 -0
- package/ios/Source/sgcs/G1.swift +2505 -0
- package/ios/Source/sgcs/G2.swift +3730 -0
- package/ios/Source/sgcs/Mach1.swift +543 -0
- package/ios/Source/sgcs/MentraLive.swift +4708 -0
- package/ios/Source/sgcs/MentraNex.swift +2479 -0
- package/ios/Source/sgcs/SGCManager.swift +243 -0
- package/ios/Source/sgcs/Simulated.swift +270 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
- package/ios/Source/stt/STTTools.swift +154 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/Constants.swift +74 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2067 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunker.swift +164 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
- package/ios/Source/utils/TarBz2Extractor.swift +206 -0
- package/package.json +78 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +4 -0
- package/plugin/build/withAndroid.js +80 -0
- package/plugin/build/withIos.d.ts +4 -0
- package/plugin/build/withIos.js +64 -0
- package/src/BluetoothSdk.types.ts +581 -0
- package/src/BluetoothSdkModule.ts +259 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,1616 @@
|
|
|
1
|
+
package com.mentra.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.mentra.bluetoothsdk.controllers.ControllerManager
|
|
14
|
+
import com.mentra.bluetoothsdk.controllers.R1
|
|
15
|
+
import com.mentra.bluetoothsdk.services.ForegroundService
|
|
16
|
+
import com.mentra.bluetoothsdk.services.PhoneMic
|
|
17
|
+
import com.mentra.bluetoothsdk.sgcs.G1
|
|
18
|
+
import com.mentra.bluetoothsdk.sgcs.G2
|
|
19
|
+
import com.mentra.bluetoothsdk.sgcs.Mach1
|
|
20
|
+
import com.mentra.bluetoothsdk.sgcs.MentraLive
|
|
21
|
+
import com.mentra.bluetoothsdk.sgcs.MentraNex
|
|
22
|
+
import com.mentra.bluetoothsdk.sgcs.SGCManager
|
|
23
|
+
import com.mentra.bluetoothsdk.sgcs.Simulated
|
|
24
|
+
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
25
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
26
|
+
import com.mentra.bluetoothsdk.utils.MicMap
|
|
27
|
+
import com.mentra.bluetoothsdk.utils.MicTypes
|
|
28
|
+
import com.mentra.lc3Lib.Lc3Cpp
|
|
29
|
+
import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
|
|
30
|
+
import java.text.SimpleDateFormat
|
|
31
|
+
import java.util.*
|
|
32
|
+
import java.util.concurrent.CountDownLatch
|
|
33
|
+
import java.util.concurrent.ExecutorService
|
|
34
|
+
import java.util.concurrent.Executors
|
|
35
|
+
import java.util.concurrent.TimeUnit
|
|
36
|
+
import kotlin.jvm.JvmStatic
|
|
37
|
+
|
|
38
|
+
class DeviceManager {
|
|
39
|
+
companion object {
|
|
40
|
+
|
|
41
|
+
@Volatile private var _instance: DeviceManager? = null
|
|
42
|
+
|
|
43
|
+
@JvmStatic
|
|
44
|
+
fun getInstance(): DeviceManager {
|
|
45
|
+
return _instance
|
|
46
|
+
?: synchronized(this) { _instance ?: DeviceManager().also { _instance = it } }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - Unique (Android)
|
|
51
|
+
private var serviceStarted = false
|
|
52
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
53
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
54
|
+
private var sendStateWorkItem: Runnable? = null
|
|
55
|
+
private var phoneMic: PhoneMic? = null
|
|
56
|
+
|
|
57
|
+
// Track last known permissions
|
|
58
|
+
private var lastHadBluetoothPermission = false
|
|
59
|
+
private var lastHadMicrophonePermission = false
|
|
60
|
+
private var permissionReceiver: BroadcastReceiver? = null
|
|
61
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
62
|
+
private var permissionCheckRunnable: Runnable? = null
|
|
63
|
+
|
|
64
|
+
// Bluetooth adapter state monitoring (detects BT toggle from control center)
|
|
65
|
+
private var bluetoothStateReceiver: BroadcastReceiver? = null
|
|
66
|
+
private var isBluetoothStateReceiverRegistered = false
|
|
67
|
+
|
|
68
|
+
// MARK: - End Unique
|
|
69
|
+
|
|
70
|
+
// MARK: - Properties
|
|
71
|
+
var sgc: SGCManager? = null
|
|
72
|
+
var controller: ControllerManager? = null
|
|
73
|
+
|
|
74
|
+
// settings:
|
|
75
|
+
private var defaultWearable: String
|
|
76
|
+
get() = DeviceStore.store.get("bluetooth", "default_wearable") as? String ?: ""
|
|
77
|
+
set(value) = DeviceStore.apply("bluetooth", "default_wearable", value)
|
|
78
|
+
|
|
79
|
+
private var pendingWearable: String
|
|
80
|
+
get() = DeviceStore.store.get("bluetooth", "pending_wearable") as? String ?: ""
|
|
81
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_wearable", value)
|
|
82
|
+
|
|
83
|
+
public var deviceName: String
|
|
84
|
+
get() = DeviceStore.store.get("bluetooth", "device_name") as? String ?: ""
|
|
85
|
+
set(value) = DeviceStore.apply("bluetooth", "device_name", value)
|
|
86
|
+
|
|
87
|
+
public var deviceAddress: String
|
|
88
|
+
get() = DeviceStore.store.get("bluetooth", "device_address") as? String ?: ""
|
|
89
|
+
set(value) = DeviceStore.apply("bluetooth", "device_address", value)
|
|
90
|
+
|
|
91
|
+
private var defaultController: String
|
|
92
|
+
get() = DeviceStore.store.get("bluetooth", "default_controller") as? String ?: ""
|
|
93
|
+
set(value) = DeviceStore.apply("bluetooth", "default_controller", value)
|
|
94
|
+
|
|
95
|
+
private var pendingController: String
|
|
96
|
+
get() = DeviceStore.store.get("bluetooth", "pending_controller") as? String ?: ""
|
|
97
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_controller", value)
|
|
98
|
+
|
|
99
|
+
private var controllerDeviceName: String
|
|
100
|
+
get() = DeviceStore.store.get("bluetooth", "controller_device_name") as? String ?: ""
|
|
101
|
+
set(value) = DeviceStore.apply("bluetooth", "controller_device_name", value)
|
|
102
|
+
|
|
103
|
+
private var searchingController: Boolean
|
|
104
|
+
get() = DeviceStore.store.get("bluetooth", "searchingController") as? Boolean ?: false
|
|
105
|
+
set(value) = DeviceStore.apply("bluetooth", "searchingController", value)
|
|
106
|
+
|
|
107
|
+
private var screenDisabled: Boolean
|
|
108
|
+
get() = DeviceStore.store.get("bluetooth", "screen_disabled") as? Boolean ?: false
|
|
109
|
+
set(value) = DeviceStore.apply("bluetooth", "screen_disabled", value)
|
|
110
|
+
|
|
111
|
+
private var preferredMic: String
|
|
112
|
+
get() = DeviceStore.store.get("bluetooth", "preferred_mic") as? String ?: "auto"
|
|
113
|
+
set(value) = DeviceStore.apply("bluetooth", "preferred_mic", value)
|
|
114
|
+
|
|
115
|
+
private var autoBrightness: Boolean
|
|
116
|
+
get() = DeviceStore.store.get("bluetooth", "auto_brightness") as? Boolean ?: true
|
|
117
|
+
set(value) = DeviceStore.apply("bluetooth", "auto_brightness", value)
|
|
118
|
+
|
|
119
|
+
private var brightness: Int
|
|
120
|
+
get() = DeviceStore.store.get("bluetooth", "brightness") as? Int ?: 50
|
|
121
|
+
set(value) = DeviceStore.apply("bluetooth", "brightness", value)
|
|
122
|
+
|
|
123
|
+
private var headUpAngle: Int
|
|
124
|
+
get() = DeviceStore.store.get("bluetooth", "head_up_angle") as? Int ?: 30
|
|
125
|
+
set(value) = DeviceStore.apply("bluetooth", "head_up_angle", value)
|
|
126
|
+
|
|
127
|
+
private var sensingEnabled: Boolean
|
|
128
|
+
get() = DeviceStore.store.get("bluetooth", "sensing_enabled") as? Boolean ?: true
|
|
129
|
+
set(value) = DeviceStore.apply("bluetooth", "sensing_enabled", value)
|
|
130
|
+
|
|
131
|
+
public var powerSavingMode: Boolean
|
|
132
|
+
get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
|
|
133
|
+
set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
|
|
134
|
+
|
|
135
|
+
private var bypassVad: Boolean
|
|
136
|
+
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: true
|
|
137
|
+
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
138
|
+
|
|
139
|
+
private var offlineCaptionsRunning: Boolean
|
|
140
|
+
get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
|
|
141
|
+
set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
|
|
142
|
+
|
|
143
|
+
private var localSttFallbackActive: Boolean
|
|
144
|
+
get() = DeviceStore.store.get("bluetooth", "local_stt_fallback_active") as? Boolean ?: false
|
|
145
|
+
set(value) = DeviceStore.apply("bluetooth", "local_stt_fallback_active", value)
|
|
146
|
+
|
|
147
|
+
private var shouldSendPcm: Boolean
|
|
148
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_pcm") as? Boolean ?: false
|
|
149
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_pcm", value)
|
|
150
|
+
|
|
151
|
+
private var shouldSendLc3: Boolean
|
|
152
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_lc3") as? Boolean ?: false
|
|
153
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_lc3", value)
|
|
154
|
+
|
|
155
|
+
private var shouldSendTranscript: Boolean
|
|
156
|
+
get() = DeviceStore.store.get("bluetooth", "should_send_transcript") as? Boolean ?: false
|
|
157
|
+
set(value) = DeviceStore.apply("bluetooth", "should_send_transcript", value)
|
|
158
|
+
|
|
159
|
+
private var contextualDashboard: Boolean
|
|
160
|
+
get() = DeviceStore.store.get("bluetooth", "contextual_dashboard") as? Boolean ?: true
|
|
161
|
+
set(value) = DeviceStore.apply("bluetooth", "contextual_dashboard", value)
|
|
162
|
+
|
|
163
|
+
private var dashboardHeight: Int
|
|
164
|
+
get() = (DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)?.toInt() ?: 4
|
|
165
|
+
set(value) = DeviceStore.apply("bluetooth", "dashboard_height", value)
|
|
166
|
+
|
|
167
|
+
private var dashboardDepth: Int
|
|
168
|
+
get() = (DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)?.toInt() ?: 2
|
|
169
|
+
set(value) = DeviceStore.apply("bluetooth", "dashboard_depth", value)
|
|
170
|
+
|
|
171
|
+
private var galleryMode: Boolean
|
|
172
|
+
get() = DeviceStore.store.get("bluetooth", "gallery_mode") as? Boolean ?: true
|
|
173
|
+
set(value) = DeviceStore.apply("bluetooth", "gallery_mode", value)
|
|
174
|
+
|
|
175
|
+
// state:
|
|
176
|
+
private var searching: Boolean
|
|
177
|
+
get() = DeviceStore.store.get("bluetooth", "searching") as? Boolean ?: false
|
|
178
|
+
set(value) = DeviceStore.apply("bluetooth", "searching", value)
|
|
179
|
+
|
|
180
|
+
private var glassesBtcConnected: Boolean
|
|
181
|
+
get() = DeviceStore.store.get("glasses", "btcConnected") as? Boolean ?: false
|
|
182
|
+
set(value) = DeviceStore.apply("glasses", "btcConnected", value)
|
|
183
|
+
|
|
184
|
+
public var micRanking: MutableList<String>
|
|
185
|
+
get() =
|
|
186
|
+
(DeviceStore.store.get("bluetooth", "micRanking") as? List<*>)
|
|
187
|
+
?.mapNotNull { it as? String }
|
|
188
|
+
?.toMutableList()
|
|
189
|
+
?: MicMap.map["auto"]?.toMutableList() ?: mutableListOf()
|
|
190
|
+
set(value) = DeviceStore.apply("bluetooth", "micRanking", value)
|
|
191
|
+
|
|
192
|
+
private var shouldSendBootingMessage: Boolean
|
|
193
|
+
get() = DeviceStore.store.get("bluetooth", "shouldSendBootingMessage") as? Boolean ?: true
|
|
194
|
+
set(value) = DeviceStore.apply("bluetooth", "shouldSendBootingMessage", value)
|
|
195
|
+
|
|
196
|
+
// Guard against duplicate ready callbacks firing back-to-back.
|
|
197
|
+
private var lastReadyHandledAtMs: Long = 0L
|
|
198
|
+
private var lastReadyHandledKey: String = ""
|
|
199
|
+
|
|
200
|
+
private var systemMicUnavailable: Boolean
|
|
201
|
+
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
202
|
+
set(value) = DeviceStore.apply("bluetooth", "systemMicUnavailable", value)
|
|
203
|
+
|
|
204
|
+
public var headUp: Boolean
|
|
205
|
+
get() = DeviceStore.store.get("glasses", "headUp") as? Boolean ?: false
|
|
206
|
+
set(value) = DeviceStore.apply("glasses", "headUp", value)
|
|
207
|
+
|
|
208
|
+
private var micEnabled: Boolean
|
|
209
|
+
get() = DeviceStore.store.get("bluetooth", "micEnabled") as? Boolean ?: false
|
|
210
|
+
set(value) = DeviceStore.apply("bluetooth", "micEnabled", value)
|
|
211
|
+
|
|
212
|
+
private var currentMic: String
|
|
213
|
+
get() = DeviceStore.store.get("bluetooth", "currentMic") as? String ?: ""
|
|
214
|
+
set(value) = DeviceStore.apply("bluetooth", "currentMic", value)
|
|
215
|
+
|
|
216
|
+
private var searchResults: List<Any>
|
|
217
|
+
get() = DeviceStore.store.get("bluetooth", "searchResults") as? List<Any> ?: emptyList()
|
|
218
|
+
set(value) = DeviceStore.apply("bluetooth", "searchResults", value)
|
|
219
|
+
|
|
220
|
+
private var wifiScanResults: List<Any>
|
|
221
|
+
get() = DeviceStore.store.get("bluetooth", "wifiScanResults") as? List<Any> ?: emptyList()
|
|
222
|
+
set(value) = DeviceStore.apply("bluetooth", "wifiScanResults", value)
|
|
223
|
+
|
|
224
|
+
private var lastLog: MutableList<String>
|
|
225
|
+
get() = DeviceStore.store.get("bluetooth", "lastLog") as? MutableList<String> ?: mutableListOf()
|
|
226
|
+
set(value) = DeviceStore.apply("bluetooth", "lastLog", value)
|
|
227
|
+
|
|
228
|
+
// LC3 Audio Encoding
|
|
229
|
+
// Audio output format enum
|
|
230
|
+
enum class AudioOutputFormat {
|
|
231
|
+
LC3,
|
|
232
|
+
PCM
|
|
233
|
+
}
|
|
234
|
+
// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
|
|
235
|
+
// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
|
|
236
|
+
private var lc3EncoderPtr: Long = 0
|
|
237
|
+
private var lc3DecoderPtr: Long = 0
|
|
238
|
+
private val lc3Lock = Any()
|
|
239
|
+
// Audio output format - defaults to LC3 for bandwidth savings
|
|
240
|
+
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
241
|
+
private var lastLc3Event: Long? = null
|
|
242
|
+
private var micReinitRunnable: Runnable? = null
|
|
243
|
+
|
|
244
|
+
// VAD
|
|
245
|
+
private val vadBuffer = mutableListOf<ByteArray>()
|
|
246
|
+
private var isSpeaking = false
|
|
247
|
+
|
|
248
|
+
// STT
|
|
249
|
+
private var transcriber: SherpaOnnxTranscriber? = null
|
|
250
|
+
|
|
251
|
+
// View states
|
|
252
|
+
private val viewStates = mutableListOf<ViewState>()
|
|
253
|
+
|
|
254
|
+
init {
|
|
255
|
+
Bridge.log("DeviceManager: init()")
|
|
256
|
+
initializeViewStates()
|
|
257
|
+
startForegroundService()
|
|
258
|
+
// setupPermissionMonitoring()
|
|
259
|
+
setupBluetoothStateMonitoring()
|
|
260
|
+
phoneMic = PhoneMic.getInstance()
|
|
261
|
+
// Initialize local STT transcriber
|
|
262
|
+
try {
|
|
263
|
+
val context = Bridge.getContext()
|
|
264
|
+
transcriber = SherpaOnnxTranscriber(context)
|
|
265
|
+
transcriber?.setTranscriptListener(
|
|
266
|
+
object : SherpaOnnxTranscriber.TranscriptListener {
|
|
267
|
+
override fun onPartialResult(text: String, language: String) {
|
|
268
|
+
Bridge.log("STT: Partial result: $text")
|
|
269
|
+
Bridge.sendLocalTranscription(text, false, language)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
override fun onFinalResult(text: String, language: String) {
|
|
273
|
+
Bridge.log("STT: Final result: $text")
|
|
274
|
+
Bridge.sendLocalTranscription(text, true, language)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
transcriber?.initialize()
|
|
279
|
+
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
280
|
+
} catch (e: Exception) {
|
|
281
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
282
|
+
transcriber = null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Initialize LC3 encoder/decoder for unified audio encoding
|
|
286
|
+
try {
|
|
287
|
+
Lc3Cpp.init()
|
|
288
|
+
lc3EncoderPtr = Lc3Cpp.initEncoder()
|
|
289
|
+
lc3DecoderPtr = Lc3Cpp.initDecoder()
|
|
290
|
+
Bridge.log("LC3 encoder/decoder initialized successfully")
|
|
291
|
+
} catch (e: Exception) {
|
|
292
|
+
Bridge.log("Failed to initialize LC3 encoder/decoder: ${e.message}")
|
|
293
|
+
lc3EncoderPtr = 0
|
|
294
|
+
lc3DecoderPtr = 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Mic reinit check every 10 seconds
|
|
298
|
+
val micReinitR =
|
|
299
|
+
object : Runnable {
|
|
300
|
+
override fun run() {
|
|
301
|
+
checkAndReinitGlassesMic()
|
|
302
|
+
mainHandler.postDelayed(this, 10_000)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
micReinitRunnable = micReinitR
|
|
306
|
+
mainHandler.postDelayed(micReinitR, 10_000)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun checkAndReinitGlassesMic() {
|
|
310
|
+
// if the glasses mic is marked as enabled (and the glasses are connected), but our last known lc3 event is from > 5 seconds ago, reinitialize the mic:
|
|
311
|
+
val glassesMicEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
312
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
313
|
+
if (!glassesMicEnabled || !glassesConnected) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// When no frame has ever been received, treat elapsed as "forever" so we
|
|
318
|
+
// actually attempt recovery (was 0 before, which made the watchdog a no-op).
|
|
319
|
+
val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
|
|
320
|
+
if (timeSinceLastLc3Event > 5000) {
|
|
321
|
+
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
|
|
322
|
+
sgc?.setMicEnabled(true)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// MARK: - Unique (Android)
|
|
327
|
+
private fun setupPermissionMonitoring() {
|
|
328
|
+
val context = Bridge.getContext()
|
|
329
|
+
|
|
330
|
+
// Store initial permission state
|
|
331
|
+
lastHadBluetoothPermission = checkBluetoothPermission(context)
|
|
332
|
+
lastHadMicrophonePermission = checkMicrophonePermission(context)
|
|
333
|
+
|
|
334
|
+
Bridge.log(
|
|
335
|
+
"MAN: Initial permissions - BT: $lastHadBluetoothPermission, Mic: $lastHadMicrophonePermission"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// Create receiver for package changes (fires when permissions change)
|
|
339
|
+
permissionReceiver =
|
|
340
|
+
object : BroadcastReceiver() {
|
|
341
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
342
|
+
if (intent?.action == Intent.ACTION_PACKAGE_CHANGED &&
|
|
343
|
+
intent.data?.schemeSpecificPart == context?.packageName
|
|
344
|
+
) {
|
|
345
|
+
|
|
346
|
+
Bridge.log("MAN: Package changed, checking permissions...")
|
|
347
|
+
checkPermissionChanges()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Register the receiver
|
|
353
|
+
try {
|
|
354
|
+
val filter =
|
|
355
|
+
IntentFilter().apply {
|
|
356
|
+
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
|
357
|
+
addDataScheme("package")
|
|
358
|
+
}
|
|
359
|
+
context.registerReceiver(permissionReceiver, filter)
|
|
360
|
+
Bridge.log("MAN: Permission monitoring started")
|
|
361
|
+
} catch (e: Exception) {
|
|
362
|
+
Bridge.log("MAN: Failed to register permission receiver: ${e.message}")
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Also set up a periodic check as backup (some devices don't fire PACKAGE_CHANGED reliably)
|
|
366
|
+
// startPeriodicPermissionCheck()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun startPeriodicPermissionCheck() {
|
|
370
|
+
permissionCheckRunnable =
|
|
371
|
+
object : Runnable {
|
|
372
|
+
override fun run() {
|
|
373
|
+
checkPermissionChanges()
|
|
374
|
+
handler.postDelayed(this, 10000) // Check every 10 seconds
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
handler.postDelayed(permissionCheckRunnable!!, 10000)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private fun checkPermissionChanges() {
|
|
381
|
+
val context = Bridge.getContext()
|
|
382
|
+
|
|
383
|
+
val currentHasBluetoothPermission = checkBluetoothPermission(context)
|
|
384
|
+
val currentHasMicrophonePermission = checkMicrophonePermission(context)
|
|
385
|
+
|
|
386
|
+
var permissionsChanged = false
|
|
387
|
+
|
|
388
|
+
if (currentHasBluetoothPermission != lastHadBluetoothPermission) {
|
|
389
|
+
Bridge.log(
|
|
390
|
+
"MAN: Bluetooth permission changed: $lastHadBluetoothPermission -> $currentHasBluetoothPermission"
|
|
391
|
+
)
|
|
392
|
+
lastHadBluetoothPermission = currentHasBluetoothPermission
|
|
393
|
+
permissionsChanged = true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (currentHasMicrophonePermission != lastHadMicrophonePermission) {
|
|
397
|
+
Bridge.log(
|
|
398
|
+
"MAN: Microphone permission changed: $lastHadMicrophonePermission -> $currentHasMicrophonePermission"
|
|
399
|
+
)
|
|
400
|
+
lastHadMicrophonePermission = currentHasMicrophonePermission
|
|
401
|
+
permissionsChanged = true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (permissionsChanged && !currentHasBluetoothPermission) {
|
|
405
|
+
Bridge.log("MAN: Bluetooth permission revoked disconnecting glasses")
|
|
406
|
+
disconnect()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (permissionsChanged && serviceStarted) {
|
|
410
|
+
Bridge.log("MAN: Permissions changed, restarting service")
|
|
411
|
+
restartForegroundService()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private fun checkBluetoothPermission(context: Context): Boolean {
|
|
416
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
417
|
+
val connect = ContextCompat.checkSelfPermission(
|
|
418
|
+
context,
|
|
419
|
+
android.Manifest.permission.BLUETOOTH_CONNECT
|
|
420
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
421
|
+
val scan = ContextCompat.checkSelfPermission(
|
|
422
|
+
context,
|
|
423
|
+
android.Manifest.permission.BLUETOOTH_SCAN
|
|
424
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
425
|
+
connect && scan
|
|
426
|
+
} else {
|
|
427
|
+
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) ==
|
|
428
|
+
PackageManager.PERMISSION_GRANTED
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private fun hasBluetoothPermissions(): Boolean {
|
|
433
|
+
return checkBluetoothPermission(Bridge.getContext())
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun checkMicrophonePermission(context: Context): Boolean {
|
|
437
|
+
return ContextCompat.checkSelfPermission(
|
|
438
|
+
context,
|
|
439
|
+
android.Manifest.permission.RECORD_AUDIO
|
|
440
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun setupBluetoothStateMonitoring() {
|
|
444
|
+
val context = Bridge.getContext()
|
|
445
|
+
|
|
446
|
+
bluetoothStateReceiver =
|
|
447
|
+
object : BroadcastReceiver() {
|
|
448
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
449
|
+
if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
|
|
450
|
+
|
|
451
|
+
val state =
|
|
452
|
+
intent.getIntExtra(
|
|
453
|
+
BluetoothAdapter.EXTRA_STATE,
|
|
454
|
+
BluetoothAdapter.ERROR
|
|
455
|
+
)
|
|
456
|
+
when (state) {
|
|
457
|
+
BluetoothAdapter.STATE_OFF -> {
|
|
458
|
+
Bridge.log("MAN: Bluetooth turned OFF (control center or settings)")
|
|
459
|
+
disconnect()
|
|
460
|
+
}
|
|
461
|
+
BluetoothAdapter.STATE_TURNING_OFF -> {
|
|
462
|
+
Bridge.log("MAN: Bluetooth turning off...")
|
|
463
|
+
}
|
|
464
|
+
BluetoothAdapter.STATE_ON -> {
|
|
465
|
+
Bridge.log("MAN: Bluetooth turned ON")
|
|
466
|
+
// Auto-reconnect to last known device if we have one
|
|
467
|
+
if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
|
|
468
|
+
Bridge.log(
|
|
469
|
+
"MAN: Bluetooth restored, attempting reconnect to: $deviceName"
|
|
470
|
+
)
|
|
471
|
+
handler.postDelayed(
|
|
472
|
+
{ connectDefault() },
|
|
473
|
+
2000
|
|
474
|
+
) // Small delay to let BT stack stabilize
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
BluetoothAdapter.STATE_TURNING_ON -> {
|
|
478
|
+
Bridge.log("MAN: Bluetooth turning on...")
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
486
|
+
context.registerReceiver(bluetoothStateReceiver, filter)
|
|
487
|
+
isBluetoothStateReceiverRegistered = true
|
|
488
|
+
Bridge.log("MAN: Bluetooth state monitoring started")
|
|
489
|
+
} catch (e: Exception) {
|
|
490
|
+
Bridge.log("MAN: Failed to register Bluetooth state receiver: ${e.message}")
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private fun stopBluetoothStateMonitoring() {
|
|
495
|
+
if (isBluetoothStateReceiverRegistered && bluetoothStateReceiver != null) {
|
|
496
|
+
try {
|
|
497
|
+
Bridge.getContext().unregisterReceiver(bluetoothStateReceiver)
|
|
498
|
+
} catch (e: Exception) {
|
|
499
|
+
Bridge.log("MAN: Error unregistering Bluetooth state receiver: ${e.message}")
|
|
500
|
+
}
|
|
501
|
+
isBluetoothStateReceiverRegistered = false
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private fun startForegroundService() {
|
|
506
|
+
val context = Bridge.getContext()
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
Bridge.log("MAN: Starting foreground service")
|
|
510
|
+
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
511
|
+
|
|
512
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
513
|
+
context.startForegroundService(serviceIntent)
|
|
514
|
+
} else {
|
|
515
|
+
context.startService(serviceIntent)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
serviceStarted = true
|
|
519
|
+
Bridge.log("MAN: Foreground service started")
|
|
520
|
+
} catch (e: Exception) {
|
|
521
|
+
Bridge.log("MAN: Failed to start service: ${e.message}")
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private fun restartForegroundService() {
|
|
526
|
+
val context = Bridge.getContext()
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
// Stop the service
|
|
530
|
+
val stopIntent = Intent(context, ForegroundService::class.java)
|
|
531
|
+
context.stopService(stopIntent)
|
|
532
|
+
|
|
533
|
+
// Small delay
|
|
534
|
+
Thread.sleep(100)
|
|
535
|
+
|
|
536
|
+
// Start it again with new permissions
|
|
537
|
+
val startIntent = Intent(context, ForegroundService::class.java)
|
|
538
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
539
|
+
context.startForegroundService(startIntent)
|
|
540
|
+
} else {
|
|
541
|
+
context.startService(startIntent)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
Bridge.log("MAN: Service restarted with updated permissions")
|
|
545
|
+
} catch (e: Exception) {
|
|
546
|
+
Bridge.log("MAN: Failed to restart service: ${e.message}")
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private fun initializeViewStates() {
|
|
551
|
+
viewStates.clear()
|
|
552
|
+
|
|
553
|
+
// Matching Swift's 4 view states exactly
|
|
554
|
+
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
555
|
+
viewStates.add(
|
|
556
|
+
ViewState(
|
|
557
|
+
" ",
|
|
558
|
+
" ",
|
|
559
|
+
" ",
|
|
560
|
+
"text_wall",
|
|
561
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
562
|
+
null,
|
|
563
|
+
null
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
567
|
+
viewStates.add(
|
|
568
|
+
ViewState(
|
|
569
|
+
" ",
|
|
570
|
+
" ",
|
|
571
|
+
" ",
|
|
572
|
+
"text_wall",
|
|
573
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
574
|
+
null,
|
|
575
|
+
null
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private fun statesEqual(s1: ViewState, s2: ViewState): Boolean {
|
|
581
|
+
val state1 =
|
|
582
|
+
"${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
|
|
583
|
+
val state2 =
|
|
584
|
+
"${s2.layoutType}${s2.text}${s2.topText}${s2.bottomText}${s2.title}${s2.data ?: ""}"
|
|
585
|
+
return state1 == state2
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private fun Map<String, Any>.getString(key: String, defaultValue: String): String {
|
|
589
|
+
return (this[key] as? String) ?: defaultValue
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Inner classes
|
|
593
|
+
|
|
594
|
+
data class ViewState(
|
|
595
|
+
var topText: String,
|
|
596
|
+
var bottomText: String,
|
|
597
|
+
var title: String,
|
|
598
|
+
var layoutType: String,
|
|
599
|
+
var text: String,
|
|
600
|
+
var data: String?,
|
|
601
|
+
var animationData: Map<String, Any>?
|
|
602
|
+
)
|
|
603
|
+
// MARK: - End Unique
|
|
604
|
+
|
|
605
|
+
// MARK: - Voice Data Handling
|
|
606
|
+
|
|
607
|
+
private fun checkSetVadStatus(speaking: Boolean) {
|
|
608
|
+
if (speaking != isSpeaking) {
|
|
609
|
+
isSpeaking = speaking
|
|
610
|
+
Bridge.sendVadEvent(isSpeaking)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private fun convertAndSendMicLc3(pcmData: ByteArray) {
|
|
615
|
+
synchronized(lc3Lock) {
|
|
616
|
+
if (lc3EncoderPtr == 0L) {
|
|
617
|
+
Bridge.log("MAN: ERROR - LC3 encoder not initialized but format is LC3")
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
val lc3FrameSize = (DeviceStore.store.get("bluetooth", "lc3_frame_size") as Number).toInt()
|
|
621
|
+
val lc3Data = Lc3Cpp.encodeLC3(lc3EncoderPtr, pcmData, lc3FrameSize)
|
|
622
|
+
if (lc3Data == null || lc3Data.isEmpty()) {
|
|
623
|
+
Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
Bridge.sendMicLc3(lc3Data)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private fun handleSendingPcm(pcmData: ByteArray) {
|
|
631
|
+
if (shouldSendPcm) {
|
|
632
|
+
Bridge.sendMicPcm(pcmData)
|
|
633
|
+
}
|
|
634
|
+
if (shouldSendLc3) {
|
|
635
|
+
convertAndSendMicLc3(pcmData)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private fun emptyVadBuffer() {
|
|
640
|
+
while (vadBuffer.isNotEmpty()) {
|
|
641
|
+
val chunk = vadBuffer.removeAt(0)
|
|
642
|
+
handleSendingPcm(chunk) // Uses our encoder, not Bridge directly
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private fun addToVadBuffer(chunk: ByteArray) {
|
|
647
|
+
val MAX_BUFFER_SIZE = 20
|
|
648
|
+
vadBuffer.add(chunk)
|
|
649
|
+
while (vadBuffer.size > MAX_BUFFER_SIZE) {
|
|
650
|
+
vadBuffer.removeAt(0)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Handle raw LC3 audio data from glasses. Decodes the glasses LC3, then passes to handlePcm for
|
|
656
|
+
* canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
|
|
657
|
+
* phone→cloud encoding.
|
|
658
|
+
*/
|
|
659
|
+
fun handleGlassesMicData(rawLC3Data: ByteArray, frameSize: Int = 40) {
|
|
660
|
+
lastLc3Event = System.currentTimeMillis()
|
|
661
|
+
val pcmData: ByteArray?
|
|
662
|
+
synchronized(lc3Lock) {
|
|
663
|
+
if (lc3DecoderPtr == 0L) {
|
|
664
|
+
Bridge.log("MAN: LC3 decoder not initialized, cannot process glasses audio")
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
// Decode glasses LC3 to PCM (glasses may use different LC3 configs)
|
|
670
|
+
pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, rawLC3Data, frameSize)
|
|
671
|
+
} catch (e: Exception) {
|
|
672
|
+
Bridge.log("MAN: Failed to decode glasses LC3: ${e.message}")
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (pcmData != null && pcmData.isNotEmpty()) {
|
|
677
|
+
// Re-encode to canonical LC3 via handlePcm (outside lock to avoid deadlock)
|
|
678
|
+
handlePcm(pcmData)
|
|
679
|
+
} else {
|
|
680
|
+
Bridge.log("MAN: LC3 decode returned empty data")
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
fun handlePcm(pcmData: ByteArray) {
|
|
685
|
+
handleSendingPcm(pcmData)
|
|
686
|
+
|
|
687
|
+
// Send PCM to local transcriber (always needs raw PCM)
|
|
688
|
+
if (shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive) {
|
|
689
|
+
transcriber?.acceptAudio(pcmData)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// turns a single mic on and turns off all other mics:
|
|
694
|
+
private var isUpdatingMicState = false
|
|
695
|
+
private var pendingMicStateUpdate = false
|
|
696
|
+
|
|
697
|
+
internal fun updateMicState() {
|
|
698
|
+
// Guard against re-entrant calls from onRouteChange callbacks
|
|
699
|
+
if (isUpdatingMicState) {
|
|
700
|
+
pendingMicStateUpdate = true
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
isUpdatingMicState = true
|
|
704
|
+
pendingMicStateUpdate = false
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
updateMicStateInternal()
|
|
708
|
+
} finally {
|
|
709
|
+
isUpdatingMicState = false
|
|
710
|
+
// If a re-entrant call was requested, run it now
|
|
711
|
+
if (pendingMicStateUpdate) {
|
|
712
|
+
pendingMicStateUpdate = false
|
|
713
|
+
updateMicState()
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private fun updateMicStateInternal() {
|
|
719
|
+
// go through the micRanking and find the first mic that is available:
|
|
720
|
+
var micUsed: String = ""
|
|
721
|
+
|
|
722
|
+
// allow the sgc to make changes to the micRanking:
|
|
723
|
+
micRanking = sgc?.sortMicRanking(micRanking) ?: micRanking
|
|
724
|
+
Bridge.log("MAN: updateMicState() micRanking: $micRanking")
|
|
725
|
+
|
|
726
|
+
if (micEnabled) {
|
|
727
|
+
|
|
728
|
+
for (micMode in micRanking) {
|
|
729
|
+
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
730
|
+
micMode == MicTypes.BT_CLASSIC ||
|
|
731
|
+
micMode == MicTypes.BT
|
|
732
|
+
) {
|
|
733
|
+
|
|
734
|
+
if (phoneMic?.isRecordingWithMode(micMode) == true) {
|
|
735
|
+
micUsed = micMode
|
|
736
|
+
break
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (systemMicUnavailable) {
|
|
740
|
+
Bridge.log("MAN: systemMicUnavailable, continuing to next mic")
|
|
741
|
+
continue
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// if the phone mic is not recording, start recording:
|
|
745
|
+
val success = phoneMic?.startMode(micMode) ?: false
|
|
746
|
+
Bridge.log("MAN: starting mic mode: $micMode -> $success")
|
|
747
|
+
if (success) {
|
|
748
|
+
micUsed = micMode
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (micMode == MicTypes.GLASSES_CUSTOM) {
|
|
754
|
+
if (sgc?.hasMic == true) {
|
|
755
|
+
// enable the mic if it's not already on:
|
|
756
|
+
if (sgc?.micEnabled == false) {
|
|
757
|
+
sgc?.setMicEnabled(true)
|
|
758
|
+
micUsed = micMode
|
|
759
|
+
break
|
|
760
|
+
} else {
|
|
761
|
+
// the mic is already on, mark it as used and break:
|
|
762
|
+
micUsed = micMode
|
|
763
|
+
break
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// if the glasses doesn't have a mic, continue to the next mic:
|
|
767
|
+
continue
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
currentMic = micUsed
|
|
773
|
+
|
|
774
|
+
if (micUsed == "" && micEnabled) {
|
|
775
|
+
Bridge.log("MAN: No available mic found!")
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// go through and disable all mics after the first used one:
|
|
780
|
+
val allMics = micRanking
|
|
781
|
+
// add any missing mics to the list:
|
|
782
|
+
for (micMode in MicMap.map["auto"]!!) {
|
|
783
|
+
if (!allMics.contains(micMode)) {
|
|
784
|
+
allMics.add(micMode)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (micMode in allMics) {
|
|
788
|
+
if (micMode == micUsed) {
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
793
|
+
micMode == MicTypes.BT_CLASSIC ||
|
|
794
|
+
micMode == MicTypes.BT
|
|
795
|
+
) {
|
|
796
|
+
phoneMic?.stopMode(micMode)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true
|
|
800
|
+
) {
|
|
801
|
+
sgc?.setMicEnabled(false)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private fun setOnboardMicEnabled(enabled: Boolean) {
|
|
807
|
+
Bridge.log("MAN: setOnboardMicEnabled(): $enabled")
|
|
808
|
+
if (enabled) {
|
|
809
|
+
phoneMic?.startRecording()
|
|
810
|
+
} else {
|
|
811
|
+
phoneMic?.stopRecording()
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
fun sendCurrentState() {
|
|
816
|
+
val hUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
817
|
+
// Bridge.log("MAN: sendCurrentState(): $isHeadUp")
|
|
818
|
+
if (screenDisabled) {
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// executor.execute {
|
|
823
|
+
var currentViewState: ViewState
|
|
824
|
+
if (hUp) {
|
|
825
|
+
currentViewState = viewStates[1]
|
|
826
|
+
} else {
|
|
827
|
+
currentViewState = viewStates[0]
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (hUp && !contextualDashboard) {
|
|
831
|
+
currentViewState = viewStates[0]
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
|
|
835
|
+
// dont send the event to glasses that aren't there:
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
var fullyBooted = sgc?.fullyBooted ?: false
|
|
840
|
+
if (!fullyBooted) {
|
|
841
|
+
Bridge.log("MAN: DeviceManager.sendCurrentState(): sgc not ready")
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Cancel any pending clear display work item
|
|
846
|
+
// sendStateWorkItem?.let { mainHandler.removeCallbacks(it) }
|
|
847
|
+
|
|
848
|
+
// Bridge.log("MAN: parsing layoutType: ${currentViewState.layoutType}")
|
|
849
|
+
// Bridge.log(
|
|
850
|
+
// "MAN: viewState text: '${currentViewState.text}' (len=${currentViewState.text.length})"
|
|
851
|
+
// )
|
|
852
|
+
|
|
853
|
+
when (currentViewState.layoutType) {
|
|
854
|
+
"text_wall" -> {
|
|
855
|
+
sgc?.sendTextWall(currentViewState.text)
|
|
856
|
+
}
|
|
857
|
+
"double_text_wall" -> {
|
|
858
|
+
sgc?.sendDoubleTextWall(currentViewState.topText, currentViewState.bottomText)
|
|
859
|
+
}
|
|
860
|
+
"reference_card" -> {
|
|
861
|
+
sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
|
|
862
|
+
}
|
|
863
|
+
"bitmap_view" -> {
|
|
864
|
+
currentViewState.data?.let { data -> sgc?.displayBitmap(data) }
|
|
865
|
+
}
|
|
866
|
+
"clear_view" -> sgc?.clearDisplay()
|
|
867
|
+
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
868
|
+
}
|
|
869
|
+
// }
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private fun parsePlaceholders(text: String): String {
|
|
873
|
+
val dateFormatter = SimpleDateFormat("M/dd, h:mm", Locale.getDefault())
|
|
874
|
+
val formattedDate = dateFormatter.format(Date())
|
|
875
|
+
|
|
876
|
+
val time12Format = SimpleDateFormat("hh:mm", Locale.getDefault())
|
|
877
|
+
val time12 = time12Format.format(Date())
|
|
878
|
+
|
|
879
|
+
val time24Format = SimpleDateFormat("HH:mm", Locale.getDefault())
|
|
880
|
+
val time24 = time24Format.format(Date())
|
|
881
|
+
|
|
882
|
+
val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault())
|
|
883
|
+
val currentDate = dateFormat.format(Date())
|
|
884
|
+
|
|
885
|
+
val placeholders =
|
|
886
|
+
mapOf(
|
|
887
|
+
"\$no_datetime$" to formattedDate,
|
|
888
|
+
"\$DATE$" to currentDate,
|
|
889
|
+
"\$TIME12$" to time12,
|
|
890
|
+
"\$TIME24$" to time24,
|
|
891
|
+
"\$GBATT$" to
|
|
892
|
+
(sgc?.batteryLevel?.let { if (it == -1) "" else "$it%" } ?: ""),
|
|
893
|
+
"\$CONNECTION_STATUS$" to "Connected"
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
return placeholders.entries.fold(text) { result, (key, value) ->
|
|
897
|
+
result.replace(key, value)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private fun appendLog(entry: String) {
|
|
902
|
+
lastLog = (lastLog + entry).takeLast(100).toMutableList()
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fun onRouteChange(reason: String, availableInputs: List<String>) {
|
|
906
|
+
Bridge.log("MAN: onRouteChange: reason: $reason")
|
|
907
|
+
Bridge.log("MAN: onRouteChange: inputs: $availableInputs")
|
|
908
|
+
|
|
909
|
+
// Handle external app conflicts - automatically switch to glasses mic if available
|
|
910
|
+
when (reason) {
|
|
911
|
+
"external_app_recording" -> {
|
|
912
|
+
// Another app is using the microphone
|
|
913
|
+
systemMicUnavailable = true
|
|
914
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
915
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
916
|
+
}
|
|
917
|
+
"audio_focus_available" -> {
|
|
918
|
+
// Audio focus is available again
|
|
919
|
+
systemMicUnavailable = false
|
|
920
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
921
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
922
|
+
}
|
|
923
|
+
"external_app_stopped" -> {
|
|
924
|
+
// External app stopped recording
|
|
925
|
+
systemMicUnavailable = false
|
|
926
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
927
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
928
|
+
}
|
|
929
|
+
"phone_call_interruption" -> {
|
|
930
|
+
// Phone call started - mark mic as unavailable
|
|
931
|
+
systemMicUnavailable = true
|
|
932
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
933
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
934
|
+
}
|
|
935
|
+
"phone_call_ended" -> {
|
|
936
|
+
// Phone call ended - mark mic as available again
|
|
937
|
+
systemMicUnavailable = false
|
|
938
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
939
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
940
|
+
}
|
|
941
|
+
"phone_call_active" -> {
|
|
942
|
+
// Tried to start recording while phone call already active
|
|
943
|
+
systemMicUnavailable = true
|
|
944
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
945
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
946
|
+
}
|
|
947
|
+
"audio_focus_denied" -> {
|
|
948
|
+
// Another app has audio focus
|
|
949
|
+
systemMicUnavailable = true
|
|
950
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
951
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
952
|
+
}
|
|
953
|
+
"permission_denied" -> {
|
|
954
|
+
// Microphone permission not granted
|
|
955
|
+
systemMicUnavailable = true
|
|
956
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
957
|
+
appendLog("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
958
|
+
// Don't trigger fallback - need to request permission from user
|
|
959
|
+
}
|
|
960
|
+
"audio_route_changed" -> {
|
|
961
|
+
// Audio route changed
|
|
962
|
+
// systemMicUnavailable = false
|
|
963
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
964
|
+
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
965
|
+
}
|
|
966
|
+
"recording_started" -> {
|
|
967
|
+
// this is an event from the PhoneMic saying we have started recording
|
|
968
|
+
// Audio recording started
|
|
969
|
+
// systemMicUnavailable = true
|
|
970
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
971
|
+
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
972
|
+
}
|
|
973
|
+
"recording_stopped" -> {
|
|
974
|
+
// this is an event from the PhoneMic saying we have stopped recording
|
|
975
|
+
// Audio recording stopped
|
|
976
|
+
// systemMicUnavailable = false
|
|
977
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
978
|
+
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
979
|
+
}
|
|
980
|
+
else -> {
|
|
981
|
+
// Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
|
|
982
|
+
// Just log for now - may want to handle these in the future
|
|
983
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
984
|
+
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
985
|
+
// systemMicUnavailable = false
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
updateMicState()
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
fun onInterruption(began: Boolean) {
|
|
993
|
+
Bridge.log("MAN: Interruption: $began")
|
|
994
|
+
systemMicUnavailable = began
|
|
995
|
+
updateMicState()
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// MARK: - Auxiliary Commands
|
|
999
|
+
|
|
1000
|
+
fun initSGC(wearable: String) {
|
|
1001
|
+
Bridge.log("Initializing manager for wearable: $wearable")
|
|
1002
|
+
if (sgc != null && sgc?.type != wearable) {
|
|
1003
|
+
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
1004
|
+
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
1005
|
+
sgc?.cleanup()
|
|
1006
|
+
sgc = null
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (sgc != null) {
|
|
1010
|
+
Bridge.log("MAN: SGC already initialized")
|
|
1011
|
+
return
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (wearable.contains(DeviceTypes.SIMULATED)) {
|
|
1015
|
+
sgc = Simulated()
|
|
1016
|
+
} else if (wearable.contains(DeviceTypes.G1)) {
|
|
1017
|
+
sgc = G1()
|
|
1018
|
+
} else if (wearable.contains(DeviceTypes.G2)) {
|
|
1019
|
+
sgc = G2()
|
|
1020
|
+
} else if (wearable.contains(DeviceTypes.LIVE)) {
|
|
1021
|
+
sgc = MentraLive()
|
|
1022
|
+
} else if (wearable.contains(DeviceTypes.NEX)) {
|
|
1023
|
+
sgc = MentraNex()
|
|
1024
|
+
} else if (wearable.contains(DeviceTypes.MACH1)) {
|
|
1025
|
+
sgc = Mach1()
|
|
1026
|
+
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
1027
|
+
sgc = Mach1() // Z100 uses same hardware/SDK as Mach1
|
|
1028
|
+
sgc?.type = DeviceTypes.Z100 // Override type to Z100
|
|
1029
|
+
} else if (wearable.contains(DeviceTypes.FRAME)) {
|
|
1030
|
+
// sgc = FrameManager()
|
|
1031
|
+
}
|
|
1032
|
+
// update device model:
|
|
1033
|
+
DeviceStore.apply("glasses", "deviceModel", sgc?.type ?: "")
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
fun initController(controllerType: String) {
|
|
1037
|
+
Bridge.log("MAN: Initializing controller: $controllerType")
|
|
1038
|
+
if (controller != null && controller?.type != controllerType) {
|
|
1039
|
+
Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
|
|
1040
|
+
controller?.cleanup()
|
|
1041
|
+
controller = null
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (controller != null) {
|
|
1045
|
+
Bridge.log("MAN: Controller already initialized")
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (controllerType == ControllerTypes.R1) {
|
|
1050
|
+
controller = R1()
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
fun restartTranscriber() {
|
|
1055
|
+
Bridge.log("MAN: Restarting transcriber via command")
|
|
1056
|
+
transcriber?.restart()
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// MARK: - connection state management
|
|
1060
|
+
|
|
1061
|
+
fun handleDeviceReady() {
|
|
1062
|
+
if (sgc == null) {
|
|
1063
|
+
Bridge.log("MAN: SGC is null, returning")
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
val readyKey = "${sgc?.type}:${deviceName}"
|
|
1068
|
+
val now = System.currentTimeMillis()
|
|
1069
|
+
if (readyKey == lastReadyHandledKey && now - lastReadyHandledAtMs < 2000) {
|
|
1070
|
+
Bridge.log("MAN: handleDeviceReady() duplicate suppressed for $readyKey")
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
lastReadyHandledKey = readyKey
|
|
1074
|
+
lastReadyHandledAtMs = now
|
|
1075
|
+
|
|
1076
|
+
Bridge.log("MAN: handleDeviceReady() ${sgc?.type}")
|
|
1077
|
+
pendingWearable = ""
|
|
1078
|
+
defaultWearable = sgc?.type ?: ""
|
|
1079
|
+
searching = false
|
|
1080
|
+
|
|
1081
|
+
// Apply dashboard position before any boot text so content doesn't jump.
|
|
1082
|
+
sgc?.setDashboardPosition(dashboardHeight, dashboardDepth)
|
|
1083
|
+
|
|
1084
|
+
// Show welcome message on first connect for all display glasses
|
|
1085
|
+
if (shouldSendBootingMessage) {
|
|
1086
|
+
shouldSendBootingMessage = false
|
|
1087
|
+
executor.execute {
|
|
1088
|
+
sgc?.sendTextWall("// MentraOS Connected")
|
|
1089
|
+
Thread.sleep(3000)
|
|
1090
|
+
sgc?.clearDisplay()
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Call device-specific setup handlers
|
|
1095
|
+
if (defaultWearable.contains(DeviceTypes.G1)) {
|
|
1096
|
+
handleG1Ready()
|
|
1097
|
+
} else if (defaultWearable.contains(DeviceTypes.MACH1)) {
|
|
1098
|
+
handleMach1Ready()
|
|
1099
|
+
} else if (defaultWearable.contains(DeviceTypes.Z100)) {
|
|
1100
|
+
handleMach1Ready() // Z100 uses same initialization as Mach1
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Re-apply microphone settings after reconnection
|
|
1104
|
+
// Cache was cleared on disconnect, so this will definitely send commands
|
|
1105
|
+
Bridge.log("MAN: Re-applying microphone settings after reconnection")
|
|
1106
|
+
updateMicState()
|
|
1107
|
+
|
|
1108
|
+
// send to the server our battery status:
|
|
1109
|
+
Bridge.sendBatteryStatus(sgc?.batteryLevel ?: -1, false)
|
|
1110
|
+
|
|
1111
|
+
// save the default_wearable now that we're connected:
|
|
1112
|
+
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
1113
|
+
Bridge.saveSetting("device_name", deviceName)
|
|
1114
|
+
Bridge.saveSetting("device_address", deviceAddress)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private fun handleG1Ready() {
|
|
1118
|
+
// G1-specific setup (if any needed in the future)
|
|
1119
|
+
// Note: G1-specific settings like silent mode, battery status,
|
|
1120
|
+
// head up angle, brightness, etc. could be configured here
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private fun handleMach1Ready() {
|
|
1124
|
+
// Mach1-specific setup (if any needed in the future)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
fun handleDeviceDisconnected() {
|
|
1128
|
+
Bridge.log("MAN: Device disconnected")
|
|
1129
|
+
DeviceStore.apply("glasses", "headUp", false)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
fun handleControllerReady() {
|
|
1133
|
+
val c = controller
|
|
1134
|
+
if (c == null) {
|
|
1135
|
+
Bridge.log("MAN: Controller is nil, returning")
|
|
1136
|
+
return
|
|
1137
|
+
}
|
|
1138
|
+
Bridge.log("MAN: handleControllerReady(): ${c.type}")
|
|
1139
|
+
|
|
1140
|
+
pendingController = ""
|
|
1141
|
+
defaultController = c.type
|
|
1142
|
+
searching = false
|
|
1143
|
+
|
|
1144
|
+
// save the default_controller now that we're connected:
|
|
1145
|
+
Bridge.saveSetting("default_controller", defaultController)
|
|
1146
|
+
Bridge.saveSetting("controller_device_name", controllerDeviceName)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
fun handleControllerDisconnected() {
|
|
1150
|
+
Bridge.log("MAN: Controller disconnected")
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// MARK: - Network Command handlers
|
|
1154
|
+
|
|
1155
|
+
fun displayText(params: Map<String, Any>) {
|
|
1156
|
+
(params["text"] as? String)?.let { text ->
|
|
1157
|
+
Bridge.log("MAN: Displaying text: $text")
|
|
1158
|
+
sgc?.sendTextWall(text)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
fun clearDisplay() {
|
|
1163
|
+
Bridge.log("MAN: Clearing Display")
|
|
1164
|
+
sgc?.clearDisplay()
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
fun displayEvent(event: Map<String, Any>) {
|
|
1168
|
+
val view = event["view"] as? String
|
|
1169
|
+
if (view == null) {
|
|
1170
|
+
Bridge.log("MAN: Invalid view")
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
val isDashboard = view == "dashboard"
|
|
1175
|
+
val stateIndex = if (isDashboard) 1 else 0
|
|
1176
|
+
|
|
1177
|
+
@Suppress("UNCHECKED_CAST") val layout = event["layout"] as? Map<String, Any> ?: return
|
|
1178
|
+
|
|
1179
|
+
val layoutType = layout["layoutType"] as? String
|
|
1180
|
+
val text = parsePlaceholders(layout.getString("text", " "))
|
|
1181
|
+
val topText = parsePlaceholders(layout.getString("topText", " "))
|
|
1182
|
+
val bottomText = parsePlaceholders(layout.getString("bottomText", " "))
|
|
1183
|
+
val title = parsePlaceholders(layout.getString("title", " "))
|
|
1184
|
+
val data = layout["data"] as? String
|
|
1185
|
+
|
|
1186
|
+
var newViewState = ViewState(topText, bottomText, title, layoutType ?: "", text, data, null)
|
|
1187
|
+
|
|
1188
|
+
val currentState = viewStates[stateIndex]
|
|
1189
|
+
|
|
1190
|
+
if (statesEqual(currentState, newViewState)) {
|
|
1191
|
+
return
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
viewStates[stateIndex] = newViewState
|
|
1195
|
+
val hUp = headUp && contextualDashboard
|
|
1196
|
+
// send the state we just received if the user is currently in that state:
|
|
1197
|
+
if (stateIndex == 0 && !hUp) {
|
|
1198
|
+
sendCurrentState()
|
|
1199
|
+
} else if (stateIndex == 1 && hUp) {
|
|
1200
|
+
sendCurrentState()
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fun showDashboard() {
|
|
1205
|
+
sgc?.showDashboard()
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
fun ping() {
|
|
1209
|
+
sgc?.ping()
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
fun dbg1() {
|
|
1213
|
+
Bridge.log("MAN: dbg1()")
|
|
1214
|
+
sgc?.dbg1()
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
fun dbg2() {
|
|
1218
|
+
Bridge.log("MAN: dbg2()")
|
|
1219
|
+
sgc?.dbg2()
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
fun startStream(message: MutableMap<String, Any>) {
|
|
1223
|
+
Bridge.log("MAN: startStream")
|
|
1224
|
+
sgc?.startStream(message)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
fun stopStream() {
|
|
1228
|
+
Bridge.log("MAN: stopStream")
|
|
1229
|
+
sgc?.stopStream()
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
fun keepStreamAlive(message: MutableMap<String, Any>) {
|
|
1233
|
+
Bridge.log("MAN: keepStreamAlive: (message)")
|
|
1234
|
+
sgc?.sendStreamKeepAlive(message)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
fun requestWifiScan() {
|
|
1238
|
+
Bridge.log("MAN: Requesting wifi scan")
|
|
1239
|
+
DeviceStore.apply("bluetooth", "wifiScanResults", emptyList<Any>())
|
|
1240
|
+
sgc?.requestWifiScan()
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
|
|
1244
|
+
Bridge.log("MAN: Sending incidentId to glasses for log upload: $incidentId")
|
|
1245
|
+
sgc?.sendIncidentId(incidentId, apiBaseUrl)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
fun sendWifiCredentials(ssid: String, password: String) {
|
|
1249
|
+
Bridge.log("MAN: Sending wifi credentials: $ssid")
|
|
1250
|
+
sgc?.sendWifiCredentials(ssid, password)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
fun forgetWifiNetwork(ssid: String) {
|
|
1254
|
+
Bridge.log("MAN: Forgetting wifi network: $ssid")
|
|
1255
|
+
sgc?.forgetWifiNetwork(ssid)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
fun setHotspotState(enabled: Boolean) {
|
|
1259
|
+
Bridge.log("MAN: Setting glasses hotspot state: $enabled")
|
|
1260
|
+
sgc?.sendHotspotState(enabled)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
fun queryGalleryStatus() {
|
|
1264
|
+
Bridge.log("MAN: Querying gallery status from glasses")
|
|
1265
|
+
sgc?.queryGalleryStatus()
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Send OTA start command to glasses. Called when user approves an update (onboarding or
|
|
1270
|
+
* background mode). Triggers glasses to begin download and installation.
|
|
1271
|
+
*/
|
|
1272
|
+
fun sendOtaStart() {
|
|
1273
|
+
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1274
|
+
(sgc as? MentraLive)?.sendOtaStart()
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
fun sendOtaQueryStatus() {
|
|
1278
|
+
Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
|
|
1279
|
+
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Read glasses media step volume (0–15) via K900 on Mentra Live only. Blocks until response,
|
|
1284
|
+
* error, or timeout (used from JS AsyncFunction on a worker thread).
|
|
1285
|
+
*/
|
|
1286
|
+
fun getGlassesMediaVolumeBlocking(): Map<String, Any> {
|
|
1287
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1288
|
+
val latch = CountDownLatch(1)
|
|
1289
|
+
var result: Map<String, Any>? = null
|
|
1290
|
+
var error: String? = null
|
|
1291
|
+
live.getGlassesMediaVolume(
|
|
1292
|
+
{ m ->
|
|
1293
|
+
result = m
|
|
1294
|
+
latch.countDown()
|
|
1295
|
+
},
|
|
1296
|
+
{ e ->
|
|
1297
|
+
error = e
|
|
1298
|
+
latch.countDown()
|
|
1299
|
+
}
|
|
1300
|
+
)
|
|
1301
|
+
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1302
|
+
if (!completed) {
|
|
1303
|
+
throw IllegalStateException("glasses_volume_timeout")
|
|
1304
|
+
}
|
|
1305
|
+
error?.let { throw IllegalStateException(it) }
|
|
1306
|
+
return result ?: throw IllegalStateException("glasses_volume_empty")
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** Set glasses media step volume (0–15) via K900 on Mentra Live only. */
|
|
1310
|
+
fun setGlassesMediaVolumeBlocking(level: Int): Map<String, Any> {
|
|
1311
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1312
|
+
val latch = CountDownLatch(1)
|
|
1313
|
+
var result: Map<String, Any>? = null
|
|
1314
|
+
var error: String? = null
|
|
1315
|
+
live.setGlassesMediaVolume(
|
|
1316
|
+
level,
|
|
1317
|
+
{ m ->
|
|
1318
|
+
result = m
|
|
1319
|
+
latch.countDown()
|
|
1320
|
+
},
|
|
1321
|
+
{ e ->
|
|
1322
|
+
error = e
|
|
1323
|
+
latch.countDown()
|
|
1324
|
+
}
|
|
1325
|
+
)
|
|
1326
|
+
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1327
|
+
if (!completed) {
|
|
1328
|
+
throw IllegalStateException("glasses_volume_timeout")
|
|
1329
|
+
}
|
|
1330
|
+
error?.let { throw IllegalStateException(it) }
|
|
1331
|
+
return result ?: throw IllegalStateException("glasses_volume_empty")
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Request version info from glasses. Glasses will respond with version_info message containing
|
|
1336
|
+
* build number, firmware version, etc.
|
|
1337
|
+
*/
|
|
1338
|
+
fun requestVersionInfo() {
|
|
1339
|
+
Bridge.log("MAN: 📱 Requesting version info from glasses")
|
|
1340
|
+
sgc?.requestVersionInfo()
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Send shutdown command to glasses. This will initiate a graceful shutdown of the device. */
|
|
1344
|
+
fun sendShutdown() {
|
|
1345
|
+
Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
|
|
1346
|
+
sgc?.sendShutdown()
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** Send reboot command to glasses. This will initiate a reboot of the device. */
|
|
1350
|
+
fun sendReboot() {
|
|
1351
|
+
Bridge.log("MAN: 🔄 Sending reboot command to glasses")
|
|
1352
|
+
sgc?.sendReboot()
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {
|
|
1356
|
+
Bridge.log(
|
|
1357
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=$flash, sound=$sound"
|
|
1358
|
+
)
|
|
1359
|
+
sgc?.startVideoRecording(requestId, save, flash, sound)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
fun stopVideoRecording(requestId: String) {
|
|
1363
|
+
Bridge.log("MAN: onStopVideoRecording: requestId=$requestId")
|
|
1364
|
+
sgc?.stopVideoRecording(requestId)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
fun setMicState() {
|
|
1368
|
+
val willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1369
|
+
val willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
|
|
1370
|
+
micEnabled = willSendPcm || willSendTranscript
|
|
1371
|
+
vadBuffer.clear()
|
|
1372
|
+
updateMicState()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
fun photoRequest(
|
|
1376
|
+
requestId: String,
|
|
1377
|
+
appId: String,
|
|
1378
|
+
size: String,
|
|
1379
|
+
webhookUrl: String,
|
|
1380
|
+
authToken: String?,
|
|
1381
|
+
compress: String,
|
|
1382
|
+
flash: Boolean,
|
|
1383
|
+
sound: Boolean
|
|
1384
|
+
) {
|
|
1385
|
+
Bridge.log(
|
|
1386
|
+
"MAN: onPhotoRequest: $requestId, $appId, $size, compress=$compress, flash=$flash, sound=$sound"
|
|
1387
|
+
)
|
|
1388
|
+
sgc?.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
fun rgbLedControl(
|
|
1392
|
+
requestId: String,
|
|
1393
|
+
packageName: String?,
|
|
1394
|
+
action: String,
|
|
1395
|
+
color: String?,
|
|
1396
|
+
ontime: Int,
|
|
1397
|
+
offtime: Int,
|
|
1398
|
+
count: Int
|
|
1399
|
+
) {
|
|
1400
|
+
Bridge.log("MAN: RGB LED control: action=$action, color=$color, requestId=$requestId")
|
|
1401
|
+
sgc?.sendRgbLedControl(requestId, packageName, action, color, ontime, offtime, count)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
fun connectDefault() {
|
|
1405
|
+
if (defaultWearable.isEmpty()) {
|
|
1406
|
+
Bridge.log("MAN: No default wearable, returning")
|
|
1407
|
+
return
|
|
1408
|
+
}
|
|
1409
|
+
if (deviceName.isEmpty()) {
|
|
1410
|
+
Bridge.log("MAN: No device name, returning")
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
if (!hasBluetoothPermissions()) {
|
|
1414
|
+
// Auto-reconnect paths (boot, BT toggle, app launch before perm flow)
|
|
1415
|
+
// may fire before user has granted runtime Bluetooth permissions on Android 12+.
|
|
1416
|
+
// Bail out instead of crashing with SecurityException on startScan / getRemoteName.
|
|
1417
|
+
Bridge.log("MAN: connectDefault skipped — bluetooth runtime permissions not granted")
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1420
|
+
initSGC(defaultWearable)
|
|
1421
|
+
searching = true
|
|
1422
|
+
sgc?.connectById(deviceName)
|
|
1423
|
+
connectDefaultController()
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
fun connectDefaultController() {
|
|
1427
|
+
if (defaultController.isEmpty()) {
|
|
1428
|
+
Bridge.log("MAN: No default controller, returning")
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
if (controllerDeviceName.isEmpty()) {
|
|
1432
|
+
Bridge.log("MAN: No controller device name, returning")
|
|
1433
|
+
return
|
|
1434
|
+
}
|
|
1435
|
+
if (!hasBluetoothPermissions()) {
|
|
1436
|
+
Bridge.log("MAN: connectDefaultController skipped — bluetooth runtime permissions not granted")
|
|
1437
|
+
return
|
|
1438
|
+
}
|
|
1439
|
+
initController(defaultController)
|
|
1440
|
+
searchingController = true
|
|
1441
|
+
controller?.connectById(controllerDeviceName)
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
fun connectByName(dName: String) {
|
|
1445
|
+
Bridge.log("MAN: Connecting to wearable: $dName")
|
|
1446
|
+
|
|
1447
|
+
var name = dName
|
|
1448
|
+
|
|
1449
|
+
// use stored device name if available:
|
|
1450
|
+
if (dName.isEmpty() && !deviceName.isEmpty()) {
|
|
1451
|
+
name = deviceName
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (pendingWearable.isEmpty() && defaultWearable.isEmpty()) {
|
|
1455
|
+
Bridge.log("MAN: No pending or default wearable, returning")
|
|
1456
|
+
return
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (pendingWearable.isEmpty() && !defaultWearable.isEmpty()) {
|
|
1460
|
+
Bridge.log("MAN: No pending wearable, using default wearable")
|
|
1461
|
+
pendingWearable = defaultWearable
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// if the pending wearable is a controller, don't disconnect the glasses;
|
|
1465
|
+
// route through the controller manager instead
|
|
1466
|
+
if (ControllerTypes.ALL.contains(pendingWearable)) {
|
|
1467
|
+
controller?.disconnect()
|
|
1468
|
+
initController(pendingWearable)
|
|
1469
|
+
controller?.connectById(name)
|
|
1470
|
+
return
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
disconnect()
|
|
1474
|
+
Thread.sleep(100)
|
|
1475
|
+
searching = true
|
|
1476
|
+
deviceName = name
|
|
1477
|
+
|
|
1478
|
+
initSGC(pendingWearable)
|
|
1479
|
+
sgc?.connectById(deviceName)
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
fun connectDevice(deviceModel: String, deviceName: String) {
|
|
1483
|
+
Bridge.log("MAN: Connecting to device: $deviceModel $deviceName")
|
|
1484
|
+
if (DeviceTypes.ALL.contains(deviceModel)) {
|
|
1485
|
+
pendingWearable = deviceModel
|
|
1486
|
+
initSGC(pendingWearable)
|
|
1487
|
+
sgc?.connectById(deviceName)
|
|
1488
|
+
return
|
|
1489
|
+
}
|
|
1490
|
+
if (ControllerTypes.ALL.contains(deviceModel)) {
|
|
1491
|
+
pendingWearable = deviceModel
|
|
1492
|
+
initController(deviceModel)
|
|
1493
|
+
controller?.connectById(deviceName)
|
|
1494
|
+
return
|
|
1495
|
+
}
|
|
1496
|
+
Bridge.log("MAN: No compatible device model, returning")
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
fun connectSimulated() {
|
|
1500
|
+
defaultWearable = DeviceTypes.SIMULATED
|
|
1501
|
+
deviceName = DeviceTypes.SIMULATED
|
|
1502
|
+
initSGC(defaultWearable)
|
|
1503
|
+
handleDeviceReady()
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
fun disconnect() {
|
|
1507
|
+
sgc?.clearDisplay()
|
|
1508
|
+
sgc?.disconnect()
|
|
1509
|
+
sgc = null // Clear the SGC reference after disconnect
|
|
1510
|
+
searching = false
|
|
1511
|
+
micEnabled = false
|
|
1512
|
+
updateMicState()
|
|
1513
|
+
shouldSendBootingMessage = true // Reset for next first connect
|
|
1514
|
+
// clear glasses properties:
|
|
1515
|
+
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1516
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1517
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
1518
|
+
// disconnect the controller as well:
|
|
1519
|
+
searchingController = false
|
|
1520
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
1521
|
+
controller?.disconnect()
|
|
1522
|
+
controller = null
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
fun disconnectController() {
|
|
1526
|
+
searchingController = false
|
|
1527
|
+
// disconnect the controller from the glasses if applicable:
|
|
1528
|
+
sgc?.disconnectController()
|
|
1529
|
+
controller?.disconnect()
|
|
1530
|
+
controller = null
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
fun forget() {
|
|
1534
|
+
Bridge.log("MAN: Forgetting smart glasses")
|
|
1535
|
+
|
|
1536
|
+
// Call forget first to stop timers/handlers/reconnect logic
|
|
1537
|
+
sgc?.forget()
|
|
1538
|
+
|
|
1539
|
+
// Then disconnect to close connections
|
|
1540
|
+
disconnect()
|
|
1541
|
+
|
|
1542
|
+
// Clear state
|
|
1543
|
+
defaultWearable = ""
|
|
1544
|
+
deviceName = ""
|
|
1545
|
+
deviceAddress = ""
|
|
1546
|
+
Bridge.saveSetting("default_wearable", "")
|
|
1547
|
+
Bridge.saveSetting("device_name", "")
|
|
1548
|
+
Bridge.saveSetting("device_address", "")
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
fun forgetController() {
|
|
1552
|
+
Bridge.log("MAN: Forgetting controller")
|
|
1553
|
+
controller?.forget()
|
|
1554
|
+
disconnectController()
|
|
1555
|
+
// Clear state
|
|
1556
|
+
defaultController = ""
|
|
1557
|
+
controllerDeviceName = ""
|
|
1558
|
+
Bridge.saveSetting("controller_device_name", "")
|
|
1559
|
+
Bridge.saveSetting("default_controller", "")
|
|
1560
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
fun findCompatibleDevices(deviceModel: String) {
|
|
1564
|
+
Bridge.log("MAN: Searching for compatible device names for: $deviceModel")
|
|
1565
|
+
|
|
1566
|
+
// reset the search results:
|
|
1567
|
+
searchResults = emptyList()
|
|
1568
|
+
|
|
1569
|
+
if (DeviceTypes.ALL.contains(deviceModel)) {
|
|
1570
|
+
pendingWearable = deviceModel
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (ControllerTypes.ALL.contains(deviceModel)) {
|
|
1574
|
+
pendingWearable = deviceModel
|
|
1575
|
+
initController(deviceModel)
|
|
1576
|
+
controller?.findCompatibleDevices()
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
initSGC(pendingWearable)
|
|
1581
|
+
Bridge.log("MAN: sgc initialized, calling findCompatibleDevices")
|
|
1582
|
+
sgc?.findCompatibleDevices()
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
fun stopScan() {
|
|
1586
|
+
controller?.stopScan()
|
|
1587
|
+
sgc?.stopScan()
|
|
1588
|
+
DeviceStore.apply("bluetooth", "searching", false)
|
|
1589
|
+
DeviceStore.apply("bluetooth", "searchingController", false)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// MARK: Cleanup
|
|
1593
|
+
fun cleanup() {
|
|
1594
|
+
stopBluetoothStateMonitoring()
|
|
1595
|
+
|
|
1596
|
+
micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1597
|
+
micReinitRunnable = null
|
|
1598
|
+
|
|
1599
|
+
// Clean up transcriber resources
|
|
1600
|
+
transcriber?.shutdown()
|
|
1601
|
+
transcriber = null
|
|
1602
|
+
|
|
1603
|
+
// Clean up LC3 encoder/decoder (synchronized to prevent use-after-free
|
|
1604
|
+
// if the recording thread is mid-encode/decode)
|
|
1605
|
+
synchronized(lc3Lock) {
|
|
1606
|
+
if (lc3EncoderPtr != 0L) {
|
|
1607
|
+
Lc3Cpp.freeEncoder(lc3EncoderPtr)
|
|
1608
|
+
lc3EncoderPtr = 0
|
|
1609
|
+
}
|
|
1610
|
+
if (lc3DecoderPtr != 0L) {
|
|
1611
|
+
Lc3Cpp.freeDecoder(lc3DecoderPtr)
|
|
1612
|
+
lc3DecoderPtr = 0
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|