@glassly/bluetooth-sdk 0.1.0-dev.100

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