@glassly/bluetooth-sdk 0.1.0-dev.26

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