@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,2325 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.bluetooth.BluetoothManager
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import com.glassly.bluetoothsdk.utils.ControllerTypes
|
|
11
|
+
import com.glassly.bluetoothsdk.utils.PhoneAudioMonitor
|
|
12
|
+
import java.util.Collections
|
|
13
|
+
import java.util.UUID
|
|
14
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
15
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
16
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
17
|
+
import kotlinx.coroutines.CancellationException
|
|
18
|
+
import kotlinx.coroutines.CompletableDeferred
|
|
19
|
+
import kotlinx.coroutines.delay
|
|
20
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
21
|
+
|
|
22
|
+
class BluetoothSdk private constructor(
|
|
23
|
+
context: Context,
|
|
24
|
+
private val config: BluetoothSdkConfig,
|
|
25
|
+
listener: BluetoothSdkListener,
|
|
26
|
+
) : AutoCloseable {
|
|
27
|
+
private val appContext = context.applicationContext
|
|
28
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
29
|
+
private val deviceManager: DeviceManager
|
|
30
|
+
private val listeners =
|
|
31
|
+
Collections.synchronizedSet(mutableSetOf<BluetoothSdkListener>())
|
|
32
|
+
private val analytics = BluetoothSdkAnalytics(appContext, config.analytics)
|
|
33
|
+
private val discoveredDeviceNames = mutableSetOf<String>()
|
|
34
|
+
private val bridgeEventSinkId: String
|
|
35
|
+
private val storeListenerId: String
|
|
36
|
+
private val activityLifecycleCallbacks =
|
|
37
|
+
object : Application.ActivityLifecycleCallbacks {
|
|
38
|
+
override fun onActivityResumed(activity: Activity) {
|
|
39
|
+
// Match the Glassly App's OnActivityEntersForeground hook. This runs after
|
|
40
|
+
// runtime permission dialogs in native host apps, allowing the service to
|
|
41
|
+
// replace its temporary dataSync type with connectedDevice/microphone/location.
|
|
42
|
+
deviceManager.refreshForegroundServiceTypes()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
|
46
|
+
override fun onActivityStarted(activity: Activity) = Unit
|
|
47
|
+
override fun onActivityPaused(activity: Activity) = Unit
|
|
48
|
+
override fun onActivityStopped(activity: Activity) = Unit
|
|
49
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
50
|
+
override fun onActivityDestroyed(activity: Activity) = Unit
|
|
51
|
+
}
|
|
52
|
+
private var activityLifecycleCallbacksRegistered = false
|
|
53
|
+
private var suppressDefaultDeviceEvents = false
|
|
54
|
+
private val streamKeepAliveLock = Any()
|
|
55
|
+
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
56
|
+
private val pendingPhotoRequests = ConcurrentHashMap<String, PendingResponse<PhotoResponseEvent>>()
|
|
57
|
+
private val pendingCameraStatusRequests = ConcurrentHashMap<String, PendingResponse<CameraStatusEvent>>()
|
|
58
|
+
private val pendingVideoRecordingRequests =
|
|
59
|
+
ConcurrentHashMap<String, PendingVideoRecordingRequest>()
|
|
60
|
+
private val pendingRgbLedRequests = ConcurrentHashMap<String, PendingResponse<RgbLedControlResponseEvent>>()
|
|
61
|
+
private val pendingSettingsRequests = ConcurrentHashMap<String, PendingResponse<SettingsAckEvent>>()
|
|
62
|
+
private val pendingStreamStarts = ConcurrentHashMap<String, PendingStreamStart>()
|
|
63
|
+
private val streamStartSeq = AtomicLong()
|
|
64
|
+
private val streamStartOrderLock = Any()
|
|
65
|
+
private val oneShotLock = Any()
|
|
66
|
+
private var pendingGalleryStatus: PendingResponse<GalleryStatusEvent>? = null
|
|
67
|
+
private var pendingOtaQuery: PendingResponse<OtaQueryResult>? = null
|
|
68
|
+
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>? = null
|
|
69
|
+
private var pendingStreamStop: PendingStreamStop? = null
|
|
70
|
+
private var pendingWifiScan: PendingWifiScan? = null
|
|
71
|
+
private var pendingWifiStatus: PendingWifiStatusRequest? = null
|
|
72
|
+
private var pendingHotspotStatus: PendingHotspotStatusRequest? = null
|
|
73
|
+
private var pendingVersionInfo: PendingResponse<VersionInfoResult>? = null
|
|
74
|
+
@Volatile private var configuredOtaVersionUrl: String? = null
|
|
75
|
+
|
|
76
|
+
init {
|
|
77
|
+
listeners.add(listener)
|
|
78
|
+
Bridge.initialize(appContext)
|
|
79
|
+
deviceManager = DeviceManager.getInstance()
|
|
80
|
+
(appContext as? Application)?.let { application ->
|
|
81
|
+
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
|
82
|
+
activityLifecycleCallbacksRegistered = true
|
|
83
|
+
}
|
|
84
|
+
bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
|
|
85
|
+
// Baseline the analytics connection state before subscribing to the store:
|
|
86
|
+
// store updates invoke listeners synchronously on the updating thread, so a
|
|
87
|
+
// connected status observed before the baseline would be reported as a fresh
|
|
88
|
+
// bluetooth_sdk_glasses_connected transition.
|
|
89
|
+
analytics.initializeGlassesStatus(getRawGlassesStatus())
|
|
90
|
+
storeListenerId = DeviceStore.store.addListener { category, changes -> dispatchStoreUpdate(category, changes) }
|
|
91
|
+
analytics.captureStarted()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
companion object {
|
|
95
|
+
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address", "project_name")
|
|
96
|
+
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
97
|
+
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
98
|
+
private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
|
|
99
|
+
// A photo response is terminal only after capture, encoding, transport, and upload.
|
|
100
|
+
// Max-quality BLE fallback can legitimately exceed the generic command deadline.
|
|
101
|
+
private const val PHOTO_REQUEST_TIMEOUT_MS = 30_000L
|
|
102
|
+
private const val WIFI_SCAN_TIMEOUT_MS = 20_000L
|
|
103
|
+
private const val VIDEO_UPLOAD_STOP_TIMEOUT_MS = 10 * 60 * 1000L
|
|
104
|
+
private const val STREAM_START_TIMEOUT_MS = 30_000L
|
|
105
|
+
private const val STREAM_STOP_TIMEOUT_MS = 15_000L
|
|
106
|
+
private const val OTA_BES_VERSION_WAIT_MS = 5_000L
|
|
107
|
+
private const val OTA_MTK_VERSION_WAIT_MS = 2_000L
|
|
108
|
+
private const val OTA_VERSION_POLL_MS = 100L
|
|
109
|
+
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
110
|
+
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
111
|
+
|
|
112
|
+
// Stream states the glasses only reach after start_stream has run past
|
|
113
|
+
// stopAllServices() — the point where every older start is actually
|
|
114
|
+
// preempted. ERROR and STOPPED are deliberately absent: an ERROR can be
|
|
115
|
+
// a command-level preflight rejection emitted before stopAllServices,
|
|
116
|
+
// and a STOPPED is a stop ack; neither proves older starts were killed.
|
|
117
|
+
private val PREEMPTION_PROVEN_STATES = setOf(
|
|
118
|
+
StreamState.INITIALIZING,
|
|
119
|
+
StreamState.RECONNECTING,
|
|
120
|
+
StreamState.RECONNECTED,
|
|
121
|
+
StreamState.STREAMING,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@JvmStatic
|
|
125
|
+
fun create(
|
|
126
|
+
context: Context,
|
|
127
|
+
listener: BluetoothSdkListener,
|
|
128
|
+
): BluetoothSdk = create(context, BluetoothSdkConfig(), listener)
|
|
129
|
+
|
|
130
|
+
@JvmStatic
|
|
131
|
+
fun create(
|
|
132
|
+
context: Context,
|
|
133
|
+
config: BluetoothSdkConfig,
|
|
134
|
+
listener: BluetoothSdkListener,
|
|
135
|
+
): BluetoothSdk = BluetoothSdk(context, config, listener)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private data class ActiveStreamKeepAlive(
|
|
139
|
+
val streamId: String,
|
|
140
|
+
val intervalMs: Long,
|
|
141
|
+
var pendingAckId: String? = null,
|
|
142
|
+
var missedAckCount: Int = 0,
|
|
143
|
+
var nextTick: Runnable? = null,
|
|
144
|
+
// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a
|
|
145
|
+
// slow startup (glasses can't ACK until they reach starting/streaming) can't trip a
|
|
146
|
+
// false keep-alive timeout before the stream is ever up.
|
|
147
|
+
var armed: Boolean = false,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// seq records send order (assigned and handed to the BLE queue under
|
|
151
|
+
// streamStartOrderLock) so that an id-carrying status for a newer start can
|
|
152
|
+
// identify which older in-flight starts it preempted.
|
|
153
|
+
private data class PendingStreamStart(
|
|
154
|
+
val seq: Long,
|
|
155
|
+
val pending: PendingResponse<StreamStatusEvent>,
|
|
156
|
+
// Set when an id-carrying error arrives: a fatal publisher error never
|
|
157
|
+
// reaches the reconnect machinery and winds down with a streamId-less
|
|
158
|
+
// stopped, so the stash both attributes that stopped to this start and
|
|
159
|
+
// preserves the real error details for the rejection.
|
|
160
|
+
var lastError: StreamStatusEvent? = null,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
private data class PendingStreamStop(
|
|
164
|
+
val streamId: String?,
|
|
165
|
+
// The stop's slot in the shared streamStartSeq order (drawn inside
|
|
166
|
+
// streamStartOrderLock at send time): every pending start with a
|
|
167
|
+
// lower seq reached the glasses' FIFO before this stop.
|
|
168
|
+
val seq: Long,
|
|
169
|
+
val pending: PendingResponse<StreamStatusEvent>,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
private data class PendingVideoRecordingRequest(
|
|
173
|
+
val expectedStatus: String,
|
|
174
|
+
val pending: PendingResponse<VideoRecordingStatusEvent>,
|
|
175
|
+
val waitForUpload: Boolean = false,
|
|
176
|
+
var stoppedEvent: VideoRecordingStatusEvent? = null,
|
|
177
|
+
var uploadSucceeded: Boolean = false,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
private data class PendingWifiScan(
|
|
181
|
+
val pending: PendingResponse<List<WifiScanResult>>,
|
|
182
|
+
val scanId: String,
|
|
183
|
+
var latestResults: List<WifiScanResult> = emptyList(),
|
|
184
|
+
var waiters: Int = 0,
|
|
185
|
+
// Chunks accumulated from scanId-echoing glasses, deduplicated by SSID;
|
|
186
|
+
// resolved only when the glasses flag the scan complete.
|
|
187
|
+
val accumulated: MutableList<WifiScanResult> = mutableListOf(),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
private data class PendingWifiStatusRequest(
|
|
191
|
+
val operation: WifiStatusOperation,
|
|
192
|
+
val ssid: String,
|
|
193
|
+
val pending: PendingResponse<WifiStatusEvent>,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
private enum class WifiStatusOperation {
|
|
197
|
+
CONNECT,
|
|
198
|
+
FORGET,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private data class PendingHotspotStatusRequest(
|
|
202
|
+
val enabled: Boolean,
|
|
203
|
+
val pending: PendingResponse<HotspotStatusEvent>,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Suspends instead of parking a thread (OS-1714): the old CountDownLatch
|
|
207
|
+
// version blocked its caller for the whole glasses round-trip. Expo runs
|
|
208
|
+
// every plain AsyncFunction of every module on ONE shared thread
|
|
209
|
+
// (expo.modules.AsyncFunctionQueue), so a blocked await here froze all
|
|
210
|
+
// miniapp event delivery (Crust.jsDispatchToJs) until the glasses
|
|
211
|
+
// responded. resolve()/reject() stay callable from any thread.
|
|
212
|
+
private class PendingResponse<T>(
|
|
213
|
+
private val operation: String,
|
|
214
|
+
) {
|
|
215
|
+
private val deferred = CompletableDeferred<T>()
|
|
216
|
+
|
|
217
|
+
fun resolve(value: T) {
|
|
218
|
+
deferred.complete(value)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun reject(error: Throwable) {
|
|
222
|
+
deferred.completeExceptionally(error)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
suspend fun await(timeoutMs: Long = DEFAULT_REQUEST_TIMEOUT_MS): T {
|
|
226
|
+
// All response types are non-null, so a null from withTimeoutOrNull
|
|
227
|
+
// can only mean the deadline elapsed. A rejected deferred rethrows
|
|
228
|
+
// its error from await() and propagates out unchanged.
|
|
229
|
+
return withTimeoutOrNull(timeoutMs) { deferred.await() }
|
|
230
|
+
?: throw BluetoothSdkException(
|
|
231
|
+
"request_timeout",
|
|
232
|
+
"$operation timed out waiting for glasses response.",
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fun addListener(listener: BluetoothSdkListener) {
|
|
238
|
+
listeners.add(listener)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fun removeListener(listener: BluetoothSdkListener) {
|
|
242
|
+
listeners.remove(listener)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fun getState(): BluetoothState =
|
|
246
|
+
BluetoothState.from(getRawGlassesStatus(), getRawBluetoothStatus())
|
|
247
|
+
|
|
248
|
+
fun getGlasses(): GlassesRuntimeState =
|
|
249
|
+
getState().glasses
|
|
250
|
+
|
|
251
|
+
fun getSdkState(): PhoneSdkRuntimeState =
|
|
252
|
+
getState().sdk
|
|
253
|
+
|
|
254
|
+
fun getScanState(): BluetoothScanState =
|
|
255
|
+
getState().scan
|
|
256
|
+
|
|
257
|
+
internal fun getRawGlassesStatus(): GlassesStatus =
|
|
258
|
+
GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses"))
|
|
259
|
+
|
|
260
|
+
internal fun getRawBluetoothStatus(): BluetoothStatus =
|
|
261
|
+
BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
|
|
262
|
+
|
|
263
|
+
fun getDefaultDevice(): Device? = currentDefaultDevice()
|
|
264
|
+
|
|
265
|
+
private fun requireGlassesConnected(operation: String) {
|
|
266
|
+
if (!getRawGlassesStatus().connected) {
|
|
267
|
+
throw BluetoothSdkException(
|
|
268
|
+
"glasses_not_connected",
|
|
269
|
+
"Cannot $operation because glasses are not connected.",
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fun setDefaultDevice(device: Device?) {
|
|
275
|
+
if (device == null) {
|
|
276
|
+
clearDefaultDevice()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
suppressDefaultDeviceEvents = true
|
|
280
|
+
try {
|
|
281
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "default_wearable", device.model.deviceType)
|
|
282
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", device.name)
|
|
283
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", device.address ?: "")
|
|
284
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", device.projectName ?: "")
|
|
285
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
286
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
287
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
288
|
+
} finally {
|
|
289
|
+
suppressDefaultDeviceEvents = false
|
|
290
|
+
}
|
|
291
|
+
dispatchDefaultDeviceChanged()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fun clearDefaultDevice() {
|
|
295
|
+
suppressDefaultDeviceEvents = true
|
|
296
|
+
try {
|
|
297
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "default_wearable", "")
|
|
298
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", "")
|
|
299
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", "")
|
|
300
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", "")
|
|
301
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
302
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
303
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
304
|
+
} finally {
|
|
305
|
+
suppressDefaultDeviceEvents = false
|
|
306
|
+
}
|
|
307
|
+
dispatchDefaultDeviceChanged()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fun startScan(model: DeviceModel) {
|
|
311
|
+
if (model != DeviceModel.SIMULATED) {
|
|
312
|
+
requireBluetoothReady("scan for glasses")
|
|
313
|
+
}
|
|
314
|
+
discoveredDeviceNames.clear()
|
|
315
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "searching", true)
|
|
316
|
+
deviceManager.findCompatibleDevices(model.deviceType)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun stopScan() {
|
|
320
|
+
stopScan(ScanStopReason.CANCELLED)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private fun stopScan(reason: ScanStopReason) {
|
|
324
|
+
deviceManager.stopScan()
|
|
325
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "searching", false)
|
|
326
|
+
dispatchToListeners { it.onScanStopped(reason) }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fun scan(
|
|
330
|
+
model: DeviceModel,
|
|
331
|
+
onResults: (List<Device>) -> Unit,
|
|
332
|
+
): ScanSession = scan(model, DEFAULT_SCAN_TIMEOUT_MS, onResults)
|
|
333
|
+
|
|
334
|
+
fun scan(
|
|
335
|
+
model: DeviceModel,
|
|
336
|
+
timeoutMs: Long,
|
|
337
|
+
onResults: (List<Device>) -> Unit,
|
|
338
|
+
): ScanSession =
|
|
339
|
+
scan(
|
|
340
|
+
model = model,
|
|
341
|
+
callback =
|
|
342
|
+
object : BluetoothScanCallback() {
|
|
343
|
+
override fun onResults(devices: List<Device>) {
|
|
344
|
+
onResults(devices)
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
timeoutMs = timeoutMs,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@JvmOverloads
|
|
351
|
+
fun scan(
|
|
352
|
+
model: DeviceModel,
|
|
353
|
+
callback: ScanCallback,
|
|
354
|
+
timeoutMs: Long = DEFAULT_SCAN_TIMEOUT_MS,
|
|
355
|
+
): ScanSession {
|
|
356
|
+
val normalizedTimeoutMs = if (timeoutMs > 0) timeoutMs else DEFAULT_SCAN_TIMEOUT_MS
|
|
357
|
+
val latestResults = mutableListOf<Device>()
|
|
358
|
+
lateinit var timeoutRunnable: Runnable
|
|
359
|
+
lateinit var session: ScanSession
|
|
360
|
+
val finished = AtomicBoolean(false)
|
|
361
|
+
|
|
362
|
+
fun emitResults(devices: List<Device>) {
|
|
363
|
+
latestResults.clear()
|
|
364
|
+
latestResults.addAll(devices)
|
|
365
|
+
callback.onResults(latestResults.toList())
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
val scanListener =
|
|
369
|
+
object : BluetoothSdkCallback() {
|
|
370
|
+
override fun onScanChanged(scan: BluetoothScanState) {
|
|
371
|
+
emitResults(scan.devices.filter { it.model == model })
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fun finish(reason: ScanStopReason) {
|
|
376
|
+
if (!finished.compareAndSet(false, true)) return
|
|
377
|
+
removeListener(scanListener)
|
|
378
|
+
mainHandler.removeCallbacks(timeoutRunnable)
|
|
379
|
+
session.markStopped()
|
|
380
|
+
stopScan(reason)
|
|
381
|
+
callback.onComplete(latestResults.toList())
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
timeoutRunnable = Runnable { finish(ScanStopReason.COMPLETED) }
|
|
385
|
+
session = ScanSession { finish(ScanStopReason.CANCELLED) }
|
|
386
|
+
addListener(scanListener)
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
emitResults(emptyList())
|
|
390
|
+
startScan(model)
|
|
391
|
+
emitResults(getRawBluetoothStatus().searchResults.filter { it.model == model })
|
|
392
|
+
mainHandler.postDelayed(timeoutRunnable, normalizedTimeoutMs)
|
|
393
|
+
return session
|
|
394
|
+
} catch (error: Throwable) {
|
|
395
|
+
removeListener(scanListener)
|
|
396
|
+
mainHandler.removeCallbacks(timeoutRunnable)
|
|
397
|
+
session.markStopped()
|
|
398
|
+
callback.onError(error.toBluetoothError("scan_failed"))
|
|
399
|
+
throw error
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@JvmOverloads
|
|
404
|
+
fun connect(device: Device, options: ConnectOptions = ConnectOptions()) {
|
|
405
|
+
if (device.model != DeviceModel.SIMULATED) {
|
|
406
|
+
requireBluetoothReady("connect to glasses")
|
|
407
|
+
}
|
|
408
|
+
val isController = ControllerTypes.ALL.contains(device.model.deviceType)
|
|
409
|
+
if (options.cancelExistingConnectionAttempt) {
|
|
410
|
+
if (isController) {
|
|
411
|
+
deviceManager.disconnectController()
|
|
412
|
+
} else {
|
|
413
|
+
cancelConnectionAttempt()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!isController) {
|
|
417
|
+
if (options.saveAsDefault) {
|
|
418
|
+
setDefaultDevice(device)
|
|
419
|
+
} else {
|
|
420
|
+
// Pairing uses saveAsDefault=false so default identity stays on the
|
|
421
|
+
// previous owner until handleDeviceReady. Stash the GATT target separately
|
|
422
|
+
// so MentraLive can connect by MAC without promoting getDefaultDevice().
|
|
423
|
+
DeviceStore.apply(
|
|
424
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
425
|
+
"pending_device_name",
|
|
426
|
+
device.name,
|
|
427
|
+
)
|
|
428
|
+
DeviceStore.apply(
|
|
429
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
430
|
+
"pending_device_address",
|
|
431
|
+
device.address ?: "",
|
|
432
|
+
)
|
|
433
|
+
DeviceStore.apply(
|
|
434
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
435
|
+
"pending_device_secure_pairing_capable",
|
|
436
|
+
device.securePairingCapable ?: "",
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_wearable", device.model.deviceType)
|
|
441
|
+
deviceManager.connectByName(device.name)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@JvmOverloads
|
|
445
|
+
fun connectDefault(options: ConnectOptions = ConnectOptions()) {
|
|
446
|
+
val defaultDevice =
|
|
447
|
+
currentDefaultDevice()
|
|
448
|
+
?: throw BluetoothSdkException(
|
|
449
|
+
"default_device_missing",
|
|
450
|
+
"Set a default glasses device before calling connectDefault.",
|
|
451
|
+
)
|
|
452
|
+
if (defaultDevice.model != DeviceModel.SIMULATED) {
|
|
453
|
+
requireBluetoothReady("connect to glasses")
|
|
454
|
+
}
|
|
455
|
+
if (options.cancelExistingConnectionAttempt) {
|
|
456
|
+
cancelConnectionAttempt()
|
|
457
|
+
}
|
|
458
|
+
deviceManager.connectDefault()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fun cancelConnectionAttempt() {
|
|
462
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
463
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
464
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
465
|
+
deviceManager.disconnect()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
internal fun connectSimulated() {
|
|
469
|
+
deviceManager.connectSimulated()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
fun disconnect() {
|
|
473
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
474
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
475
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
476
|
+
deviceManager.disconnect()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fun forget() {
|
|
480
|
+
deviceManager.forget()
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@JvmOverloads
|
|
484
|
+
fun displayText(text: String, x: Int = 0, y: Int = 0, size: Int = 24) {
|
|
485
|
+
displayText(DisplayTextRequest(text = text, x = x, y = y, size = size))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fun displayText(request: DisplayTextRequest) {
|
|
489
|
+
deviceManager.displayText(request.toMap())
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
internal fun displayEvent(request: DisplayEventRequest) {
|
|
493
|
+
deviceManager.displayEvent(request.toMap())
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fun clearDisplay() {
|
|
497
|
+
deviceManager.clearDisplay()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fun showDashboard() {
|
|
501
|
+
deviceManager.showDashboard()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fun toggleHeadUp() {
|
|
505
|
+
deviceManager.headUp = !deviceManager.headUp
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fun showNotificationsPanel() {
|
|
509
|
+
deviceManager.showNotificationsPanel()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---------- Native navigation (glasses-rendered nav UI) ----------
|
|
513
|
+
|
|
514
|
+
fun navigationModeStart() {
|
|
515
|
+
deviceManager.navigationModeStart()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fun navigationModeStop(arrived: Boolean) {
|
|
519
|
+
deviceManager.navigationModeStop(arrived)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fun navigationUpdateInfo(params: Map<String, Any?>) {
|
|
523
|
+
deviceManager.navigationUpdateInfo(params)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
fun navigationMiniMap(params: Map<String, Any?>) {
|
|
527
|
+
deviceManager.navigationMiniMap(params)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
fun navigationOverviewMap(params: Map<String, Any?>) {
|
|
531
|
+
deviceManager.navigationOverviewMap(params)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
internal fun setBrightness(level: Int, autoMode: Boolean? = null) {
|
|
535
|
+
autoMode?.let { DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "auto_brightness", it) }
|
|
536
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "brightness", level)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
internal fun setAutoBrightness(enabled: Boolean) {
|
|
540
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "auto_brightness", enabled)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
fun setDashboardPosition(height: Int, depth: Int) {
|
|
544
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "dashboard_height", height)
|
|
545
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "dashboard_depth", depth)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
fun setDashboardPosition(request: DashboardPositionRequest) {
|
|
549
|
+
setDashboardPosition(height = request.height, depth = request.depth)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
internal fun setDashboardMenu(items: List<DashboardMenuItem>) {
|
|
553
|
+
DeviceStore.apply(
|
|
554
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
555
|
+
"menu_apps",
|
|
556
|
+
items.map { it.toMap() },
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
internal fun setCalendarEvents(events: List<CalendarEvent>) {
|
|
561
|
+
DeviceStore.apply(
|
|
562
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
563
|
+
"calendar_events",
|
|
564
|
+
events.map { it.toMap() },
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
fun setHeadUpAngle(angleDegrees: Int) {
|
|
569
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "head_up_angle", angleDegrees)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
fun setScreenDisabled(disabled: Boolean) {
|
|
573
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "screen_disabled", disabled)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
suspend fun setGalleryModeEnabled(enabled: Boolean): SettingsAckEvent =
|
|
577
|
+
performSettingsCommand(
|
|
578
|
+
setting = "gallery_mode",
|
|
579
|
+
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "gallery_mode", enabled) },
|
|
580
|
+
send = { requestId -> deviceManager.sendGalleryMode(requestId, enabled) },
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
private suspend fun performSettingsCommand(
|
|
584
|
+
setting: String,
|
|
585
|
+
updateStore: (SettingsAckEvent) -> Unit,
|
|
586
|
+
send: (String) -> Unit,
|
|
587
|
+
): SettingsAckEvent {
|
|
588
|
+
val requestId = "settings-$setting-${UUID.randomUUID()}"
|
|
589
|
+
val pending = PendingResponse<SettingsAckEvent>("set $setting")
|
|
590
|
+
pendingSettingsRequests[requestId] = pending
|
|
591
|
+
try {
|
|
592
|
+
send(requestId)
|
|
593
|
+
val ack = pending.await()
|
|
594
|
+
updateStore(ack)
|
|
595
|
+
return ack
|
|
596
|
+
} finally {
|
|
597
|
+
pendingSettingsRequests.remove(requestId, pending)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
fun setVoiceActivityDetectionEnabled(enabled: Boolean) {
|
|
602
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fun setLoudnessGateEnabled(enabled: Boolean) {
|
|
606
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "loudness_gate_enabled", enabled)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
@Deprecated(
|
|
610
|
+
message =
|
|
611
|
+
"Sticky action-button photo presets are deprecated. Prefer per-request " +
|
|
612
|
+
"requestPhoto(...) options (e.g. mode=TEXT for text sensor size/crop, or explicit per-shot fields). " +
|
|
613
|
+
"This method still works but will be removed in a future release.",
|
|
614
|
+
)
|
|
615
|
+
suspend fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
|
|
616
|
+
performSettingsCommand(
|
|
617
|
+
setting = "button_photo",
|
|
618
|
+
updateStore = { _ ->
|
|
619
|
+
if (settings.resetCaptureTuning) {
|
|
620
|
+
// Clear all stored scan-tuning keys from phone cache
|
|
621
|
+
listOf(
|
|
622
|
+
"button_photo_zsl_mfnr",
|
|
623
|
+
"button_photo_mfnr",
|
|
624
|
+
"button_photo_zsl",
|
|
625
|
+
"button_photo_noise_reduction",
|
|
626
|
+
"button_photo_edge_enhancement",
|
|
627
|
+
"button_photo_isp_digital_gain",
|
|
628
|
+
"button_photo_isp_analog_gain",
|
|
629
|
+
"button_photo_ae_exposure_divisor",
|
|
630
|
+
"button_photo_iso_cap",
|
|
631
|
+
"button_photo_compress",
|
|
632
|
+
"button_photo_sound",
|
|
633
|
+
).forEach { key ->
|
|
634
|
+
DeviceStore.store.remove(ObservableStore.BLUETOOTH_CATEGORY, key)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Only update size if explicitly provided; omitted size = leave stored value unchanged
|
|
638
|
+
settings.size?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_size", it.value) }
|
|
639
|
+
settings.mfnr?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_mfnr", it) }
|
|
640
|
+
settings.zsl?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_zsl", it) }
|
|
641
|
+
settings.noiseReduction?.let {
|
|
642
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_noise_reduction", it)
|
|
643
|
+
}
|
|
644
|
+
settings.edgeEnhancement?.let {
|
|
645
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_edge_enhancement", it)
|
|
646
|
+
}
|
|
647
|
+
settings.ispDigitalGain?.let {
|
|
648
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_isp_digital_gain", it)
|
|
649
|
+
}
|
|
650
|
+
settings.ispAnalogGain?.let {
|
|
651
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_isp_analog_gain", it)
|
|
652
|
+
}
|
|
653
|
+
settings.aeExposureDivisor?.let {
|
|
654
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_ae_exposure_divisor", it)
|
|
655
|
+
}
|
|
656
|
+
settings.isoCap?.let {
|
|
657
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_iso_cap", it)
|
|
658
|
+
}
|
|
659
|
+
settings.compress?.let {
|
|
660
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_compress", it)
|
|
661
|
+
}
|
|
662
|
+
settings.sound?.let {
|
|
663
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_sound", it)
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
send = { requestId -> deviceManager.sendButtonPhotoSettings(requestId, settings) },
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
suspend fun setVideoRecordingDefaults(defaults: VideoRecordingDefaults): SettingsAckEvent =
|
|
670
|
+
performSettingsCommand(
|
|
671
|
+
setting = "button_video_recording",
|
|
672
|
+
updateStore = { _ ->
|
|
673
|
+
DeviceStore.set(
|
|
674
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
675
|
+
"button_video_settings",
|
|
676
|
+
mapOf("width" to defaults.width, "height" to defaults.height, "fps" to defaults.fps),
|
|
677
|
+
)
|
|
678
|
+
// Keep legacy cache keys readable for older internal callers during migration.
|
|
679
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", defaults.width)
|
|
680
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", defaults.height)
|
|
681
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", defaults.fps)
|
|
682
|
+
},
|
|
683
|
+
send = { requestId ->
|
|
684
|
+
deviceManager.sendButtonVideoRecordingSettings(requestId, defaults.width, defaults.height, defaults.fps)
|
|
685
|
+
},
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
suspend fun setMaxVideoRecordingDuration(minutes: Int): SettingsAckEvent =
|
|
689
|
+
performSettingsCommand(
|
|
690
|
+
setting = "button_max_recording_time",
|
|
691
|
+
updateStore = { _ ->
|
|
692
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_max_recording_time", minutes)
|
|
693
|
+
},
|
|
694
|
+
send = { requestId -> deviceManager.sendButtonMaxRecordingTime(requestId, minutes) },
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
suspend fun setCameraFov(fov: CameraFov): CameraFovResult {
|
|
698
|
+
val ack = performSettingsCommand(
|
|
699
|
+
setting = "camera_fov",
|
|
700
|
+
updateStore = { _ -> },
|
|
701
|
+
send = { requestId -> deviceManager.sendCameraFovSetting(requestId, fov.fov, fov.roiPosition.value) },
|
|
702
|
+
)
|
|
703
|
+
val result = CameraFovResult.fromAck(ack, fov)
|
|
704
|
+
DeviceStore.set(
|
|
705
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
706
|
+
"camera_fov",
|
|
707
|
+
mapOf("fov" to result.fov, "roi_position" to result.roiPosition.value),
|
|
708
|
+
)
|
|
709
|
+
return result
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Compatibility path for ASG clients that support only the older one-way FOV command.
|
|
714
|
+
* It resolves once the setting is queued to BLE because those clients never acknowledge it.
|
|
715
|
+
*/
|
|
716
|
+
fun setLegacyCameraFov(fov: CameraFov): CameraFovResult {
|
|
717
|
+
deviceManager.sendLegacyCameraFovSetting(fov.fov, fov.roiPosition.value)
|
|
718
|
+
return CameraFovResult("legacy", fov.fov, fov.roiPosition, System.currentTimeMillis())
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Restores the persistent base FOV through the acknowledgement-free compatibility path. */
|
|
722
|
+
fun restoreLegacyCameraFov() {
|
|
723
|
+
deviceManager.restoreLegacyCameraFovSetting()
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
suspend fun setCameraFovOverride(leaseId: String, fov: CameraFov, ttlMs: Int): CameraFovResult {
|
|
727
|
+
val ack = performSettingsCommand(
|
|
728
|
+
setting = "camera_fov_override",
|
|
729
|
+
updateStore = { _ -> },
|
|
730
|
+
send = { requestId ->
|
|
731
|
+
deviceManager.sendCameraFovOverride(
|
|
732
|
+
requestId,
|
|
733
|
+
leaseId,
|
|
734
|
+
fov.fov,
|
|
735
|
+
fov.roiPosition.value,
|
|
736
|
+
ttlMs,
|
|
737
|
+
)
|
|
738
|
+
},
|
|
739
|
+
)
|
|
740
|
+
return CameraFovResult.fromAck(ack, fov)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
suspend fun releaseCameraFovOverride(leaseId: String): SettingsAckEvent =
|
|
744
|
+
performSettingsCommand(
|
|
745
|
+
setting = "camera_fov_override",
|
|
746
|
+
updateStore = { _ -> },
|
|
747
|
+
send = { requestId -> deviceManager.releaseCameraFovOverride(requestId, leaseId) },
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Configure camera HAL tuning on Mentra Live glasses.
|
|
752
|
+
*
|
|
753
|
+
* The phone sends a {@code camera_tuning_config} BLE message; the ASG client relays it as a
|
|
754
|
+
* {@code camconfig} broadcast to SystemUI's CTReceiver so the camera HAL picks up the new
|
|
755
|
+
* parameters without a reboot.
|
|
756
|
+
*
|
|
757
|
+
* @param anrOn {@code true} = ANR enabled, {@code false} = ANR disabled (pixsmart param)
|
|
758
|
+
* @param gainOn {@code true} = stock gain params, {@code false} = pixsmart gain-off params
|
|
759
|
+
*/
|
|
760
|
+
suspend fun setCameraTuningConfig(anrOn: Boolean, gainOn: Boolean): SettingsAckEvent =
|
|
761
|
+
performSettingsCommand(
|
|
762
|
+
setting = "camera_tuning",
|
|
763
|
+
updateStore = { _ -> },
|
|
764
|
+
send = { requestId -> deviceManager.sendCameraTuningConfig(requestId, anrOn, gainOn) },
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
fun setMicState(
|
|
768
|
+
enabled: Boolean,
|
|
769
|
+
useGlassesMic: Boolean = true,
|
|
770
|
+
sendTranscript: Boolean = false,
|
|
771
|
+
sendLc3Data: Boolean = false,
|
|
772
|
+
) {
|
|
773
|
+
if (enabled) {
|
|
774
|
+
DeviceStore.apply(
|
|
775
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
776
|
+
"preferred_mic",
|
|
777
|
+
if (useGlassesMic) MicPreference.GLASSES.value else MicPreference.PHONE.value,
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
applyMicState(
|
|
781
|
+
sendPcmData = enabled,
|
|
782
|
+
sendTranscript = enabled && sendTranscript,
|
|
783
|
+
sendLc3Data = enabled && sendLc3Data,
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private fun applyMicState(
|
|
788
|
+
sendPcmData: Boolean,
|
|
789
|
+
sendTranscript: Boolean,
|
|
790
|
+
sendLc3Data: Boolean,
|
|
791
|
+
) {
|
|
792
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_pcm", sendPcmData)
|
|
793
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_lc3", sendLc3Data)
|
|
794
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_transcript", sendTranscript)
|
|
795
|
+
deviceManager.setMicState()
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
fun setPreferredMic(preferredMic: MicPreference) {
|
|
799
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "preferred_mic", preferredMic.value)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
fun setOwnAppAudioPlaying(playing: Boolean) {
|
|
803
|
+
PhoneAudioMonitor.getInstance(appContext).setOwnAppAudioPlaying(playing)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fun getGlassesMediaVolume(): GlassesMediaVolumeGetResult =
|
|
807
|
+
GlassesMediaVolumeGetResult.fromMap(deviceManager.getGlassesMediaVolumeBlocking())
|
|
808
|
+
|
|
809
|
+
fun setGlassesMediaVolume(level: Int): GlassesMediaVolumeSetResult {
|
|
810
|
+
require(level in 0..15) { "Glasses media volume must be between 0 and 15." }
|
|
811
|
+
return GlassesMediaVolumeSetResult.fromMap(deviceManager.setGlassesMediaVolumeBlocking(level))
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
suspend fun requestWifiScan(): List<WifiScanResult> {
|
|
815
|
+
val pending = PendingResponse<List<WifiScanResult>>("WiFi scan request")
|
|
816
|
+
val request = PendingWifiScan(pending, scanId = "scan-${UUID.randomUUID()}", waiters = 1)
|
|
817
|
+
val existing =
|
|
818
|
+
synchronized(oneShotLock) {
|
|
819
|
+
pendingWifiScan?.also { it.waiters++ } ?: run {
|
|
820
|
+
pendingWifiScan = request
|
|
821
|
+
null
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (existing != null) {
|
|
825
|
+
// Join the in-flight scan instead of failing with request_in_flight;
|
|
826
|
+
// the scan screen auto-starts a scan on mount and can be pushed twice.
|
|
827
|
+
try {
|
|
828
|
+
return existing.pending.await(WIFI_SCAN_TIMEOUT_MS)
|
|
829
|
+
} finally {
|
|
830
|
+
releaseWifiScanWaiter(existing)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
// Claim the scan-results store for this scan before the command goes
|
|
835
|
+
// out, so a delayed chunk from an older, abandoned scan can no longer
|
|
836
|
+
// mutate it.
|
|
837
|
+
Bridge.claimWifiScanResults(request.scanId)
|
|
838
|
+
deviceManager.requestWifiScan(request.scanId)
|
|
839
|
+
// The glasses wait up to 15s for scan-results broadcasts before sending
|
|
840
|
+
// scan_complete, so give them longer than that before falling back.
|
|
841
|
+
return pending.await(WIFI_SCAN_TIMEOUT_MS)
|
|
842
|
+
} catch (cancellation: CancellationException) {
|
|
843
|
+
// Only this caller is going away; the glasses scan keeps running and
|
|
844
|
+
// other coroutines may have joined the same pending entry. Leave the
|
|
845
|
+
// shared deferred alone so scan_complete can still resolve for them.
|
|
846
|
+
throw cancellation
|
|
847
|
+
} catch (error: Throwable) {
|
|
848
|
+
if (error is BluetoothSdkException && error.code == "request_timeout") {
|
|
849
|
+
val fallbackResults = synchronized(oneShotLock) { request.latestResults }
|
|
850
|
+
if (fallbackResults.isNotEmpty()) {
|
|
851
|
+
// Resolve so callers joined on the same scan get the fallback too.
|
|
852
|
+
pending.resolve(fallbackResults)
|
|
853
|
+
return fallbackResults
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Fail joined callers immediately instead of letting them run out their
|
|
857
|
+
// own timeout.
|
|
858
|
+
pending.reject(error)
|
|
859
|
+
throw error
|
|
860
|
+
} finally {
|
|
861
|
+
releaseWifiScanWaiter(request)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Last waiter (starter or joiner) out clears the shared scan entry so a
|
|
866
|
+
* cancelled starter can't strand it, and an abandoned scan can't wedge
|
|
867
|
+
* future ones. handleWifiScanResultsForRequests may clear it first. */
|
|
868
|
+
private fun releaseWifiScanWaiter(request: PendingWifiScan) {
|
|
869
|
+
synchronized(oneShotLock) {
|
|
870
|
+
request.waiters--
|
|
871
|
+
if (request.waiters <= 0 && pendingWifiScan === request) {
|
|
872
|
+
pendingWifiScan = null
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
suspend fun sendWifiCredentials(ssid: String, password: String): WifiStatusEvent {
|
|
878
|
+
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
879
|
+
synchronized(oneShotLock) {
|
|
880
|
+
if (pendingWifiStatus != null) {
|
|
881
|
+
throw BluetoothSdkException(
|
|
882
|
+
"request_in_flight",
|
|
883
|
+
"A WiFi status command is already waiting for a glasses response.",
|
|
884
|
+
)
|
|
885
|
+
}
|
|
886
|
+
pendingWifiStatus = PendingWifiStatusRequest(WifiStatusOperation.CONNECT, ssid, pending)
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
deviceManager.sendWifiCredentials(ssid, password)
|
|
890
|
+
return pending.await()
|
|
891
|
+
} finally {
|
|
892
|
+
synchronized(oneShotLock) {
|
|
893
|
+
if (pendingWifiStatus?.pending === pending) {
|
|
894
|
+
pendingWifiStatus = null
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
suspend fun forgetWifiNetwork(ssid: String): WifiStatusEvent {
|
|
901
|
+
val pending = PendingResponse<WifiStatusEvent>("WiFi forget request")
|
|
902
|
+
synchronized(oneShotLock) {
|
|
903
|
+
if (pendingWifiStatus != null) {
|
|
904
|
+
throw BluetoothSdkException(
|
|
905
|
+
"request_in_flight",
|
|
906
|
+
"A WiFi status command is already waiting for a glasses response.",
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
pendingWifiStatus = PendingWifiStatusRequest(WifiStatusOperation.FORGET, ssid, pending)
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
deviceManager.forgetWifiNetwork(ssid)
|
|
913
|
+
return pending.await()
|
|
914
|
+
} finally {
|
|
915
|
+
synchronized(oneShotLock) {
|
|
916
|
+
if (pendingWifiStatus?.pending === pending) {
|
|
917
|
+
pendingWifiStatus = null
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
suspend fun setHotspotState(enabled: Boolean): HotspotStatusEvent {
|
|
924
|
+
val pending = PendingResponse<HotspotStatusEvent>("hotspot ${if (enabled) "enable" else "disable"} request")
|
|
925
|
+
synchronized(oneShotLock) {
|
|
926
|
+
if (pendingHotspotStatus != null) {
|
|
927
|
+
throw BluetoothSdkException(
|
|
928
|
+
"request_in_flight",
|
|
929
|
+
"A hotspot command is already waiting for a glasses response.",
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
pendingHotspotStatus = PendingHotspotStatusRequest(enabled, pending)
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
deviceManager.setHotspotState(enabled)
|
|
936
|
+
return pending.await()
|
|
937
|
+
} finally {
|
|
938
|
+
synchronized(oneShotLock) {
|
|
939
|
+
if (pendingHotspotStatus?.pending === pending) {
|
|
940
|
+
pendingHotspotStatus = null
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
fun setWifiAdbState(enabled: Boolean) {
|
|
947
|
+
deviceManager.setWifiAdbState(enabled)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
fun setSystemTime(timestampMs: Long) {
|
|
951
|
+
deviceManager.setSystemTime(timestampMs)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
suspend fun requestPhoto(request: PhotoRequest): PhotoResponseEvent {
|
|
955
|
+
val routedRequest =
|
|
956
|
+
nonBlankRequestId(request.requestId)
|
|
957
|
+
?.let { if (it == request.requestId) request else request.copy(requestId = it) }
|
|
958
|
+
?: request.copy(requestId = generatedCameraRequestId("photo"))
|
|
959
|
+
Bridge.log(
|
|
960
|
+
"NATIVE: PHOTO PIPELINE [3b/6] BluetoothSdk.requestPhoto requestId=${routedRequest.requestId}"
|
|
961
|
+
)
|
|
962
|
+
val pending = PendingResponse<PhotoResponseEvent>("photo request ${routedRequest.requestId}")
|
|
963
|
+
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
964
|
+
try {
|
|
965
|
+
deviceManager.requestPhoto(routedRequest)
|
|
966
|
+
return pending.await(PHOTO_REQUEST_TIMEOUT_MS)
|
|
967
|
+
} finally {
|
|
968
|
+
pendingPhotoRequests.remove(routedRequest.requestId, pending)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
suspend fun warmUpCamera(
|
|
973
|
+
requestId: String? = null,
|
|
974
|
+
size: PhotoSize,
|
|
975
|
+
mode: PhotoMode = PhotoMode.PHOTO,
|
|
976
|
+
exposureTimeNs: Long?,
|
|
977
|
+
durationMs: Int,
|
|
978
|
+
zsl: Boolean? = null,
|
|
979
|
+
mfnr: Boolean? = null,
|
|
980
|
+
): CameraStatusEvent {
|
|
981
|
+
val effectiveRequestId = nonBlankRequestId(requestId) ?: generatedCameraRequestId("warm")
|
|
982
|
+
val pending = PendingResponse<CameraStatusEvent>("camera warm up $effectiveRequestId")
|
|
983
|
+
// Guard against a concurrent warm-up reusing the same requestId: a blind put would overwrite
|
|
984
|
+
// (and strand) the first caller's continuation until it times out. Reject the duplicate.
|
|
985
|
+
if (pendingCameraStatusRequests.putIfAbsent(effectiveRequestId, pending) != null) {
|
|
986
|
+
throw BluetoothSdkException(
|
|
987
|
+
"request_in_flight",
|
|
988
|
+
"A camera warm-up request with this requestId is already waiting for a glasses response.",
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
deviceManager.warmUpCamera(effectiveRequestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr)
|
|
993
|
+
return pending.await()
|
|
994
|
+
} finally {
|
|
995
|
+
pendingCameraStatusRequests.remove(effectiveRequestId, pending)
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
fun stopCameraWarmUp(requestId: String) {
|
|
1000
|
+
val effectiveRequestId = nonBlankRequestId(requestId)
|
|
1001
|
+
?: throw BluetoothSdkException("invalid_request", "A warm-up request ID is required.")
|
|
1002
|
+
deviceManager.stopCameraWarmUp(effectiveRequestId)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
suspend fun queryGalleryStatus(): GalleryStatusEvent {
|
|
1006
|
+
val pending = PendingResponse<GalleryStatusEvent>("gallery status query")
|
|
1007
|
+
synchronized(oneShotLock) {
|
|
1008
|
+
if (pendingGalleryStatus != null) {
|
|
1009
|
+
throw BluetoothSdkException(
|
|
1010
|
+
"request_in_flight",
|
|
1011
|
+
"A gallery status query is already waiting for a glasses response.",
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
pendingGalleryStatus = pending
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
deviceManager.queryGalleryStatus()
|
|
1018
|
+
return pending.await()
|
|
1019
|
+
} finally {
|
|
1020
|
+
synchronized(oneShotLock) {
|
|
1021
|
+
if (pendingGalleryStatus === pending) {
|
|
1022
|
+
pendingGalleryStatus = null
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
suspend fun startStream(request: StreamRequest): StreamStatusEvent =
|
|
1029
|
+
startStream(request, startSdkKeepAlive = true)
|
|
1030
|
+
|
|
1031
|
+
internal suspend fun startExternallyManagedStream(request: StreamRequest): StreamStatusEvent =
|
|
1032
|
+
startStream(request, startSdkKeepAlive = false)
|
|
1033
|
+
|
|
1034
|
+
private suspend fun startStream(
|
|
1035
|
+
request: StreamRequest,
|
|
1036
|
+
startSdkKeepAlive: Boolean,
|
|
1037
|
+
): StreamStatusEvent {
|
|
1038
|
+
val message = request.toMap().toMutableMap()
|
|
1039
|
+
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
1040
|
+
?: "sdk-${UUID.randomUUID()}"
|
|
1041
|
+
message["streamId"] = streamId
|
|
1042
|
+
val pending = PendingResponse<StreamStatusEvent>("start stream $streamId")
|
|
1043
|
+
var start: PendingStreamStart? = null
|
|
1044
|
+
try {
|
|
1045
|
+
// seq assignment and the BLE hand-off must be one critical section:
|
|
1046
|
+
// rejectPreemptedStreamStarts only works if seq order equals the
|
|
1047
|
+
// order the commands reach the glasses' FIFO.
|
|
1048
|
+
synchronized(streamStartOrderLock) {
|
|
1049
|
+
val registered = PendingStreamStart(streamStartSeq.incrementAndGet(), pending)
|
|
1050
|
+
pendingStreamStarts[streamId] = registered
|
|
1051
|
+
start = registered
|
|
1052
|
+
stopStreamKeepAliveMonitor()
|
|
1053
|
+
deviceManager.startStream(message)
|
|
1054
|
+
}
|
|
1055
|
+
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
1056
|
+
if (startSdkKeepAlive) {
|
|
1057
|
+
startStreamKeepAliveMonitor(streamId, DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS)
|
|
1058
|
+
}
|
|
1059
|
+
return event
|
|
1060
|
+
} finally {
|
|
1061
|
+
start?.let { pendingStreamStarts.remove(streamId, it) }
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
internal fun sendExternallyManagedStreamKeepAlive(request: StreamKeepAliveRequest) {
|
|
1066
|
+
deviceManager.keepStreamAlive(request.toMap().toMutableMap())
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
suspend fun rgbLedControl(request: RgbLedRequest): RgbLedControlResponseEvent {
|
|
1070
|
+
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}")
|
|
1071
|
+
pendingRgbLedRequests[request.requestId] = pending
|
|
1072
|
+
try {
|
|
1073
|
+
deviceManager.rgbLedControl(
|
|
1074
|
+
request.requestId,
|
|
1075
|
+
request.packageName,
|
|
1076
|
+
request.action.value,
|
|
1077
|
+
request.color?.value,
|
|
1078
|
+
request.onDurationMs,
|
|
1079
|
+
request.offDurationMs,
|
|
1080
|
+
request.count,
|
|
1081
|
+
)
|
|
1082
|
+
return pending.await()
|
|
1083
|
+
} finally {
|
|
1084
|
+
pendingRgbLedRequests.remove(request.requestId, pending)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
suspend fun stopStream(): StreamStatusEvent {
|
|
1089
|
+
val pending = PendingResponse<StreamStatusEvent>("stop stream")
|
|
1090
|
+
val targetStreamId = synchronized(streamKeepAliveLock) {
|
|
1091
|
+
activeStreamKeepAlive?.streamId
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
// The seq draw and the BLE hand-off share startStream's ordering
|
|
1095
|
+
// critical section: the stop takes its own slot in the send order,
|
|
1096
|
+
// so its ack can separate the pending starts the glasses consumed
|
|
1097
|
+
// before it (lower seq) from starts sent after it (higher seq).
|
|
1098
|
+
synchronized(streamStartOrderLock) {
|
|
1099
|
+
synchronized(oneShotLock) {
|
|
1100
|
+
if (pendingStreamStop != null) {
|
|
1101
|
+
throw BluetoothSdkException(
|
|
1102
|
+
"request_in_flight",
|
|
1103
|
+
"A stream stop command is already waiting for a glasses response.",
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
pendingStreamStop =
|
|
1107
|
+
PendingStreamStop(targetStreamId, streamStartSeq.incrementAndGet(), pending)
|
|
1108
|
+
}
|
|
1109
|
+
stopStreamKeepAliveMonitor()
|
|
1110
|
+
deviceManager.stopStream()
|
|
1111
|
+
}
|
|
1112
|
+
return pending.await(STREAM_STOP_TIMEOUT_MS)
|
|
1113
|
+
} finally {
|
|
1114
|
+
synchronized(oneShotLock) {
|
|
1115
|
+
if (pendingStreamStop?.pending === pending) {
|
|
1116
|
+
pendingStreamStop = null
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
suspend fun startVideoRecording(request: VideoRecordingRequest): VideoRecordingStatusEvent {
|
|
1123
|
+
require(request.requestId.isNotBlank()) { "requestId is required to start video recording." }
|
|
1124
|
+
requireGlassesConnected("start video recording")
|
|
1125
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
|
|
1126
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
|
|
1127
|
+
if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
|
|
1128
|
+
throw BluetoothSdkException(
|
|
1129
|
+
"request_in_flight",
|
|
1130
|
+
"A video recording command is already waiting for requestId ${request.requestId}.",
|
|
1131
|
+
)
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
deviceManager.startVideoRecording(
|
|
1135
|
+
request.requestId,
|
|
1136
|
+
request.save,
|
|
1137
|
+
request.sound,
|
|
1138
|
+
request.width,
|
|
1139
|
+
request.height,
|
|
1140
|
+
request.fps,
|
|
1141
|
+
request.maxRecordingTimeMinutes,
|
|
1142
|
+
)
|
|
1143
|
+
return pending.await()
|
|
1144
|
+
} finally {
|
|
1145
|
+
pendingVideoRecordingRequests.remove(request.requestId, pendingRequest)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
@JvmOverloads
|
|
1150
|
+
suspend fun stopVideoRecording(
|
|
1151
|
+
requestId: String,
|
|
1152
|
+
webhookUrl: String? = null,
|
|
1153
|
+
authToken: String? = null,
|
|
1154
|
+
): VideoRecordingStatusEvent {
|
|
1155
|
+
require(requestId.isNotBlank()) { "requestId is required to stop video recording." }
|
|
1156
|
+
requireGlassesConnected("stop video recording")
|
|
1157
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("stop video recording")
|
|
1158
|
+
val waitForUpload = !webhookUrl.isNullOrBlank()
|
|
1159
|
+
val pendingRequest =
|
|
1160
|
+
PendingVideoRecordingRequest(
|
|
1161
|
+
expectedStatus = "recording_stopped",
|
|
1162
|
+
pending = pending,
|
|
1163
|
+
waitForUpload = waitForUpload,
|
|
1164
|
+
)
|
|
1165
|
+
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
1166
|
+
throw BluetoothSdkException(
|
|
1167
|
+
"request_in_flight",
|
|
1168
|
+
"A video recording command is already waiting for requestId $requestId.",
|
|
1169
|
+
)
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
deviceManager.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1173
|
+
val timeoutMs =
|
|
1174
|
+
if (waitForUpload) VIDEO_UPLOAD_STOP_TIMEOUT_MS else DEFAULT_REQUEST_TIMEOUT_MS
|
|
1175
|
+
return pending.await(timeoutMs)
|
|
1176
|
+
} finally {
|
|
1177
|
+
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
suspend fun queryVideoRecordingStatus(requestId: String): VideoRecordingStatusEvent {
|
|
1182
|
+
require(requestId.isNotBlank()) { "requestId is required to query video recording status." }
|
|
1183
|
+
requireGlassesConnected("query video recording status")
|
|
1184
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("query video recording status")
|
|
1185
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_status", pending)
|
|
1186
|
+
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
1187
|
+
throw BluetoothSdkException(
|
|
1188
|
+
"request_in_flight",
|
|
1189
|
+
"A video recording command is already waiting for requestId $requestId.",
|
|
1190
|
+
)
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
deviceManager.queryVideoRecordingStatus(requestId)
|
|
1194
|
+
return pending.await()
|
|
1195
|
+
} finally {
|
|
1196
|
+
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
suspend fun requestVersionInfo(): VersionInfoResult {
|
|
1201
|
+
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
1202
|
+
synchronized(oneShotLock) {
|
|
1203
|
+
if (pendingVersionInfo != null) {
|
|
1204
|
+
throw BluetoothSdkException(
|
|
1205
|
+
"request_in_flight",
|
|
1206
|
+
"A version info request is already waiting for a glasses response.",
|
|
1207
|
+
)
|
|
1208
|
+
}
|
|
1209
|
+
pendingVersionInfo = pending
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
deviceManager.requestVersionInfo()
|
|
1213
|
+
return pending.await()
|
|
1214
|
+
} finally {
|
|
1215
|
+
synchronized(oneShotLock) {
|
|
1216
|
+
if (pendingVersionInfo === pending) {
|
|
1217
|
+
pendingVersionInfo = null
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
fun availableG2FirmwareArtifacts(): List<Map<String, Any>> {
|
|
1224
|
+
requireGlassesConnected("list G2 firmware")
|
|
1225
|
+
return deviceManager.availableG2FirmwareArtifacts()
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
suspend fun startG2FirmwareFlash(target: String): Map<String, Any> {
|
|
1229
|
+
requireGlassesConnected("flash G2 firmware")
|
|
1230
|
+
return deviceManager.startG2FirmwareFlash(target)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** Select the HTTP(S) manifest used by subsequent OTA checks and installs. */
|
|
1234
|
+
fun setOtaVersionUrl(otaVersionUrl: String) {
|
|
1235
|
+
configuredOtaVersionUrl = OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Return the configured OTA manifest, or this SDK release's default manifest. */
|
|
1239
|
+
fun getOtaVersionUrl(): String = configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1240
|
+
|
|
1241
|
+
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
1242
|
+
suspend fun checkForOtaUpdate(): Boolean {
|
|
1243
|
+
val status = getFreshGlassesStatus()
|
|
1244
|
+
if (!status.connected) {
|
|
1245
|
+
throw BluetoothSdkException(
|
|
1246
|
+
"glasses_not_connected",
|
|
1247
|
+
"Cannot check OTA update because glasses are not connected.",
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
if (status.buildNumber.isBlank()) {
|
|
1251
|
+
throw BluetoothSdkException(
|
|
1252
|
+
"missing_glasses_version",
|
|
1253
|
+
"Cannot check OTA update because glasses build number is unavailable.",
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
val manifestUrl = resolveOtaVersionUrl(status)
|
|
1258
|
+
val manifest = OtaManifestChecker.fetch(manifestUrl)
|
|
1259
|
+
val otaStatus = waitForOtaManifestStatus(status, manifest)
|
|
1260
|
+
return OtaManifestChecker.hasUpdate(
|
|
1261
|
+
otaStatus.buildNumber,
|
|
1262
|
+
otaStatus.mtkFirmwareVersion,
|
|
1263
|
+
otaStatus.besFirmwareVersion,
|
|
1264
|
+
manifest,
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
|
|
1269
|
+
fun getReleaseChangelogs(
|
|
1270
|
+
fromVersion: String? = null,
|
|
1271
|
+
toVersion: String? = null,
|
|
1272
|
+
): List<ReleaseChangelog> = ReleaseChangelogCatalog.select(fromVersion, toVersion)
|
|
1273
|
+
|
|
1274
|
+
/** Ask connected Mentra Live glasses to report the current OTA install/session status. */
|
|
1275
|
+
private suspend fun queryOtaStatus(): OtaQueryResult =
|
|
1276
|
+
performOtaQuery("OTA status query") {
|
|
1277
|
+
deviceManager.sendOtaQueryStatus()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
private suspend fun performOtaQuery(
|
|
1281
|
+
operation: String,
|
|
1282
|
+
sendRequest: () -> Unit,
|
|
1283
|
+
): OtaQueryResult {
|
|
1284
|
+
val pending = PendingResponse<OtaQueryResult>(operation)
|
|
1285
|
+
synchronized(oneShotLock) {
|
|
1286
|
+
if (pendingOtaQuery != null) {
|
|
1287
|
+
throw BluetoothSdkException(
|
|
1288
|
+
"request_in_flight",
|
|
1289
|
+
"An OTA status query is already waiting for a glasses response.",
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
pendingOtaQuery = pending
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
sendRequest()
|
|
1296
|
+
return pending.await()
|
|
1297
|
+
} finally {
|
|
1298
|
+
synchronized(oneShotLock) {
|
|
1299
|
+
if (pendingOtaQuery === pending) {
|
|
1300
|
+
pendingOtaQuery = null
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** Start the OTA flow after your app has presented the available update to the user. */
|
|
1307
|
+
suspend fun startOtaUpdate(): OtaStartAckEvent {
|
|
1308
|
+
val otaVersionUrl = resolveOtaVersionUrl(getFreshGlassesStatus())
|
|
1309
|
+
return startOtaUpdate(otaVersionUrl)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private suspend fun startOtaCommand(otaVersionUrl: String): OtaStartAckEvent {
|
|
1313
|
+
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
1314
|
+
synchronized(oneShotLock) {
|
|
1315
|
+
if (pendingOtaStart != null) {
|
|
1316
|
+
throw BluetoothSdkException(
|
|
1317
|
+
"request_in_flight",
|
|
1318
|
+
"An OTA start command is already waiting for a glasses response.",
|
|
1319
|
+
)
|
|
1320
|
+
}
|
|
1321
|
+
pendingOtaStart = pending
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
deviceManager.sendOtaStart(otaVersionUrl)
|
|
1325
|
+
return pending.await()
|
|
1326
|
+
} finally {
|
|
1327
|
+
synchronized(oneShotLock) {
|
|
1328
|
+
if (pendingOtaStart === pending) {
|
|
1329
|
+
pendingOtaStart = null
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
internal suspend fun startOtaUpdate(otaVersionUrl: String): OtaStartAckEvent =
|
|
1336
|
+
startOtaCommand(otaVersionUrl)
|
|
1337
|
+
|
|
1338
|
+
internal suspend fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1339
|
+
|
|
1340
|
+
fun startAr99OtaFromFile(path: String): Boolean {
|
|
1341
|
+
requireGlassesConnected("start AR99 OTA")
|
|
1342
|
+
return deviceManager.startAr99OtaFromFile(path)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
fun cancelAr99Ota() {
|
|
1346
|
+
deviceManager.cancelAr99Ota()
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
fun sendAr99FactoryReset() {
|
|
1350
|
+
requireGlassesConnected("factory reset AR99")
|
|
1351
|
+
deviceManager.sendAr99FactoryReset()
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private suspend fun getFreshGlassesStatus(): GlassesStatus {
|
|
1355
|
+
val status = getRawGlassesStatus()
|
|
1356
|
+
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
1357
|
+
return status
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return try {
|
|
1361
|
+
val versionInfo = requestVersionInfo()
|
|
1362
|
+
status.copy(
|
|
1363
|
+
androidVersion = versionInfo.androidVersion.ifBlank { status.androidVersion },
|
|
1364
|
+
firmwareVersion = versionInfo.firmwareVersion.ifBlank { status.firmwareVersion },
|
|
1365
|
+
besFirmwareVersion = versionInfo.besFirmwareVersion.ifBlank { status.besFirmwareVersion },
|
|
1366
|
+
mtkFirmwareVersion = versionInfo.mtkFirmwareVersion.ifBlank { status.mtkFirmwareVersion },
|
|
1367
|
+
buildNumber = versionInfo.buildNumber.ifBlank { status.buildNumber },
|
|
1368
|
+
systemTimeMs = versionInfo.systemTimeMs ?: status.systemTimeMs,
|
|
1369
|
+
otaVersionUrl = versionInfo.otaVersionUrl.ifBlank { status.otaVersionUrl },
|
|
1370
|
+
appVersion = versionInfo.appVersion.ifBlank { status.appVersion },
|
|
1371
|
+
hotspotOtaVersion =
|
|
1372
|
+
if (versionInfo.hotspotOtaVersion > 0) {
|
|
1373
|
+
versionInfo.hotspotOtaVersion
|
|
1374
|
+
} else {
|
|
1375
|
+
status.hotspotOtaVersion
|
|
1376
|
+
},
|
|
1377
|
+
)
|
|
1378
|
+
} catch (cancellation: CancellationException) {
|
|
1379
|
+
// Never swallow cancellation into the stale-status fallback: callers
|
|
1380
|
+
// like startOtaUpdate() would otherwise keep running side effects
|
|
1381
|
+
// (sendOtaStart) on behalf of an already-cancelled coroutine.
|
|
1382
|
+
throw cancellation
|
|
1383
|
+
} catch (_: Throwable) {
|
|
1384
|
+
status
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private suspend fun waitForOtaManifestStatus(
|
|
1389
|
+
initialStatus: GlassesStatus,
|
|
1390
|
+
manifest: org.json.JSONObject,
|
|
1391
|
+
): GlassesStatus {
|
|
1392
|
+
var status = initialStatus
|
|
1393
|
+
if (OtaManifestChecker.hasBesFirmware(manifest) && status.besFirmwareVersion.isBlank()) {
|
|
1394
|
+
status = waitForGlassesStatus(status, OTA_BES_VERSION_WAIT_MS) {
|
|
1395
|
+
!it.connected || it.besFirmwareVersion.isNotBlank()
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (OtaManifestChecker.hasMtkPatches(manifest) && status.mtkFirmwareVersion.isBlank()) {
|
|
1400
|
+
status = waitForGlassesStatus(status, OTA_MTK_VERSION_WAIT_MS) {
|
|
1401
|
+
!it.connected || it.mtkFirmwareVersion.isNotBlank()
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (!status.connected) {
|
|
1406
|
+
throw BluetoothSdkException(
|
|
1407
|
+
"glasses_not_connected",
|
|
1408
|
+
"Cannot check OTA update because glasses disconnected.",
|
|
1409
|
+
)
|
|
1410
|
+
}
|
|
1411
|
+
return status
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private suspend fun waitForGlassesStatus(
|
|
1415
|
+
initialStatus: GlassesStatus,
|
|
1416
|
+
timeoutMs: Long,
|
|
1417
|
+
isReady: (GlassesStatus) -> Boolean,
|
|
1418
|
+
): GlassesStatus {
|
|
1419
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
1420
|
+
var status = initialStatus
|
|
1421
|
+
while (System.currentTimeMillis() < deadline) {
|
|
1422
|
+
status = getRawGlassesStatus()
|
|
1423
|
+
if (isReady(status)) return status
|
|
1424
|
+
val remainingMs = deadline - System.currentTimeMillis()
|
|
1425
|
+
if (remainingMs <= 0L) break
|
|
1426
|
+
delay(minOf(OTA_VERSION_POLL_MS, remainingMs))
|
|
1427
|
+
}
|
|
1428
|
+
return getRawGlassesStatus()
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private fun resolveOtaVersionUrl(status: GlassesStatus): String {
|
|
1432
|
+
val deviceUrl = status.otaVersionUrl.trim()
|
|
1433
|
+
if (isLegacyAsgOtaStartBuild(status.buildNumber)) {
|
|
1434
|
+
return OtaManifestDefaults.LEGACY_PROD_OTA_VERSION_URL
|
|
1435
|
+
}
|
|
1436
|
+
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1437
|
+
// A future glasses-advertised URL should not silently change that pairing.
|
|
1438
|
+
return configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private fun isLegacyAsgOtaStartBuild(buildNumber: String): Boolean {
|
|
1442
|
+
val parsed = buildNumber.toIntOrNull()
|
|
1443
|
+
return parsed != null && parsed < 39
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
internal fun sendShutdown() {
|
|
1447
|
+
deviceManager.sendShutdown()
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
internal fun sendReboot() {
|
|
1451
|
+
deviceManager.sendReboot()
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
internal fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
|
|
1455
|
+
deviceManager.sendIncidentId(incidentId, apiBaseUrl)
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
override fun close() {
|
|
1459
|
+
stopStreamKeepAliveMonitor()
|
|
1460
|
+
if (activityLifecycleCallbacksRegistered) {
|
|
1461
|
+
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1462
|
+
activityLifecycleCallbacks,
|
|
1463
|
+
)
|
|
1464
|
+
activityLifecycleCallbacksRegistered = false
|
|
1465
|
+
}
|
|
1466
|
+
Bridge.removeEventSink(bridgeEventSinkId)
|
|
1467
|
+
DeviceStore.store.removeListener(storeListenerId)
|
|
1468
|
+
analytics.shutdown()
|
|
1469
|
+
listeners.clear()
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
1473
|
+
when (ObservableStore.normalizeCategory(category)) {
|
|
1474
|
+
"glasses" -> {
|
|
1475
|
+
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
1476
|
+
val state = getState()
|
|
1477
|
+
dispatchToListeners {
|
|
1478
|
+
it.onStateChanged(state)
|
|
1479
|
+
it.onGlassesChanged(state.glasses)
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
ObservableStore.BLUETOOTH_CATEGORY -> {
|
|
1483
|
+
val state = getState()
|
|
1484
|
+
val scanChanged = changes.keys.any { it in SCAN_STATE_KEYS }
|
|
1485
|
+
dispatchToListeners {
|
|
1486
|
+
it.onStateChanged(state)
|
|
1487
|
+
it.onSdkStateChanged(state.sdk)
|
|
1488
|
+
if (scanChanged) {
|
|
1489
|
+
it.onScanChanged(state.scan)
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (!suppressDefaultDeviceEvents && changes.keys.any { it in DEFAULT_DEVICE_KEYS }) {
|
|
1493
|
+
dispatchDefaultDeviceChanged()
|
|
1494
|
+
}
|
|
1495
|
+
dispatchDiscoveredDevices(changes["searchResults"])
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private fun dispatchDefaultDeviceChanged() {
|
|
1501
|
+
val defaultDevice = currentDefaultDevice()
|
|
1502
|
+
dispatchToListeners { it.onDefaultDeviceChanged(defaultDevice) }
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
private fun currentDefaultDevice(): Device? {
|
|
1506
|
+
val core = DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
|
|
1507
|
+
val model = core["default_wearable"] as? String ?: return null
|
|
1508
|
+
val name = core["device_name"] as? String ?: return null
|
|
1509
|
+
if (model.isBlank() || name.isBlank()) return null
|
|
1510
|
+
val address = (core["device_address"] as? String)?.takeIf { it.isNotBlank() }
|
|
1511
|
+
val projectName = (core["project_name"] as? String)?.takeIf { it.isNotBlank() }
|
|
1512
|
+
return Device(
|
|
1513
|
+
model = DeviceModel.fromDeviceType(model),
|
|
1514
|
+
name = name,
|
|
1515
|
+
address = address,
|
|
1516
|
+
projectName = projectName,
|
|
1517
|
+
)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
private fun requireBluetoothReady(operation: String) {
|
|
1521
|
+
val bluetoothManager = appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
1522
|
+
val adapter =
|
|
1523
|
+
bluetoothManager?.adapter
|
|
1524
|
+
?: throw BluetoothSdkException(
|
|
1525
|
+
"bluetooth_unsupported",
|
|
1526
|
+
"This phone does not support Bluetooth.",
|
|
1527
|
+
)
|
|
1528
|
+
val enabled =
|
|
1529
|
+
try {
|
|
1530
|
+
adapter.isEnabled
|
|
1531
|
+
} catch (error: SecurityException) {
|
|
1532
|
+
throw BluetoothSdkException(
|
|
1533
|
+
"bluetooth_permission_denied",
|
|
1534
|
+
"Allow Bluetooth permission to $operation.",
|
|
1535
|
+
error,
|
|
1536
|
+
)
|
|
1537
|
+
}
|
|
1538
|
+
if (!enabled) {
|
|
1539
|
+
throw BluetoothSdkException(
|
|
1540
|
+
"bluetooth_powered_off",
|
|
1541
|
+
"Turn on phone Bluetooth to $operation.",
|
|
1542
|
+
)
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
private fun Throwable.toBluetoothError(defaultCode: String): BluetoothError =
|
|
1547
|
+
if (this is BluetoothSdkException) {
|
|
1548
|
+
BluetoothError(code, message ?: code, this)
|
|
1549
|
+
} else {
|
|
1550
|
+
BluetoothError(defaultCode, message ?: toString(), this)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private fun dispatchDiscoveredDevices(rawSearchResults: Any?) {
|
|
1554
|
+
val results = rawSearchResults as? List<*> ?: return
|
|
1555
|
+
results.forEach { rawResult ->
|
|
1556
|
+
val result = rawResult as? Map<*, *> ?: return@forEach
|
|
1557
|
+
val name = result["name"] as? String ?: return@forEach
|
|
1558
|
+
if (!discoveredDeviceNames.add(name)) return@forEach
|
|
1559
|
+
val values =
|
|
1560
|
+
result.entries.mapNotNull { (key, value) ->
|
|
1561
|
+
if (key is String && value != null) key to value else null
|
|
1562
|
+
}.toMap()
|
|
1563
|
+
val device = Device.fromMap(values) ?: return@forEach
|
|
1564
|
+
dispatchToListeners { it.onDeviceDiscovered(device) }
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private fun dispatchBridgeEvent(eventName: String, data: Map<String, Any>) {
|
|
1569
|
+
when (eventName) {
|
|
1570
|
+
"log" -> dispatchToListeners { it.onLog(data["message"] as? String ?: data.toString()) }
|
|
1571
|
+
"button_press" ->
|
|
1572
|
+
dispatchToListeners {
|
|
1573
|
+
it.onButtonPress(
|
|
1574
|
+
ButtonPressEvent(
|
|
1575
|
+
buttonId = data["buttonId"] as? String ?: "",
|
|
1576
|
+
pressType = data["pressType"] as? String ?: "",
|
|
1577
|
+
timestamp = (data["timestamp"] as? Number)?.toLong(),
|
|
1578
|
+
)
|
|
1579
|
+
)
|
|
1580
|
+
}
|
|
1581
|
+
"touch_event" -> {
|
|
1582
|
+
val event = TouchEvent(data)
|
|
1583
|
+
dispatchToListeners { it.onTouch(event) }
|
|
1584
|
+
if (event.isSwipe) {
|
|
1585
|
+
dispatchToListeners { it.onSwipe(SwipeEvent(data)) }
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
"head_up" -> dispatchToListeners { it.onHeadUpChanged(data["up"] as? Boolean ?: false) }
|
|
1589
|
+
"voice_activity_detection_status" ->
|
|
1590
|
+
dispatchToListeners {
|
|
1591
|
+
it.onVoiceActivityDetectionStatus(
|
|
1592
|
+
VoiceActivityDetectionStatusEvent(
|
|
1593
|
+
voiceActivityDetectionEnabled =
|
|
1594
|
+
data["voiceActivityDetectionEnabled"] as? Boolean
|
|
1595
|
+
?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
1596
|
+
values = data,
|
|
1597
|
+
)
|
|
1598
|
+
)
|
|
1599
|
+
}
|
|
1600
|
+
"speaking_status" ->
|
|
1601
|
+
dispatchToListeners {
|
|
1602
|
+
it.onSpeakingStatus(
|
|
1603
|
+
SpeakingStatusEvent(
|
|
1604
|
+
speaking = data["speaking"] as? Boolean ?: false,
|
|
1605
|
+
values = data,
|
|
1606
|
+
)
|
|
1607
|
+
)
|
|
1608
|
+
}
|
|
1609
|
+
"battery_status" ->
|
|
1610
|
+
dispatchToListeners {
|
|
1611
|
+
it.onBatteryStatus(
|
|
1612
|
+
BatteryStatusEvent(
|
|
1613
|
+
level = (data["level"] as? Number)?.toInt(),
|
|
1614
|
+
charging = data["charging"] as? Boolean,
|
|
1615
|
+
values = data,
|
|
1616
|
+
)
|
|
1617
|
+
)
|
|
1618
|
+
}
|
|
1619
|
+
"wifi_status_change" -> {
|
|
1620
|
+
val event = WifiStatusEvent(data)
|
|
1621
|
+
handleWifiStatusForRequests(event)
|
|
1622
|
+
dispatchToListeners { it.onWifiStatusChanged(event) }
|
|
1623
|
+
}
|
|
1624
|
+
"wifi_scan_result" -> {
|
|
1625
|
+
val networks =
|
|
1626
|
+
(data["networks"] as? List<*>)
|
|
1627
|
+
?.mapNotNull { (it as? Map<*, *>)?.stringKeyedMap() }
|
|
1628
|
+
?.map(WifiScanResult::fromMap)
|
|
1629
|
+
?: emptyList()
|
|
1630
|
+
val hasCompletionFlag =
|
|
1631
|
+
data.containsKey("scanComplete") || data.containsKey("scan_complete")
|
|
1632
|
+
val scanComplete =
|
|
1633
|
+
(data["scanComplete"] as? Boolean) ?: (data["scan_complete"] as? Boolean) ?: false
|
|
1634
|
+
val scanId = (data["scanId"] as? String)?.ifEmpty { null }
|
|
1635
|
+
if (scanId != null) {
|
|
1636
|
+
handleCorrelatedWifiScanChunk(scanId, networks, scanComplete)
|
|
1637
|
+
} else {
|
|
1638
|
+
// Old firmware: no correlation id, keep the pre-scanId heuristics.
|
|
1639
|
+
updateWifiScanLatestResults(networks)
|
|
1640
|
+
if (scanComplete || !hasCompletionFlag) {
|
|
1641
|
+
handleWifiScanResultsForRequests(networks)
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1645
|
+
}
|
|
1646
|
+
"hotspot_status_change" -> {
|
|
1647
|
+
val event = HotspotStatusEvent(data)
|
|
1648
|
+
handleHotspotStatusForRequests(event)
|
|
1649
|
+
dispatchToListeners { it.onHotspotStatusChanged(event) }
|
|
1650
|
+
}
|
|
1651
|
+
"hotspot_error" -> {
|
|
1652
|
+
val event = HotspotErrorEvent(data)
|
|
1653
|
+
handleHotspotErrorForRequests(event)
|
|
1654
|
+
dispatchToListeners { it.onHotspotError(event) }
|
|
1655
|
+
}
|
|
1656
|
+
"gallery_status" -> {
|
|
1657
|
+
val event = GalleryStatusEvent(data)
|
|
1658
|
+
synchronized(oneShotLock) {
|
|
1659
|
+
pendingGalleryStatus?.resolve(event)
|
|
1660
|
+
}
|
|
1661
|
+
dispatchToListeners { it.onGalleryStatus(event) }
|
|
1662
|
+
}
|
|
1663
|
+
"pairing_info" -> {
|
|
1664
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1665
|
+
}
|
|
1666
|
+
"photo_response" -> {
|
|
1667
|
+
val event = PhotoResponseEvent(data)
|
|
1668
|
+
handlePhotoResponseForRequests(event)
|
|
1669
|
+
dispatchToListeners { it.onPhotoResponse(event) }
|
|
1670
|
+
}
|
|
1671
|
+
"photo_status" -> dispatchToListeners { it.onPhotoStatus(PhotoStatusEvent(data)) }
|
|
1672
|
+
"camera_status" -> {
|
|
1673
|
+
val event = CameraStatusEvent(data)
|
|
1674
|
+
handleCameraStatusForRequests(event)
|
|
1675
|
+
dispatchToListeners { it.onCameraStatus(event) }
|
|
1676
|
+
}
|
|
1677
|
+
"video_recording_status" -> {
|
|
1678
|
+
val event = VideoRecordingStatusEvent(data)
|
|
1679
|
+
handleVideoRecordingStatusForRequests(event)
|
|
1680
|
+
dispatchToListeners { it.onVideoRecordingStatus(event) }
|
|
1681
|
+
}
|
|
1682
|
+
"media_success", "media_error" -> {
|
|
1683
|
+
val event = MediaUploadEvent(data)
|
|
1684
|
+
handleMediaUploadForRequests(event)
|
|
1685
|
+
dispatchToListeners { it.onMediaUpload(event) }
|
|
1686
|
+
}
|
|
1687
|
+
"rgb_led_control_response" -> {
|
|
1688
|
+
val event = RgbLedControlResponseEvent(data)
|
|
1689
|
+
handleRgbLedResponseForRequests(event)
|
|
1690
|
+
dispatchToListeners { it.onRgbLedControlResponse(event) }
|
|
1691
|
+
}
|
|
1692
|
+
"stream_status" -> {
|
|
1693
|
+
val event = StreamStatusEvent(data)
|
|
1694
|
+
handleStreamStatusForRequests(event)
|
|
1695
|
+
handleStreamStatusForKeepAlive(event.status)
|
|
1696
|
+
dispatchToListeners { it.onStreamStatus(event) }
|
|
1697
|
+
}
|
|
1698
|
+
"keep_alive_ack" -> {
|
|
1699
|
+
val event = KeepAliveAckEvent(data)
|
|
1700
|
+
if (!handleStreamKeepAliveAck(event)) {
|
|
1701
|
+
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
"ota_start_ack" -> {
|
|
1705
|
+
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
1706
|
+
synchronized(oneShotLock) {
|
|
1707
|
+
pendingOtaStart?.resolve(event)
|
|
1708
|
+
}
|
|
1709
|
+
dispatchToListeners { it.onOtaStartAck(event) }
|
|
1710
|
+
}
|
|
1711
|
+
"ota_status" -> {
|
|
1712
|
+
val resultValues = data + mapOf("type" to "ota_status")
|
|
1713
|
+
val event = OtaStatusEvent.fromMap(resultValues)
|
|
1714
|
+
synchronized(oneShotLock) {
|
|
1715
|
+
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1716
|
+
otaStartRejectionErrorCode(event)?.let { errorCode ->
|
|
1717
|
+
pendingOtaStart?.reject(
|
|
1718
|
+
BluetoothSdkException(errorCode, "Glasses rejected OTA start: $errorCode")
|
|
1719
|
+
)
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
dispatchToListeners { it.onOtaStatus(event) }
|
|
1723
|
+
}
|
|
1724
|
+
"mic_health" -> {
|
|
1725
|
+
dispatchToListeners { it.onMicHealth(MicHealthEvent.fromMap(data)) }
|
|
1726
|
+
}
|
|
1727
|
+
"settings_ack" -> {
|
|
1728
|
+
val event = SettingsAckEvent(data)
|
|
1729
|
+
handleSettingsAckForRequests(event)
|
|
1730
|
+
dispatchToListeners { it.onSettingsAck(event) }
|
|
1731
|
+
}
|
|
1732
|
+
"version_info" -> {
|
|
1733
|
+
val event = VersionInfoResult.fromMap(data)
|
|
1734
|
+
synchronized(oneShotLock) {
|
|
1735
|
+
pendingVersionInfo?.resolve(event)
|
|
1736
|
+
}
|
|
1737
|
+
dispatchToListeners { it.onVersionInfo(event) }
|
|
1738
|
+
}
|
|
1739
|
+
"mic_pcm" -> {
|
|
1740
|
+
val event = MicPcmEvent(data)
|
|
1741
|
+
if (event.pcm.isNotEmpty()) {
|
|
1742
|
+
dispatchToListeners { it.onMicPcm(event) }
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
"mic_lc3" -> {
|
|
1746
|
+
val event = MicLc3Event(data)
|
|
1747
|
+
if (event.lc3.isNotEmpty()) {
|
|
1748
|
+
dispatchToListeners { it.onMicLc3(event) }
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
"local_transcription" ->
|
|
1752
|
+
dispatchToListeners {
|
|
1753
|
+
it.onLocalTranscription(
|
|
1754
|
+
LocalTranscriptionEvent(
|
|
1755
|
+
text = data["text"] as? String ?: "",
|
|
1756
|
+
isFinal = data["isFinal"] as? Boolean ?: false,
|
|
1757
|
+
values = data,
|
|
1758
|
+
)
|
|
1759
|
+
)
|
|
1760
|
+
}
|
|
1761
|
+
"compatible_glasses_search_stop" ->
|
|
1762
|
+
dispatchToListeners { it.onScanStopped(ScanStopReason.COMPLETED) }
|
|
1763
|
+
"pair_failure" ->
|
|
1764
|
+
dispatchToListeners {
|
|
1765
|
+
it.onError(
|
|
1766
|
+
BluetoothError(
|
|
1767
|
+
code = "pair_failure",
|
|
1768
|
+
message = data["error"] as? String ?: data.toString(),
|
|
1769
|
+
)
|
|
1770
|
+
)
|
|
1771
|
+
}
|
|
1772
|
+
else -> dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private fun dispatchToListeners(callback: (BluetoothSdkListener) -> Unit) {
|
|
1777
|
+
val snapshot = synchronized(listeners) { listeners.toList() }
|
|
1778
|
+
val deliver = {
|
|
1779
|
+
snapshot.forEach { listener ->
|
|
1780
|
+
try {
|
|
1781
|
+
callback(listener)
|
|
1782
|
+
} catch (error: Throwable) {
|
|
1783
|
+
// Listener exceptions should not crash Bluetooth event delivery.
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (config.deliverCallbacksOnMainThread && Looper.myLooper() != Looper.getMainLooper()) {
|
|
1788
|
+
mainHandler.post(deliver)
|
|
1789
|
+
} else {
|
|
1790
|
+
deliver()
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
private fun startStreamKeepAliveMonitor(streamId: String, requestedIntervalSeconds: Int) {
|
|
1795
|
+
val intervalSeconds =
|
|
1796
|
+
requestedIntervalSeconds.takeIf { it > 0 } ?: DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS
|
|
1797
|
+
val tracker = ActiveStreamKeepAlive(
|
|
1798
|
+
streamId = streamId,
|
|
1799
|
+
intervalMs = intervalSeconds * 1_000L,
|
|
1800
|
+
)
|
|
1801
|
+
synchronized(streamKeepAliveLock) {
|
|
1802
|
+
activeStreamKeepAlive = tracker
|
|
1803
|
+
}
|
|
1804
|
+
sendNextStreamKeepAlive(tracker)
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
private fun stopStreamKeepAliveMonitor() {
|
|
1808
|
+
val tracker =
|
|
1809
|
+
synchronized(streamKeepAliveLock) {
|
|
1810
|
+
val current = activeStreamKeepAlive
|
|
1811
|
+
activeStreamKeepAlive = null
|
|
1812
|
+
current
|
|
1813
|
+
}
|
|
1814
|
+
tracker?.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private fun handleStreamStatusForRequests(event: StreamStatusEvent) {
|
|
1818
|
+
val startMatch = matchingStreamStart(event)
|
|
1819
|
+
if (startMatch != null) {
|
|
1820
|
+
val (streamId, start) = startMatch
|
|
1821
|
+
// Preemption may only be inferred from states the glasses emit AFTER
|
|
1822
|
+
// start_stream has run stopAllServices() (see PREEMPTION_PROVEN_STATES).
|
|
1823
|
+
// ERROR must not fire it: a command-level preflight rejection (missing
|
|
1824
|
+
// URL, low battery, no WiFi) carries this start's streamId but is sent
|
|
1825
|
+
// BEFORE stopAllServices, so older starts were not preempted — and
|
|
1826
|
+
// rejecting one that already resolved and started its keep-alive
|
|
1827
|
+
// monitor would strand a live stream without keep-alives. STOPPED
|
|
1828
|
+
// must not fire it either: it is the stop-ack shape and proves
|
|
1829
|
+
// nothing about the start path having run.
|
|
1830
|
+
if (!event.streamId.isNullOrBlank() && event.state in PREEMPTION_PROVEN_STATES) {
|
|
1831
|
+
rejectPreemptedStreamStarts(start.seq)
|
|
1832
|
+
}
|
|
1833
|
+
when (event.state) {
|
|
1834
|
+
// `reconnected` also proves the stream is live: when an async start
|
|
1835
|
+
// failure triggers the glasses' publisher retry, a successful retry
|
|
1836
|
+
// reports `reconnected` instead of `streaming`.
|
|
1837
|
+
StreamState.STREAMING,
|
|
1838
|
+
StreamState.RECONNECTED -> {
|
|
1839
|
+
pendingStreamStarts.remove(streamId, start)
|
|
1840
|
+
start.pending.resolve(event)
|
|
1841
|
+
}
|
|
1842
|
+
StreamState.RECONNECT_FAILED,
|
|
1843
|
+
StreamState.STOPPED -> {
|
|
1844
|
+
pendingStreamStarts.remove(streamId, start)
|
|
1845
|
+
start.pending.reject(
|
|
1846
|
+
streamStatusException(start.lastError ?: event, "stream_start_failed")
|
|
1847
|
+
)
|
|
1848
|
+
}
|
|
1849
|
+
StreamState.ERROR -> {
|
|
1850
|
+
if (!event.streamId.isNullOrBlank() && event.willRetry == true) {
|
|
1851
|
+
// The glasses flag errors their publisher will retry with
|
|
1852
|
+
// `willRetry` (emitting side lands in PR #3488), so keep the
|
|
1853
|
+
// start pending for the retry's verdict — `reconnected` or
|
|
1854
|
+
// `streaming` on success, `reconnect_failed` or the
|
|
1855
|
+
// streamId-less `stopped` wind-down (which reports these
|
|
1856
|
+
// stashed details) on failure.
|
|
1857
|
+
start.lastError = event
|
|
1858
|
+
} else {
|
|
1859
|
+
// An id-less error is the glasses' command-level rejection
|
|
1860
|
+
// (missing URL, low battery, no WiFi) emitted before any
|
|
1861
|
+
// publisher starts; an id-carrying error without `willRetry`
|
|
1862
|
+
// is terminal (`camera_busy` emits an error and nothing else).
|
|
1863
|
+
// Old firmware never sends `willRetry`, so its errors always
|
|
1864
|
+
// fail fast here — the shipped pre-#3487 Android behavior.
|
|
1865
|
+
pendingStreamStarts.remove(streamId, start)
|
|
1866
|
+
start.pending.reject(streamStatusException(event, "stream_start_failed"))
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
else -> Unit
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
val stop = synchronized(oneShotLock) { pendingStreamStop }
|
|
1874
|
+
if (stop != null && streamStatusMatches(event, stop.streamId)) {
|
|
1875
|
+
when {
|
|
1876
|
+
isAlreadyStoppedStreamStatus(event) -> {
|
|
1877
|
+
synchronized(oneShotLock) {
|
|
1878
|
+
if (pendingStreamStop === stop) {
|
|
1879
|
+
pendingStreamStop = null
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
rejectStreamStartsSupersededByStop(stop.seq)
|
|
1883
|
+
stop.pending.resolve(stoppedStreamEvent(event, stop.streamId))
|
|
1884
|
+
}
|
|
1885
|
+
event.state == StreamState.ERROR || event.state == StreamState.RECONNECT_FAILED -> {
|
|
1886
|
+
synchronized(oneShotLock) {
|
|
1887
|
+
if (pendingStreamStop === stop) {
|
|
1888
|
+
pendingStreamStop = null
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
stop.pending.reject(streamStatusException(event, "stream_stop_failed"))
|
|
1892
|
+
}
|
|
1893
|
+
else -> Unit
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// The glasses process BLE commands FIFO and every start_stream begins by
|
|
1899
|
+
// stopping whatever runs, so an id-carrying status for start X in one of
|
|
1900
|
+
// these states proves every lower-seq start has already been preempted.
|
|
1901
|
+
// Their only verdict on current firmware is a streamId-less stopped, which
|
|
1902
|
+
// the id-less heuristic deliberately ignores — without this they would run
|
|
1903
|
+
// out the 30s timeout instead of failing fast.
|
|
1904
|
+
private fun rejectPreemptedStreamStarts(winnerSeq: Long) {
|
|
1905
|
+
for ((streamId, start) in pendingStreamStarts) {
|
|
1906
|
+
if (start.seq < winnerSeq && pendingStreamStarts.remove(streamId, start)) {
|
|
1907
|
+
start.pending.reject(
|
|
1908
|
+
BluetoothSdkException(
|
|
1909
|
+
"stream_preempted",
|
|
1910
|
+
"start stream $streamId was preempted by a newer start_stream; " +
|
|
1911
|
+
"the glasses run a single stream and the last request wins.",
|
|
1912
|
+
)
|
|
1913
|
+
)
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// The glasses process BLE commands FIFO, so this stop's ack also settles
|
|
1919
|
+
// every start sent before it: whatever those starts brought up (or would
|
|
1920
|
+
// have brought up) has been stopped, and no further status will arrive
|
|
1921
|
+
// for them. Without this they would run out the 30s start timeout.
|
|
1922
|
+
// Starts sent after the stop keep waiting for their own statuses.
|
|
1923
|
+
private fun rejectStreamStartsSupersededByStop(stopSeq: Long) {
|
|
1924
|
+
for ((streamId, start) in pendingStreamStarts) {
|
|
1925
|
+
if (start.seq < stopSeq && pendingStreamStarts.remove(streamId, start)) {
|
|
1926
|
+
start.pending.reject(
|
|
1927
|
+
BluetoothSdkException(
|
|
1928
|
+
"stream_stopped",
|
|
1929
|
+
"start stream $streamId was superseded by a stop_stream issued after it.",
|
|
1930
|
+
)
|
|
1931
|
+
)
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private fun matchingStreamStart(event: StreamStatusEvent): Pair<String, PendingStreamStart>? {
|
|
1937
|
+
val streamId = event.streamId
|
|
1938
|
+
if (!streamId.isNullOrBlank()) {
|
|
1939
|
+
val start = pendingStreamStarts[streamId] ?: return null
|
|
1940
|
+
return streamId to start
|
|
1941
|
+
}
|
|
1942
|
+
if (event.state == StreamState.ERROR) {
|
|
1943
|
+
// An id-less error is a command-level preflight rejection (missing
|
|
1944
|
+
// URL, low battery, no WiFi) emitted synchronously, while lifecycle
|
|
1945
|
+
// statuses arrive async — so with overlapping starts the wire is
|
|
1946
|
+
// genuinely ambiguous about which start it answers. Attribute it to
|
|
1947
|
+
// the newest pending start: the common case is a later start
|
|
1948
|
+
// failing preflight while an earlier one is already emitting
|
|
1949
|
+
// lifecycle statuses, and at worst this swaps which start carries
|
|
1950
|
+
// the error instead of letting both time out.
|
|
1951
|
+
val entry = pendingStreamStarts.entries.maxByOrNull { it.value.seq } ?: return null
|
|
1952
|
+
return entry.key to entry.value
|
|
1953
|
+
}
|
|
1954
|
+
if (pendingStreamStarts.size == 1) {
|
|
1955
|
+
// firstOrNull: timeouts and other handler threads mutate the
|
|
1956
|
+
// ConcurrentHashMap concurrently, so the map can empty between the
|
|
1957
|
+
// size check and this read; bail out instead of throwing.
|
|
1958
|
+
val entry = pendingStreamStarts.entries.firstOrNull() ?: return null
|
|
1959
|
+
// A streamId-less STOPPED is the glasses' stop-ack for a PREVIOUS
|
|
1960
|
+
// stream (their stop ack carries no streamId), not a verdict on the
|
|
1961
|
+
// pending start — a start_stream that replaces a running publisher
|
|
1962
|
+
// emits exactly this sequence (stopped -> initializing -> streaming).
|
|
1963
|
+
// Attributing it here would reject a start that is about to succeed.
|
|
1964
|
+
// After a stashed `willRetry` error for the pending start, though,
|
|
1965
|
+
// the stopped is the retrying publisher's wind-down (the stop-ack
|
|
1966
|
+
// always precedes any status for the new start), so it is that
|
|
1967
|
+
// start's verdict.
|
|
1968
|
+
if (event.state == StreamState.STOPPED && entry.value.lastError == null) {
|
|
1969
|
+
return null
|
|
1970
|
+
}
|
|
1971
|
+
return entry.key to entry.value
|
|
1972
|
+
}
|
|
1973
|
+
return null
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
private fun streamStatusMatches(event: StreamStatusEvent, streamId: String?): Boolean =
|
|
1977
|
+
streamId.isNullOrBlank() || event.streamId.isNullOrBlank() || event.streamId == streamId
|
|
1978
|
+
|
|
1979
|
+
private fun isAlreadyStoppedStreamStatus(event: StreamStatusEvent): Boolean {
|
|
1980
|
+
if (event.state == StreamState.STOPPED) {
|
|
1981
|
+
return true
|
|
1982
|
+
}
|
|
1983
|
+
val details = (event.status as? StreamStatus.Error)?.errorDetails?.lowercase()
|
|
1984
|
+
return details in setOf("not_streaming", "already_stopped", "not streaming")
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
private fun stoppedStreamEvent(event: StreamStatusEvent, streamId: String?): StreamStatusEvent =
|
|
1988
|
+
StreamStatusEvent(
|
|
1989
|
+
StreamStatus.Lifecycle(
|
|
1990
|
+
state = StreamState.STOPPED,
|
|
1991
|
+
streamId = event.streamId ?: streamId,
|
|
1992
|
+
timestamp = event.status.timestamp ?: System.currentTimeMillis(),
|
|
1993
|
+
resolvedConfig = event.resolvedConfig,
|
|
1994
|
+
)
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
private fun streamStatusException(event: StreamStatusEvent, code: String): BluetoothSdkException {
|
|
1998
|
+
val details = (event.status as? StreamStatus.Error)?.errorDetails
|
|
1999
|
+
?: "Stream status ${event.state.value}"
|
|
2000
|
+
return BluetoothSdkException(code, details)
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
private fun handlePhotoResponseForRequests(event: PhotoResponseEvent) {
|
|
2004
|
+
val pending = pendingPhotoRequests[event.requestId] ?: return
|
|
2005
|
+
when (val response = event.response) {
|
|
2006
|
+
is PhotoResponse.Success -> pending.resolve(event)
|
|
2007
|
+
is PhotoResponse.Error ->
|
|
2008
|
+
pending.reject(
|
|
2009
|
+
BluetoothSdkException(
|
|
2010
|
+
response.errorCode ?: "photo_request_failed",
|
|
2011
|
+
response.errorMessage,
|
|
2012
|
+
)
|
|
2013
|
+
)
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
private fun handleCameraStatusForRequests(event: CameraStatusEvent) {
|
|
2018
|
+
val pending = pendingCameraStatusRequests[event.requestId] ?: return
|
|
2019
|
+
when (event.state.lowercase()) {
|
|
2020
|
+
"ready" -> pending.resolve(event)
|
|
2021
|
+
"error" ->
|
|
2022
|
+
pending.reject(
|
|
2023
|
+
BluetoothSdkException(
|
|
2024
|
+
event.errorCode ?: "camera_warm_up_failed",
|
|
2025
|
+
event.errorMessage ?: "Camera warm-up failed.",
|
|
2026
|
+
)
|
|
2027
|
+
)
|
|
2028
|
+
// "warming"/"stopped" are progress updates; leave the pending promise alone.
|
|
2029
|
+
else -> {}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
private fun handleVideoRecordingStatusForRequests(event: VideoRecordingStatusEvent) {
|
|
2034
|
+
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
2035
|
+
if (event.success) {
|
|
2036
|
+
if (event.status == request.expectedStatus) {
|
|
2037
|
+
if (request.waitForUpload) {
|
|
2038
|
+
request.stoppedEvent = event
|
|
2039
|
+
if (request.uploadSucceeded) {
|
|
2040
|
+
request.pending.resolve(event)
|
|
2041
|
+
}
|
|
2042
|
+
} else {
|
|
2043
|
+
request.pending.resolve(event)
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
} else {
|
|
2047
|
+
request.pending.reject(
|
|
2048
|
+
BluetoothSdkException(
|
|
2049
|
+
event.status.ifBlank { "video_recording_failed" },
|
|
2050
|
+
event.details ?: "Video recording command failed.",
|
|
2051
|
+
)
|
|
2052
|
+
)
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
private fun handleMediaUploadForRequests(event: MediaUploadEvent) {
|
|
2057
|
+
if (!event.isVideo) return
|
|
2058
|
+
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
2059
|
+
if (!request.waitForUpload) return
|
|
2060
|
+
if (event.isSuccess) {
|
|
2061
|
+
val stoppedEvent = request.stoppedEvent
|
|
2062
|
+
if (stoppedEvent != null) {
|
|
2063
|
+
request.pending.resolve(stoppedEvent)
|
|
2064
|
+
} else {
|
|
2065
|
+
request.uploadSucceeded = true
|
|
2066
|
+
}
|
|
2067
|
+
} else {
|
|
2068
|
+
request.pending.reject(
|
|
2069
|
+
BluetoothSdkException(
|
|
2070
|
+
"video_upload_failed",
|
|
2071
|
+
event.errorMessage ?: "Video upload failed.",
|
|
2072
|
+
)
|
|
2073
|
+
)
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
private fun handleRgbLedResponseForRequests(event: RgbLedControlResponseEvent) {
|
|
2078
|
+
val pending = pendingRgbLedRequests[event.requestId] ?: return
|
|
2079
|
+
if (event.state == "success") {
|
|
2080
|
+
pending.resolve(event)
|
|
2081
|
+
} else {
|
|
2082
|
+
pending.reject(
|
|
2083
|
+
BluetoothSdkException(
|
|
2084
|
+
event.errorCode ?: "rgb_led_control_failed",
|
|
2085
|
+
event.errorCode ?: "RGB LED command failed.",
|
|
2086
|
+
)
|
|
2087
|
+
)
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
private fun handleSettingsAckForRequests(event: SettingsAckEvent) {
|
|
2092
|
+
val pending = pendingSettingsRequests[event.requestId] ?: return
|
|
2093
|
+
if (isFailureStatus(event.status)) {
|
|
2094
|
+
pending.reject(
|
|
2095
|
+
BluetoothSdkException(
|
|
2096
|
+
event.errorCode ?: "${event.setting.ifBlank { "settings" }}_failed",
|
|
2097
|
+
event.errorMessage ?: "Settings command ${event.setting.ifBlank { event.requestId }} failed.",
|
|
2098
|
+
)
|
|
2099
|
+
)
|
|
2100
|
+
} else {
|
|
2101
|
+
pending.resolve(event)
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
private fun isFailureStatus(status: String): Boolean =
|
|
2106
|
+
status.lowercase() in setOf("error", "failed", "failure", "rejected")
|
|
2107
|
+
|
|
2108
|
+
private fun handleWifiScanResultsForRequests(results: List<WifiScanResult>) {
|
|
2109
|
+
val request = synchronized(oneShotLock) { pendingWifiScan } ?: return
|
|
2110
|
+
synchronized(oneShotLock) {
|
|
2111
|
+
if (pendingWifiScan === request) {
|
|
2112
|
+
pendingWifiScan = null
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
request.pending.resolve(results)
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// scanId-echoing glasses: results for another scan are ignored instead of
|
|
2119
|
+
// resolving the pending request, and matching chunks accumulate until the
|
|
2120
|
+
// glasses flag the scan complete.
|
|
2121
|
+
private fun handleCorrelatedWifiScanChunk(
|
|
2122
|
+
scanId: String,
|
|
2123
|
+
results: List<WifiScanResult>,
|
|
2124
|
+
scanComplete: Boolean,
|
|
2125
|
+
) {
|
|
2126
|
+
val request: PendingWifiScan
|
|
2127
|
+
val accumulated: List<WifiScanResult>
|
|
2128
|
+
synchronized(oneShotLock) {
|
|
2129
|
+
request = pendingWifiScan?.takeIf { it.scanId == scanId } ?: return
|
|
2130
|
+
for (network in results) {
|
|
2131
|
+
if (request.accumulated.none { it.ssid == network.ssid }) {
|
|
2132
|
+
request.accumulated.add(network)
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
accumulated = request.accumulated.toList()
|
|
2136
|
+
if (accumulated.isNotEmpty()) {
|
|
2137
|
+
request.latestResults = accumulated
|
|
2138
|
+
}
|
|
2139
|
+
if (!scanComplete) return
|
|
2140
|
+
if (pendingWifiScan === request) {
|
|
2141
|
+
pendingWifiScan = null
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
request.pending.resolve(accumulated)
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
private fun updateWifiScanLatestResults(results: List<WifiScanResult>) {
|
|
2148
|
+
if (results.isEmpty()) return
|
|
2149
|
+
synchronized(oneShotLock) {
|
|
2150
|
+
pendingWifiScan?.latestResults = results
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
2155
|
+
val request = synchronized(oneShotLock) { pendingWifiStatus } ?: return
|
|
2156
|
+
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2157
|
+
// verdict for the in-flight connect: reject now instead of running out the
|
|
2158
|
+
// request timeout. Only the error field counts as failure — the glasses'
|
|
2159
|
+
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2160
|
+
// credentials while association is still in progress, and rejecting on that
|
|
2161
|
+
// would kill every connect attempt early.
|
|
2162
|
+
if (request.operation == WifiStatusOperation.CONNECT && event.error != null) {
|
|
2163
|
+
synchronized(oneShotLock) {
|
|
2164
|
+
if (pendingWifiStatus === request) {
|
|
2165
|
+
pendingWifiStatus = null
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
request.pending.reject(
|
|
2169
|
+
BluetoothSdkException(
|
|
2170
|
+
event.error,
|
|
2171
|
+
"Glasses failed to join \"${request.ssid}\": ${event.error}",
|
|
2172
|
+
)
|
|
2173
|
+
)
|
|
2174
|
+
return
|
|
2175
|
+
}
|
|
2176
|
+
if (!wifiStatusMatches(event.status, request)) return
|
|
2177
|
+
synchronized(oneShotLock) {
|
|
2178
|
+
if (pendingWifiStatus === request) {
|
|
2179
|
+
pendingWifiStatus = null
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
request.pending.resolve(event)
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
private fun wifiStatusMatches(status: WifiStatus, request: PendingWifiStatusRequest): Boolean =
|
|
2186
|
+
when (request.operation) {
|
|
2187
|
+
WifiStatusOperation.CONNECT ->
|
|
2188
|
+
status is WifiStatus.Connected && status.ssid == request.ssid
|
|
2189
|
+
WifiStatusOperation.FORGET ->
|
|
2190
|
+
status == WifiStatus.Disconnected || (status is WifiStatus.Connected && status.ssid != request.ssid)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
private fun handleHotspotStatusForRequests(event: HotspotStatusEvent) {
|
|
2194
|
+
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
2195
|
+
if (!hotspotStatusMatches(event.status, request.enabled)) return
|
|
2196
|
+
synchronized(oneShotLock) {
|
|
2197
|
+
if (pendingHotspotStatus === request) {
|
|
2198
|
+
pendingHotspotStatus = null
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
request.pending.resolve(event)
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
private fun hotspotStatusMatches(status: HotspotStatus, enabled: Boolean): Boolean =
|
|
2205
|
+
if (enabled) {
|
|
2206
|
+
status is HotspotStatus.Enabled
|
|
2207
|
+
} else {
|
|
2208
|
+
status == HotspotStatus.Disabled
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
private fun handleHotspotErrorForRequests(event: HotspotErrorEvent) {
|
|
2212
|
+
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
2213
|
+
synchronized(oneShotLock) {
|
|
2214
|
+
if (pendingHotspotStatus === request) {
|
|
2215
|
+
pendingHotspotStatus = null
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
request.pending.reject(
|
|
2219
|
+
BluetoothSdkException(
|
|
2220
|
+
"hotspot_command_failed",
|
|
2221
|
+
event.message ?: "Hotspot command failed.",
|
|
2222
|
+
)
|
|
2223
|
+
)
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
private fun sendNextStreamKeepAlive(tracker: ActiveStreamKeepAlive) {
|
|
2227
|
+
var timeoutEvent: StreamStatusEvent? = null
|
|
2228
|
+
var request: StreamKeepAliveRequest? = null
|
|
2229
|
+
|
|
2230
|
+
synchronized(streamKeepAliveLock) {
|
|
2231
|
+
if (activeStreamKeepAlive !== tracker) {
|
|
2232
|
+
return
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (tracker.armed && tracker.pendingAckId != null) {
|
|
2236
|
+
tracker.missedAckCount += 1
|
|
2237
|
+
if (tracker.missedAckCount >= MAX_MISSED_STREAM_KEEP_ALIVE_ACKS) {
|
|
2238
|
+
activeStreamKeepAlive = null
|
|
2239
|
+
tracker.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
2240
|
+
timeoutEvent =
|
|
2241
|
+
StreamStatusEvent(
|
|
2242
|
+
StreamStatus.Error(
|
|
2243
|
+
streamId = tracker.streamId,
|
|
2244
|
+
errorDetails =
|
|
2245
|
+
"Stream keep-alive timed out after ${tracker.missedAckCount} missed ACKs",
|
|
2246
|
+
timestamp = System.currentTimeMillis(),
|
|
2247
|
+
resolvedConfig = null,
|
|
2248
|
+
)
|
|
2249
|
+
)
|
|
2250
|
+
return@synchronized
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
val ackId = "ack-${System.currentTimeMillis()}"
|
|
2255
|
+
tracker.pendingAckId = ackId
|
|
2256
|
+
request = StreamKeepAliveRequest(streamId = tracker.streamId, ackId = ackId)
|
|
2257
|
+
val nextTick = Runnable { sendNextStreamKeepAlive(tracker) }
|
|
2258
|
+
tracker.nextTick = nextTick
|
|
2259
|
+
mainHandler.postDelayed(nextTick, tracker.intervalMs)
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
timeoutEvent?.let { event ->
|
|
2263
|
+
dispatchToListeners { it.onStreamStatus(event) }
|
|
2264
|
+
stopStreamKeepAliveMonitor()
|
|
2265
|
+
deviceManager.stopStream()
|
|
2266
|
+
return
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
request?.let { keepAlive ->
|
|
2270
|
+
deviceManager.keepStreamAlive(keepAlive.toMap().toMutableMap())
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
private fun handleStreamKeepAliveAck(event: KeepAliveAckEvent): Boolean {
|
|
2275
|
+
synchronized(streamKeepAliveLock) {
|
|
2276
|
+
val tracker = activeStreamKeepAlive ?: return false
|
|
2277
|
+
if (event.streamId != tracker.streamId || event.ackId != tracker.pendingAckId) {
|
|
2278
|
+
return false
|
|
2279
|
+
}
|
|
2280
|
+
tracker.pendingAckId = null
|
|
2281
|
+
tracker.missedAckCount = 0
|
|
2282
|
+
return true
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private fun handleStreamStatusForKeepAlive(status: StreamStatus) {
|
|
2287
|
+
val streamId = status.streamId
|
|
2288
|
+
val activeStreamId = synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId }
|
|
2289
|
+
if (streamId == null || activeStreamId != streamId) {
|
|
2290
|
+
return
|
|
2291
|
+
}
|
|
2292
|
+
when (status.state) {
|
|
2293
|
+
StreamState.STOPPED,
|
|
2294
|
+
StreamState.STOPPING,
|
|
2295
|
+
StreamState.ERROR,
|
|
2296
|
+
StreamState.RECONNECT_FAILED -> stopStreamKeepAliveMonitor()
|
|
2297
|
+
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
2298
|
+
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
2299
|
+
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
2300
|
+
// pre-arm bookkeeping so a stale unacked id (sent before the glasses could ACK)
|
|
2301
|
+
// can't immediately count as a miss.
|
|
2302
|
+
else ->
|
|
2303
|
+
synchronized(streamKeepAliveLock) {
|
|
2304
|
+
activeStreamKeepAlive?.let {
|
|
2305
|
+
if (it.streamId == streamId && !it.armed) {
|
|
2306
|
+
it.armed = true
|
|
2307
|
+
it.pendingAckId = null
|
|
2308
|
+
it.missedAckCount = 0
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/** OTA status messages are not request-correlated, so only known pre-ack failures settle a start. */
|
|
2317
|
+
internal fun otaStartRejectionErrorCode(event: OtaStatusEvent): String? =
|
|
2318
|
+
if (
|
|
2319
|
+
event.status.equals("failed", ignoreCase = true) &&
|
|
2320
|
+
event.errorMessage.equals("battery_low", ignoreCase = true)
|
|
2321
|
+
) {
|
|
2322
|
+
"battery_low"
|
|
2323
|
+
} else {
|
|
2324
|
+
null
|
|
2325
|
+
}
|