@mentra/bluetooth-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +278 -0
  2. package/android/build.gradle +167 -0
  3. package/android/lc3Lib/CMakeLists.txt +6 -0
  4. package/android/lc3Lib/build.gradle +109 -0
  5. package/android/lc3Lib/proguard-rules.pro +33 -0
  6. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  7. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  8. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  26. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  81. package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
  82. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  83. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  86. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  119. package/android/settings.gradle +1 -0
  120. package/android/src/main/AndroidManifest.xml +40 -0
  121. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
  122. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
  123. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
  124. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
  125. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
  126. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
  127. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
  128. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  129. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
  130. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
  131. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
  132. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
  133. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
  134. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
  135. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
  136. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
  137. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
  138. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
  139. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
  140. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
  141. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
  142. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
  143. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
  144. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  145. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  146. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
  147. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
  148. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
  149. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  150. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
  151. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
  152. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
  153. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
  154. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
  155. package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  156. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  157. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  158. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  159. package/android/src/main/res/values/strings.xml +4 -0
  160. package/app.plugin.js +1 -0
  161. package/build/BluetoothSdk.types.d.ts +498 -0
  162. package/build/BluetoothSdk.types.d.ts.map +1 -0
  163. package/build/BluetoothSdk.types.js +23 -0
  164. package/build/BluetoothSdk.types.js.map +1 -0
  165. package/build/BluetoothSdkModule.d.ts +71 -0
  166. package/build/BluetoothSdkModule.d.ts.map +1 -0
  167. package/build/BluetoothSdkModule.js +106 -0
  168. package/build/BluetoothSdkModule.js.map +1 -0
  169. package/build/index.d.ts +3 -0
  170. package/build/index.d.ts.map +1 -0
  171. package/build/index.js +5 -0
  172. package/build/index.js.map +1 -0
  173. package/expo-module.config.json +9 -0
  174. package/ios/BluetoothSdkModule.swift +665 -0
  175. package/ios/MentraBluetoothSDK.podspec +77 -0
  176. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  177. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  178. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  179. package/ios/Packages/CoreObjC/attdet.c +92 -0
  180. package/ios/Packages/CoreObjC/attdet.h +44 -0
  181. package/ios/Packages/CoreObjC/bits.c +375 -0
  182. package/ios/Packages/CoreObjC/bits.h +315 -0
  183. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  184. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  185. package/ios/Packages/CoreObjC/common.h +151 -0
  186. package/ios/Packages/CoreObjC/energy.c +70 -0
  187. package/ios/Packages/CoreObjC/energy.h +43 -0
  188. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  189. package/ios/Packages/CoreObjC/lc3.c +704 -0
  190. package/ios/Packages/CoreObjC/lc3.h +313 -0
  191. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  192. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  193. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  194. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  195. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  196. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  197. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  198. package/ios/Packages/CoreObjC/mdct.c +469 -0
  199. package/ios/Packages/CoreObjC/mdct.h +57 -0
  200. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  201. package/ios/Packages/CoreObjC/meson.build +61 -0
  202. package/ios/Packages/CoreObjC/plc.c +61 -0
  203. package/ios/Packages/CoreObjC/plc.h +57 -0
  204. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  205. package/ios/Packages/CoreObjC/sns.c +880 -0
  206. package/ios/Packages/CoreObjC/sns.h +103 -0
  207. package/ios/Packages/CoreObjC/spec.c +907 -0
  208. package/ios/Packages/CoreObjC/spec.h +119 -0
  209. package/ios/Packages/CoreObjC/tables.c +3457 -0
  210. package/ios/Packages/CoreObjC/tables.h +94 -0
  211. package/ios/Packages/CoreObjC/tns.c +457 -0
  212. package/ios/Packages/CoreObjC/tns.h +99 -0
  213. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  214. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  215. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
  216. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
  217. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
  218. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
  219. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
  220. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  221. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  222. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  223. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  224. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
  225. package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
  226. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
  227. package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
  228. package/ios/Packages/VAD/Data/Configuration.swift +68 -0
  229. package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
  230. package/ios/Packages/VAD/Data/Record.swift +13 -0
  231. package/ios/Packages/VAD/Data/Result.swift +22 -0
  232. package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
  233. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  234. package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
  235. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
  236. package/ios/Packages/libbz2/module.modulemap +5 -0
  237. package/ios/Packages/libbz2/shim.h +1 -0
  238. package/ios/Source/Bridge.swift +353 -0
  239. package/ios/Source/Bridging-Header.h +16 -0
  240. package/ios/Source/DeviceManager.swift +1478 -0
  241. package/ios/Source/DeviceStore.swift +295 -0
  242. package/ios/Source/MentraBluetoothSDK.swift +2439 -0
  243. package/ios/Source/ObservableStore.swift +88 -0
  244. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  245. package/ios/Source/controllers/ControllerManager.swift +187 -0
  246. package/ios/Source/controllers/R1.swift +827 -0
  247. package/ios/Source/services/PhoneMic.swift +662 -0
  248. package/ios/Source/sgcs/Frame.swift +651 -0
  249. package/ios/Source/sgcs/G1.swift +2505 -0
  250. package/ios/Source/sgcs/G2.swift +3730 -0
  251. package/ios/Source/sgcs/Mach1.swift +543 -0
  252. package/ios/Source/sgcs/MentraLive.swift +4708 -0
  253. package/ios/Source/sgcs/MentraNex.swift +2479 -0
  254. package/ios/Source/sgcs/SGCManager.swift +243 -0
  255. package/ios/Source/sgcs/Simulated.swift +270 -0
  256. package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
  257. package/ios/Source/stt/STTTools.swift +154 -0
  258. package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
  259. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  260. package/ios/Source/utils/Constants.swift +74 -0
  261. package/ios/Source/utils/Enums.swift +95 -0
  262. package/ios/Source/utils/G1Text.swift +2067 -0
  263. package/ios/Source/utils/JSCExperiment.swift +241 -0
  264. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  265. package/ios/Source/utils/MessageChunker.swift +164 -0
  266. package/ios/Source/utils/Models.swift +135 -0
  267. package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
  268. package/ios/Source/utils/TarBz2Extractor.swift +206 -0
  269. package/package.json +78 -0
  270. package/plugin/build/index.d.ts +6 -0
  271. package/plugin/build/index.js +12 -0
  272. package/plugin/build/withAndroid.d.ts +4 -0
  273. package/plugin/build/withAndroid.js +80 -0
  274. package/plugin/build/withIos.d.ts +4 -0
  275. package/plugin/build/withIos.js +64 -0
  276. package/src/BluetoothSdk.types.ts +581 -0
  277. package/src/BluetoothSdkModule.ts +259 -0
  278. package/src/index.ts +4 -0
@@ -0,0 +1,1109 @@
1
+ package com.mentra.bluetoothsdk.services
2
+
3
+ import android.Manifest
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothHeadset
7
+ import android.content.BroadcastReceiver
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.IntentFilter
11
+ import android.content.pm.PackageManager
12
+ import android.media.*
13
+ import android.os.Build
14
+ import android.os.Handler
15
+ import android.os.Looper
16
+ import android.telephony.PhoneStateListener
17
+ import android.telephony.TelephonyManager
18
+ import androidx.core.app.ActivityCompat
19
+ import androidx.core.content.ContextCompat
20
+ import com.mentra.bluetoothsdk.Bridge
21
+ import com.mentra.bluetoothsdk.DeviceManager
22
+ import com.mentra.bluetoothsdk.utils.MicTypes
23
+ import java.nio.ByteBuffer
24
+ import java.nio.ByteOrder
25
+ import java.util.concurrent.atomic.AtomicBoolean
26
+ import kotlinx.coroutines.*
27
+
28
+ class PhoneMic private constructor(private val context: Context) {
29
+
30
+ companion object {
31
+ @Volatile private var instance: PhoneMic? = null
32
+
33
+ fun getInstance(): PhoneMic {
34
+ return instance
35
+ ?: synchronized(this) {
36
+ instance ?: PhoneMic(Bridge.getContext()).also { instance = it }
37
+ }
38
+ }
39
+
40
+ // Audio configuration constants
41
+ private const val SAMPLE_RATE = 16000
42
+ private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
43
+ private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
44
+ private const val BUFFER_SIZE_MULTIPLIER = 2
45
+
46
+ // Debouncing and retry constants
47
+ private const val MODE_CHANGE_DEBOUNCE_MS = 500L
48
+ private const val MAX_SCO_RETRIES = 3
49
+ private const val FOCUS_REGAIN_DELAY_MS = 500L
50
+ private const val SAMSUNG_MIC_TEST_DELAY_MS = 500L
51
+ private const val MIC_SWITCH_DELAY_MS = 300L // Time for DeviceManager to switch mics
52
+ }
53
+
54
+ // Audio recording components
55
+ private var audioRecord: AudioRecord? = null
56
+ private var recordingThread: Thread? = null
57
+ public val isRecording = AtomicBoolean(false)
58
+ private var preferScoMode = true // Try SCO first, fallback to normal if needed
59
+
60
+ // Audio manager and routing
61
+ private val audioManager: AudioManager =
62
+ context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
63
+ private var bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
64
+ private var bluetoothHeadset: BluetoothHeadset? = null
65
+
66
+ // Broadcast receivers
67
+ private var audioRouteReceiver: BroadcastReceiver? = null
68
+ private var bluetoothReceiver: BroadcastReceiver? = null
69
+
70
+ // Phone call detection
71
+ private var telephonyManager: TelephonyManager? = null
72
+ private var phoneStateListener: PhoneStateListener? = null
73
+ private var isPhoneCallActive = false
74
+
75
+ // Audio focus management
76
+ private var audioFocusListener: AudioManager.OnAudioFocusChangeListener? = null
77
+ private var hasAudioFocus = false
78
+ private var audioFocusRequest: AudioFocusRequest? = null // For Android 8.0+
79
+
80
+ // Audio recording conflict detection (API 24+)
81
+ private var audioRecordingCallback: AudioManager.AudioRecordingCallback? = null
82
+ private val ourAudioSessionIds = mutableListOf<Int>()
83
+ private var isExternalAudioActive = false
84
+
85
+ // State tracking
86
+ private var lastModeChangeTime = 0L
87
+ private var scoRetries = 0
88
+ private var pendingRecordingRequest = false
89
+ private var currentMicMode: String = ""
90
+
91
+ // Handler for main thread operations
92
+ private val mainHandler = Handler(Looper.getMainLooper())
93
+
94
+ // Coroutine scope for async operations
95
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
96
+
97
+ init {
98
+ setupPhoneCallDetection()
99
+ setupAudioFocusListener()
100
+ setupAudioRecordingDetection()
101
+ setupAudioRouteListener()
102
+ setupBluetoothListener()
103
+ }
104
+
105
+ // MARK: - Public Methods (Simplified Interface)
106
+
107
+ /**
108
+ * Start recording from the phone microphone Will automatically handle SCO mode, conflicts, and
109
+ * fallbacks
110
+ */
111
+ fun startRecording(): Boolean {
112
+ // Ensure we're on main thread for consistency
113
+ if (Looper.myLooper() != Looper.getMainLooper()) {
114
+ var result = false
115
+ runBlocking { withContext(Dispatchers.Main) { result = startRecording() } }
116
+ return result
117
+ }
118
+
119
+ // Check if already recording
120
+ if (isRecording.get()) {
121
+ Bridge.log("MIC: Already recording")
122
+ return true
123
+ }
124
+
125
+ // Check permissions
126
+ if (!checkPermissions()) {
127
+ Bridge.log("MIC: Microphone permissions not granted")
128
+ notifyDeviceManager("permission_denied", emptyList())
129
+ return false
130
+ }
131
+
132
+ // Smart debouncing
133
+ val now = System.currentTimeMillis()
134
+ if (now - lastModeChangeTime < MODE_CHANGE_DEBOUNCE_MS) {
135
+ Bridge.log("MIC: Debouncing rapid recording request")
136
+ pendingRecordingRequest = true
137
+ mainHandler.postDelayed(
138
+ {
139
+ if (pendingRecordingRequest && !isRecording.get()) {
140
+ startRecordingInternal()
141
+ }
142
+ pendingRecordingRequest = false
143
+ },
144
+ MODE_CHANGE_DEBOUNCE_MS
145
+ )
146
+ return false
147
+ }
148
+
149
+ return startRecordingInternal()
150
+ }
151
+
152
+ /** Stop recording from the phone microphone */
153
+ fun stopRecording() {
154
+ // Ensure we're on main thread for consistency
155
+ if (Looper.myLooper() != Looper.getMainLooper()) {
156
+ runBlocking { withContext(Dispatchers.Main) { stopRecording() } }
157
+ return
158
+ }
159
+
160
+ if (!isRecording.get()) {
161
+ return
162
+ }
163
+
164
+ Bridge.log("MIC: Stopping recording")
165
+
166
+ // Clean up recording
167
+ cleanUpRecording()
168
+
169
+ // Abandon audio focus
170
+ abandonAudioFocus()
171
+
172
+ // Reset Bluetooth SCO
173
+ if (audioManager.isBluetoothScoOn) {
174
+ audioManager.stopBluetoothSco()
175
+ audioManager.isBluetoothScoOn = false
176
+ }
177
+
178
+ // Reset audio mode
179
+ audioManager.mode = AudioManager.MODE_NORMAL
180
+
181
+ // Notify DeviceManager
182
+ notifyDeviceManager("recording_stopped", getAvailableInputDevices().values.toList())
183
+
184
+ Bridge.log("MIC: Recording stopped")
185
+ }
186
+
187
+ fun isRecordingWithMode(mode: String): Boolean {
188
+ return isRecording.get() && currentMicMode == mode
189
+ }
190
+
191
+ /**
192
+ * Start recording from a specific microphone type
193
+ * @param mode One of MicTypes constants (PHONE_INTERNAL, BT_CLASSIC, BT)
194
+ * @return true if successfully started recording, false otherwise
195
+ */
196
+ fun startMode(mode: String): Boolean {
197
+ // Ensure we're on main thread for consistency
198
+ if (Looper.myLooper() != Looper.getMainLooper()) {
199
+ var result = false
200
+ runBlocking { withContext(Dispatchers.Main) { result = startMode(mode) } }
201
+ return result
202
+ }
203
+
204
+ if (isRecordingWithMode(mode)) {
205
+ return true
206
+ }
207
+
208
+ // recording with a different mode, so stop recording and start recording with the new mode:
209
+ if (isRecording.get()) {
210
+ Bridge.log(
211
+ "MIC: Already recording with different mode ($currentMicMode), stopping first"
212
+ )
213
+ stopRecording()
214
+ }
215
+
216
+ // Check permissions
217
+ if (!checkPermissions()) {
218
+ Bridge.log("MIC: Microphone permissions not granted")
219
+ notifyDeviceManager("permission_denied", emptyList())
220
+ return false
221
+ }
222
+
223
+ // Smart debouncing
224
+ val now = System.currentTimeMillis()
225
+ if (now - lastModeChangeTime < MODE_CHANGE_DEBOUNCE_MS) {
226
+ Bridge.log("MIC: Debouncing rapid recording request")
227
+ return false
228
+ }
229
+
230
+ lastModeChangeTime = System.currentTimeMillis()
231
+
232
+ // Check for conflicts
233
+ if (isPhoneCallActive) {
234
+ Bridge.log("MIC: Cannot start recording - phone call active")
235
+ notifyDeviceManager("phone_call_active", emptyList())
236
+ return false
237
+ }
238
+
239
+ // Request audio focus for all modes (needed to detect Chrome STT)
240
+ if (!requestAudioFocus()) {
241
+ Bridge.log("MIC: Failed to get audio focus")
242
+ if (isSamsungDevice()) {
243
+ testMicrophoneAvailabilityOnSamsung()
244
+ } else {
245
+ notifyDeviceManager("audio_focus_denied", emptyList())
246
+ }
247
+ return false
248
+ }
249
+
250
+ // Start recording based on mode
251
+ return when (mode) {
252
+ MicTypes.PHONE_INTERNAL -> {
253
+ Bridge.log("MIC: Starting phone internal mic")
254
+ return startRecordingPhoneInternal()
255
+ }
256
+ MicTypes.BT_CLASSIC -> {
257
+ Bridge.log("MIC: Starting Bluetooth Classic (SCO)")
258
+ if (!audioManager.isBluetoothScoAvailableOffCall) {
259
+ Bridge.log("MIC: Bluetooth SCO not available")
260
+ notifyDeviceManager("bt_classic_unavailable", emptyList())
261
+ return false
262
+ }
263
+ return startRecordingBtClassic()
264
+ }
265
+ MicTypes.BT -> {
266
+ Bridge.log("MIC: Starting high-quality Bluetooth mic")
267
+ if (!isHighQualityBluetoothAvailable()) {
268
+ Bridge.log("MIC: High-quality Bluetooth not available")
269
+ notifyDeviceManager("bt_hq_unavailable", emptyList())
270
+ return false
271
+ }
272
+ return startRecordingBtHighQuality()
273
+ }
274
+ else -> {
275
+ Bridge.log("MIC: Unknown mic type: $mode")
276
+ return false
277
+ }
278
+ }
279
+ }
280
+
281
+
282
+ fun stopMode(mode: String): Boolean {
283
+ if (isRecordingWithMode(mode)) {
284
+ stopRecording()
285
+ return true
286
+ }
287
+ return false
288
+ }
289
+
290
+ private fun startRecordingPhoneInternal(): Boolean {
291
+ try {
292
+ Bridge.log("MIC: startRecordingPhoneInternal() - Setting up phone mic ONLY")
293
+
294
+ // Use MODE_NORMAL to avoid any Bluetooth routing
295
+ audioManager.mode = AudioManager.MODE_NORMAL
296
+ Bridge.log("MIC: Set audio mode to MODE_NORMAL (${AudioManager.MODE_NORMAL})")
297
+
298
+ // Ensure Bluetooth SCO is off
299
+ if (audioManager.isBluetoothScoOn) {
300
+ Bridge.log("MIC: Bluetooth SCO was on, turning it off")
301
+ audioManager.stopBluetoothSco()
302
+ audioManager.isBluetoothScoOn = false
303
+ }
304
+
305
+ // Disable speaker phone to ensure proper routing
306
+ if (audioManager.isSpeakerphoneOn) {
307
+ Bridge.log("MIC: Speakerphone was on, turning it off")
308
+ audioManager.isSpeakerphoneOn = false
309
+ }
310
+
311
+ // Log current audio routing state
312
+ Bridge.log("MIC: Audio state before recording - Mode: ${audioManager.mode}, " +
313
+ "BT SCO: ${audioManager.isBluetoothScoOn}, " +
314
+ "Speakerphone: ${audioManager.isSpeakerphoneOn}")
315
+
316
+ // EXPERIMENTAL: Skip audio focus for phone internal mic to avoid media routing issues
317
+ // This allows YouTube/Spotify to play normally through speakers
318
+ Bridge.log("MIC: Skipping audio focus request for phone internal mic")
319
+
320
+ val success = createAndStartAudioRecord(MediaRecorder.AudioSource.MIC)
321
+ if (success) {
322
+ currentMicMode = MicTypes.PHONE_INTERNAL
323
+ Bridge.log("MIC: Phone internal mic started successfully")
324
+ } else {
325
+ Bridge.log("MIC: Failed to start phone internal mic")
326
+ }
327
+ return success
328
+ } catch (e: Exception) {
329
+ Bridge.log("MIC: Phone internal recording failed: ${e.message}")
330
+ e.printStackTrace()
331
+ return false
332
+ }
333
+ }
334
+
335
+ private fun startRecordingBtClassic(): Boolean {
336
+ try {
337
+ // Use MODE_IN_COMMUNICATION for SCO
338
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
339
+
340
+ // Start Bluetooth SCO
341
+ audioManager.startBluetoothSco()
342
+ audioManager.isBluetoothScoOn = true
343
+
344
+ // Wait briefly for SCO to connect
345
+ Thread.sleep(100)
346
+
347
+ val success = createAndStartAudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
348
+ if (success) {
349
+ currentMicMode = MicTypes.BT_CLASSIC
350
+ }
351
+ return success
352
+ } catch (e: Exception) {
353
+ Bridge.log("MIC: BT Classic recording failed: ${e.message}")
354
+
355
+ // Clean up SCO
356
+ audioManager.stopBluetoothSco()
357
+ audioManager.isBluetoothScoOn = false
358
+ audioManager.mode = AudioManager.MODE_NORMAL
359
+
360
+ return false
361
+ }
362
+ }
363
+
364
+ private fun startRecordingBtHighQuality(): Boolean {
365
+ try {
366
+ // For high-quality BT devices like AirPods, we want to avoid SCO mode
367
+ // and use the standard microphone source which will route to BT automatically
368
+ audioManager.mode = AudioManager.MODE_NORMAL
369
+
370
+ // Ensure SCO is off - we want standard BT audio profile
371
+ if (audioManager.isBluetoothScoOn) {
372
+ audioManager.stopBluetoothSco()
373
+ audioManager.isBluetoothScoOn = false
374
+ }
375
+
376
+ // Use UNPROCESSED if available for highest quality, otherwise MIC
377
+ val audioSource =
378
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
379
+ MediaRecorder.AudioSource.UNPROCESSED
380
+ } else {
381
+ MediaRecorder.AudioSource.MIC
382
+ }
383
+
384
+ val success = createAndStartAudioRecord(audioSource)
385
+ if (success) {
386
+ currentMicMode = MicTypes.BT
387
+ }
388
+ return success
389
+ } catch (e: Exception) {
390
+ Bridge.log("MIC: BT high-quality recording failed: ${e.message}")
391
+ return false
392
+ }
393
+ }
394
+
395
+ private fun isHighQualityBluetoothAvailable(): Boolean {
396
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
397
+ val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
398
+ // Look for Bluetooth A2DP or LE Audio devices (not just SCO)
399
+ return devices.any { device ->
400
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
401
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
402
+ device.type == AudioDeviceInfo.TYPE_BLE_HEADSET)
403
+ }
404
+ }
405
+ // Fallback: if any Bluetooth audio device is connected
406
+ return bluetoothAdapter?.let { adapter ->
407
+ if (ActivityCompat.checkSelfPermission(
408
+ context,
409
+ Manifest.permission.BLUETOOTH_CONNECT
410
+ ) == PackageManager.PERMISSION_GRANTED
411
+ ) {
412
+ adapter.bondedDevices.any { device ->
413
+ device.bluetoothClass?.majorDeviceClass ==
414
+ android.bluetooth.BluetoothClass.Device.Major.AUDIO_VIDEO
415
+ }
416
+ } else {
417
+ false
418
+ }
419
+ }
420
+ ?: false
421
+ }
422
+
423
+ // MARK: - Private Methods
424
+
425
+ private fun startRecordingInternal(): Boolean {
426
+ lastModeChangeTime = System.currentTimeMillis()
427
+
428
+ // Check for conflicts
429
+ if (isPhoneCallActive) {
430
+ Bridge.log("MIC: Cannot start recording - phone call active")
431
+ notifyDeviceManager("phone_call_active", emptyList())
432
+ return false
433
+ }
434
+
435
+ // Request audio focus
436
+ if (!requestAudioFocus()) {
437
+ Bridge.log("MIC: Failed to get audio focus")
438
+ // On Samsung, test if another app actually needs the mic
439
+ if (isSamsungDevice()) {
440
+ testMicrophoneAvailabilityOnSamsung()
441
+ } else {
442
+ notifyDeviceManager("audio_focus_denied", emptyList())
443
+ }
444
+ return false
445
+ }
446
+
447
+ // Try SCO mode first (if preferred)
448
+ if (preferScoMode && audioManager.isBluetoothScoAvailableOffCall) {
449
+ Bridge.log("MIC: Attempting to start with Bluetooth SCO")
450
+ if (startRecordingWithSco()) {
451
+ return true
452
+ }
453
+ Bridge.log("MIC: SCO failed, falling back to normal mode")
454
+ }
455
+
456
+ // Fallback to normal mode
457
+ return startRecordingNormal()
458
+ }
459
+
460
+ private fun startRecordingWithSco(): Boolean {
461
+ try {
462
+ // Use MODE_IN_COMMUNICATION instead of MODE_IN_CALL
463
+ // This allows media playback to coexist with microphone recording
464
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
465
+
466
+ // Start Bluetooth SCO
467
+ audioManager.startBluetoothSco()
468
+ audioManager.isBluetoothScoOn = true
469
+
470
+ // Wait briefly for SCO to connect
471
+ Thread.sleep(100)
472
+
473
+ return createAndStartAudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
474
+ } catch (e: Exception) {
475
+ Bridge.log("MIC: SCO recording failed: ${e.message}")
476
+
477
+ // Clean up SCO
478
+ audioManager.stopBluetoothSco()
479
+ audioManager.isBluetoothScoOn = false
480
+ audioManager.mode = AudioManager.MODE_NORMAL
481
+
482
+ // Retry logic
483
+ if (scoRetries < MAX_SCO_RETRIES) {
484
+ scoRetries++
485
+ Bridge.log("MIC: Retrying SCO (attempt $scoRetries)")
486
+ return startRecordingWithSco()
487
+ }
488
+
489
+ return false
490
+ }
491
+ }
492
+
493
+ private fun startRecordingNormal(): Boolean {
494
+ try {
495
+ // Set appropriate audio mode for Samsung devices
496
+ if (isSamsungDevice()) {
497
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
498
+ }
499
+
500
+ return createAndStartAudioRecord(MediaRecorder.AudioSource.MIC)
501
+ } catch (e: Exception) {
502
+ Bridge.log("MIC: Normal recording failed: ${e.message}")
503
+ return false
504
+ }
505
+ }
506
+
507
+ private fun createAndStartAudioRecord(audioSource: Int): Boolean {
508
+ // Calculate buffer size
509
+ val minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
510
+
511
+ if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
512
+ Bridge.log("MIC: Failed to get min buffer size")
513
+ return false
514
+ }
515
+
516
+ val bufferSize = minBufferSize * BUFFER_SIZE_MULTIPLIER
517
+
518
+ // Create AudioRecord
519
+ audioRecord =
520
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
521
+ AudioRecord.Builder()
522
+ .setAudioSource(audioSource)
523
+ .setAudioFormat(
524
+ AudioFormat.Builder()
525
+ .setSampleRate(SAMPLE_RATE)
526
+ .setChannelMask(CHANNEL_CONFIG)
527
+ .setEncoding(AUDIO_FORMAT)
528
+ .build()
529
+ )
530
+ .setBufferSizeInBytes(bufferSize)
531
+ .build()
532
+ } else {
533
+ AudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize)
534
+ }
535
+
536
+ if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
537
+ Bridge.log("MIC: AudioRecord failed to initialize")
538
+ audioRecord?.release()
539
+ audioRecord = null
540
+ return false
541
+ }
542
+
543
+ // Register this AudioRecord's session ID
544
+ audioRecord?.let { ourAudioSessionIds.add(it.audioSessionId) }
545
+
546
+ // Start recording
547
+ audioRecord?.startRecording()
548
+ isRecording.set(true)
549
+
550
+ // Start recording thread
551
+ startRecordingThread(bufferSize)
552
+
553
+ // Notify DeviceManager
554
+ val activeDevice = getActiveInputDevice() ?: "Unknown"
555
+ Bridge.log("MIC: Started recording from: $activeDevice")
556
+ Bridge.log("MIC: Current audio mode: ${audioManager.mode} (NORMAL=${AudioManager.MODE_NORMAL}, IN_COMM=${AudioManager.MODE_IN_COMMUNICATION}, IN_CALL=${AudioManager.MODE_IN_CALL})")
557
+
558
+ // Log detailed routing info for debugging
559
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
560
+ audioRecord?.routedDevice?.let { device ->
561
+ Bridge.log("MIC: Routed to device - Type: ${device.type}, Name: ${device.productName}")
562
+ }
563
+ }
564
+
565
+ notifyDeviceManager("recording_started", listOf(activeDevice))
566
+
567
+ // Reset retry counter on success
568
+ scoRetries = 0
569
+
570
+ return true
571
+ }
572
+
573
+ private fun startRecordingThread(bufferSize: Int) {
574
+ recordingThread =
575
+ Thread {
576
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO)
577
+
578
+ val audioBuffer = ShortArray(bufferSize / 2)
579
+
580
+ while (isRecording.get()) {
581
+ // Capture local reference to avoid use-after-free if cleanUpRecording()
582
+ // releases the AudioRecord while we're in read()
583
+ val record = audioRecord ?: break
584
+
585
+ val readResult = try {
586
+ record.read(audioBuffer, 0, audioBuffer.size)
587
+ } catch (e: Exception) {
588
+ Bridge.log("MIC: AudioRecord.read() exception: ${e.message}")
589
+ break
590
+ }
591
+
592
+ if (readResult > 0) {
593
+ // Convert short array to byte array (16-bit PCM)
594
+ val pcmData = ByteArray(readResult * 2)
595
+ val byteBuffer = ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN)
596
+
597
+ for (i in 0 until readResult) {
598
+ byteBuffer.putShort(audioBuffer[i])
599
+ }
600
+
601
+ // Send PCM data to DeviceManager
602
+ DeviceManager.getInstance().handlePcm(pcmData)
603
+ } else if (readResult < 0) {
604
+ // AudioRecord error (ERROR, ERROR_INVALID_OPERATION, ERROR_BAD_VALUE, ERROR_DEAD_OBJECT)
605
+ Bridge.log("MIC: AudioRecord.read() returned error: $readResult")
606
+ break
607
+ }
608
+ }
609
+ }
610
+ .apply {
611
+ name = "AudioRecordingThread"
612
+ start()
613
+ }
614
+ }
615
+
616
+ private fun cleanUpRecording() {
617
+ isRecording.set(false)
618
+
619
+ // Wait for recording thread to finish BEFORE touching audioRecord
620
+ // This prevents use-after-free where read() races with release()
621
+ recordingThread?.let { thread ->
622
+ thread.interrupt()
623
+ try {
624
+ thread.join(1000) // Wait up to 1 second for thread to finish
625
+ } catch (e: InterruptedException) {
626
+ Bridge.log("MIC: Interrupted while waiting for recording thread to finish")
627
+ Thread.currentThread().interrupt()
628
+ }
629
+ }
630
+ recordingThread = null
631
+
632
+ // NOW safe to stop and release - recording thread is no longer reading
633
+ audioRecord?.let { record ->
634
+ try {
635
+ // Unregister session ID
636
+ ourAudioSessionIds.remove(record.audioSessionId)
637
+
638
+ if (record.state == AudioRecord.STATE_INITIALIZED) {
639
+ record.stop()
640
+ }
641
+ record.release()
642
+ } catch (e: Exception) {
643
+ Bridge.log("MIC: Error cleaning up AudioRecord: ${e.message}")
644
+ }
645
+ }
646
+ audioRecord = null
647
+ }
648
+
649
+ private fun setupPhoneCallDetection() {
650
+ // Check for phone state permission
651
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) !=
652
+ PackageManager.PERMISSION_GRANTED
653
+ ) {
654
+ Bridge.log("MIC: READ_PHONE_STATE permission not granted, skipping call detection")
655
+ return
656
+ }
657
+
658
+ telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
659
+
660
+ phoneStateListener =
661
+ object : PhoneStateListener() {
662
+ override fun onCallStateChanged(state: Int, phoneNumber: String?) {
663
+ val wasCallActive = isPhoneCallActive
664
+ isPhoneCallActive = (state != TelephonyManager.CALL_STATE_IDLE)
665
+
666
+ if (wasCallActive != isPhoneCallActive) {
667
+ if (isPhoneCallActive) {
668
+ Bridge.log("MIC: Phone call started - stopping recording")
669
+ if (isRecording.get()) {
670
+ // Notify DeviceManager BEFORE stopping - allows switch to glasses
671
+ // mic
672
+ notifyDeviceManager("phone_call_interruption", emptyList())
673
+ // Give DeviceManager time to switch to glasses mic
674
+ mainHandler.postDelayed(
675
+ { stopRecording() },
676
+ MIC_SWITCH_DELAY_MS
677
+ )
678
+ } else {
679
+ // Not currently recording, but still notify about
680
+ // unavailability
681
+ notifyDeviceManager("phone_call_interruption", emptyList())
682
+ }
683
+ } else {
684
+ Bridge.log("MIC: Phone call ended")
685
+ notifyDeviceManager(
686
+ "phone_call_ended",
687
+ getAvailableInputDevices().values.toList()
688
+ )
689
+ }
690
+ }
691
+ }
692
+ }
693
+
694
+ telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
695
+ }
696
+
697
+ private fun setupAudioFocusListener() {
698
+ audioFocusListener =
699
+ AudioManager.OnAudioFocusChangeListener { focusChange ->
700
+ when (focusChange) {
701
+ AudioManager.AUDIOFOCUS_LOSS -> {
702
+ Bridge.log("MIC: Permanent audio focus loss - mode: $currentMicMode")
703
+ hasAudioFocus = false
704
+
705
+ // For phone internal mic, ignore AUDIOFOCUS_LOSS to prevent routing changes
706
+ // This allows media apps (YouTube, Spotify) to play normally
707
+ if (currentMicMode == MicTypes.PHONE_INTERNAL) {
708
+ Bridge.log("MIC: Ignoring AUDIOFOCUS_LOSS for phone internal mic (allows media playback)")
709
+ } else {
710
+ // For other modes (BT, etc), respect audio focus loss
711
+ if (isRecording.get()) {
712
+ notifyDeviceManager("audio_focus_lost", emptyList())
713
+ stopRecording()
714
+ }
715
+ }
716
+ }
717
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
718
+ Bridge.log("MIC: Transient audio focus loss - mode: $currentMicMode, isExternalAudioActive: $isExternalAudioActive")
719
+ hasAudioFocus = false
720
+
721
+ // For phone internal mic, only stop if we know an external app is actually recording
722
+ // AUDIOFOCUS_LOSS_TRANSIENT fires for many reasons (app switching, system UI, etc)
723
+ // AudioRecordingCallback is more reliable for detecting actual mic conflicts
724
+ if (currentMicMode == MicTypes.PHONE_INTERNAL && isRecording.get()) {
725
+ if (isExternalAudioActive) {
726
+ // External app is already recording (detected by AudioRecordingCallback)
727
+ Bridge.log("MIC: AUDIOFOCUS_LOSS_TRANSIENT with external app recording - stopping")
728
+ notifyDeviceManager("external_app_recording", emptyList())
729
+ stopRecording()
730
+ } else {
731
+ // AudioRecordingCallback might fire shortly after AUDIOFOCUS_LOSS_TRANSIENT
732
+ // Wait briefly to see if an external app actually starts recording
733
+ Bridge.log("MIC: AUDIOFOCUS_LOSS_TRANSIENT - waiting 150ms to check for external app")
734
+ mainHandler.postDelayed({
735
+ if (isRecording.get() && isExternalAudioActive) {
736
+ Bridge.log("MIC: External app detected after delay - stopping")
737
+ notifyDeviceManager("external_app_recording", emptyList())
738
+ stopRecording()
739
+ } else if (isRecording.get()) {
740
+ Bridge.log("MIC: No external app after delay - continuing recording")
741
+ }
742
+ }, 150)
743
+ }
744
+ } else if (isSamsungDevice() && isRecording.get()) {
745
+ // Samsung non-phone modes need special handling
746
+ testMicrophoneAvailabilityOnSamsung()
747
+ }
748
+ }
749
+ AudioManager.AUDIOFOCUS_GAIN -> {
750
+ Bridge.log("MIC: Regained audio focus")
751
+ hasAudioFocus = true
752
+
753
+ if (isSamsungDevice()) {
754
+ isExternalAudioActive = false
755
+ }
756
+
757
+ // Notify that focus is available again
758
+ if (!isRecording.get()) {
759
+ notifyDeviceManager(
760
+ "audio_focus_available",
761
+ getAvailableInputDevices().values.toList()
762
+ )
763
+ }
764
+ }
765
+ }
766
+ }
767
+ }
768
+
769
+ private fun setupAudioRecordingDetection() {
770
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
771
+ audioRecordingCallback =
772
+ object : AudioManager.AudioRecordingCallback() {
773
+ override fun onRecordingConfigChanged(
774
+ configs: MutableList<AudioRecordingConfiguration>?
775
+ ) {
776
+ configs ?: return
777
+
778
+ // Filter out our own recordings
779
+ val otherAppRecordings =
780
+ configs.filter { config ->
781
+ !ourAudioSessionIds.contains(config.clientAudioSessionId)
782
+ }
783
+
784
+ val wasExternalActive = isExternalAudioActive
785
+ isExternalAudioActive = otherAppRecordings.isNotEmpty()
786
+
787
+ if (wasExternalActive != isExternalAudioActive) {
788
+ if (isExternalAudioActive) {
789
+ Bridge.log("MIC: External app started recording - isRecording: ${isRecording.get()}")
790
+ if (isRecording.get()) {
791
+ // Notify DeviceManager BEFORE stopping - allows switch to glasses mic
792
+ notifyDeviceManager("external_app_recording", emptyList())
793
+ // Stop IMMEDIATELY to prevent audio corruption with Gboard
794
+ stopRecording()
795
+ } else {
796
+ // Not currently recording, but still notify about
797
+ // unavailability
798
+ notifyDeviceManager("external_app_recording", emptyList())
799
+ }
800
+ } else {
801
+ Bridge.log("MIC: External app stopped recording")
802
+ // Notify DeviceManager that phone mic is available again
803
+ notifyDeviceManager(
804
+ "external_app_stopped",
805
+ getAvailableInputDevices().values.toList()
806
+ )
807
+ }
808
+ }
809
+ }
810
+ }
811
+
812
+ if (audioRecordingCallback != null) {
813
+ audioManager.registerAudioRecordingCallback(audioRecordingCallback!!, mainHandler)
814
+ }
815
+ }
816
+ }
817
+
818
+ private fun setupAudioRouteListener() {
819
+ audioRouteReceiver =
820
+ object : BroadcastReceiver() {
821
+ override fun onReceive(context: Context?, intent: Intent?) {
822
+ when (intent?.action) {
823
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
824
+ Bridge.log("MIC: Audio becoming noisy")
825
+ handleAudioRouteChange()
826
+ }
827
+ AudioManager.ACTION_HEADSET_PLUG -> {
828
+ val state = intent.getIntExtra("state", -1)
829
+ if (state == 1) {
830
+ Bridge.log("MIC: Headset connected")
831
+ } else if (state == 0) {
832
+ Bridge.log("MIC: Headset disconnected")
833
+ }
834
+ handleAudioRouteChange()
835
+ }
836
+ AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
837
+ val state =
838
+ intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
839
+ when (state) {
840
+ AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
841
+ Bridge.log("MIC: Bluetooth SCO connected")
842
+ handleAudioRouteChange()
843
+ }
844
+ AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
845
+ Bridge.log("MIC: Bluetooth SCO disconnected")
846
+ handleAudioRouteChange()
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+ val filter =
855
+ IntentFilter().apply {
856
+ addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
857
+ addAction(AudioManager.ACTION_HEADSET_PLUG)
858
+ addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
859
+ }
860
+
861
+ context.registerReceiver(audioRouteReceiver, filter)
862
+ }
863
+
864
+ private fun setupBluetoothListener() {
865
+ bluetoothReceiver =
866
+ object : BroadcastReceiver() {
867
+ override fun onReceive(context: Context?, intent: Intent?) {
868
+ when (intent?.action) {
869
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
870
+ val device =
871
+ intent.getParcelableExtra<BluetoothDevice>(
872
+ BluetoothDevice.EXTRA_DEVICE
873
+ )
874
+ Bridge.log(
875
+ "MIC: Bluetooth device connected: ${device?.name ?: "Unknown"}"
876
+ )
877
+ handleAudioRouteChange()
878
+ }
879
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
880
+ val device =
881
+ intent.getParcelableExtra<BluetoothDevice>(
882
+ BluetoothDevice.EXTRA_DEVICE
883
+ )
884
+ Bridge.log(
885
+ "MIC: Bluetooth device disconnected: ${device?.name ?: "Unknown"}"
886
+ )
887
+ handleAudioRouteChange()
888
+ }
889
+ }
890
+ }
891
+ }
892
+
893
+ val filter =
894
+ IntentFilter().apply {
895
+ addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
896
+ addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
897
+ }
898
+
899
+ context.registerReceiver(bluetoothReceiver, filter)
900
+ }
901
+
902
+ private fun handleAudioRouteChange() {
903
+ val availableInputs = getAvailableInputDevices().values.toList()
904
+ notifyDeviceManager("audio_route_changed", availableInputs)
905
+ }
906
+
907
+ private fun requestAudioFocus(): Boolean {
908
+ Bridge.log("MIC: Requesting audio focus with USAGE_VOICE_COMMUNICATION")
909
+ val result =
910
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
911
+ val audioAttributes =
912
+ AudioAttributes.Builder()
913
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
914
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
915
+ .build()
916
+
917
+ audioFocusRequest =
918
+ AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
919
+ .setAudioAttributes(audioAttributes)
920
+ .setOnAudioFocusChangeListener(
921
+ audioFocusListener!!,
922
+ mainHandler
923
+ )
924
+ .setAcceptsDelayedFocusGain(false)
925
+ .build()
926
+
927
+ audioManager.requestAudioFocus(audioFocusRequest!!)
928
+ } else {
929
+ audioManager.requestAudioFocus(
930
+ audioFocusListener,
931
+ AudioManager.STREAM_VOICE_CALL,
932
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
933
+ )
934
+ }
935
+
936
+ hasAudioFocus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
937
+ Bridge.log("MIC: Audio focus ${if (hasAudioFocus) "GRANTED" else "DENIED"}")
938
+ return hasAudioFocus
939
+ }
940
+
941
+ private fun abandonAudioFocus() {
942
+ if (!hasAudioFocus) return
943
+
944
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && audioFocusRequest != null) {
945
+ audioManager.abandonAudioFocusRequest(audioFocusRequest!!)
946
+ audioFocusRequest = null
947
+ } else {
948
+ audioManager.abandonAudioFocus(audioFocusListener)
949
+ }
950
+
951
+ hasAudioFocus = false
952
+ }
953
+
954
+ private fun testMicrophoneAvailabilityOnSamsung() {
955
+ Bridge.log("MIC: Samsung - testing mic availability")
956
+
957
+ val currentRecord = audioRecord
958
+
959
+ // Temporarily stop recording
960
+ cleanUpRecording()
961
+
962
+ // Wait and try to recreate
963
+ mainHandler.postDelayed(
964
+ {
965
+ if (tryCreateTestAudioRecord()) {
966
+ Bridge.log("MIC: Samsung - mic available, just playback app")
967
+ // Restart recording
968
+ startRecordingInternal()
969
+ } else {
970
+ Bridge.log("MIC: Samsung - mic taken by another app")
971
+ isExternalAudioActive = true
972
+ notifyDeviceManager("samsung_mic_conflict", emptyList())
973
+ }
974
+ },
975
+ SAMSUNG_MIC_TEST_DELAY_MS
976
+ )
977
+ }
978
+
979
+ private fun tryCreateTestAudioRecord(): Boolean {
980
+ var testRecorder: AudioRecord? = null
981
+ try {
982
+ val minBufferSize =
983
+ AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
984
+
985
+ testRecorder =
986
+ AudioRecord(
987
+ MediaRecorder.AudioSource.MIC,
988
+ SAMPLE_RATE,
989
+ CHANNEL_CONFIG,
990
+ AUDIO_FORMAT,
991
+ minBufferSize
992
+ )
993
+
994
+ return testRecorder.state == AudioRecord.STATE_INITIALIZED
995
+ } catch (e: Exception) {
996
+ return false
997
+ } finally {
998
+ testRecorder?.release()
999
+ }
1000
+ }
1001
+
1002
+ private fun isSamsungDevice(): Boolean {
1003
+ return Build.MANUFACTURER.equals("samsung", ignoreCase = true)
1004
+ }
1005
+
1006
+ private fun checkPermissions(): Boolean {
1007
+ return ActivityCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
1008
+ PackageManager.PERMISSION_GRANTED
1009
+ }
1010
+
1011
+ private fun getAvailableInputDevices(): Map<String, String> {
1012
+ val deviceInfo = mutableMapOf<String, String>()
1013
+
1014
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1015
+ val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
1016
+ for (device in devices) {
1017
+ val name =
1018
+ when (device.type) {
1019
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Built-in Microphone"
1020
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth Headset"
1021
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
1022
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
1023
+ else -> device.productName.toString()
1024
+ }
1025
+ deviceInfo[device.id.toString()] = name
1026
+ }
1027
+ } else {
1028
+ deviceInfo["default"] = "Default Microphone"
1029
+ if (audioManager.isBluetoothScoAvailableOffCall) {
1030
+ deviceInfo["bluetooth"] = "Bluetooth Headset"
1031
+ }
1032
+ if (audioManager.isWiredHeadsetOn) {
1033
+ deviceInfo["wired"] = "Wired Headset"
1034
+ }
1035
+ }
1036
+
1037
+ return deviceInfo
1038
+ }
1039
+
1040
+ private fun getActiveInputDevice(): String? {
1041
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1042
+ audioRecord?.routedDevice?.let { device ->
1043
+ when (device.type) {
1044
+ AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Built-in Microphone"
1045
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth Headset"
1046
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
1047
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
1048
+ else -> device.productName.toString()
1049
+ }
1050
+ }
1051
+ } else {
1052
+ when {
1053
+ audioManager.isBluetoothScoOn -> "Bluetooth Headset"
1054
+ audioManager.isWiredHeadsetOn -> "Wired Headset"
1055
+ else -> "Built-in Microphone"
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ private fun notifyDeviceManager(reason: String, availableInputs: List<String>) {
1061
+ mainHandler.post {
1062
+ DeviceManager.getInstance()
1063
+ .onRouteChange(reason = reason, availableInputs = availableInputs)
1064
+ }
1065
+ }
1066
+
1067
+ fun cleanup() {
1068
+ stopRecording()
1069
+
1070
+ // CRITICAL: Force reset audio mode to prevent system-wide Bluetooth audio breakage
1071
+ // This ensures that even if stopRecording() failed, we restore normal audio routing
1072
+ try {
1073
+ if (audioManager.isBluetoothScoOn) {
1074
+ Bridge.log("MIC: Force stopping Bluetooth SCO in cleanup")
1075
+ audioManager.stopBluetoothSco()
1076
+ audioManager.isBluetoothScoOn = false
1077
+ }
1078
+
1079
+ if (audioManager.mode != AudioManager.MODE_NORMAL) {
1080
+ Bridge.log("MIC: Force resetting audio mode to NORMAL in cleanup")
1081
+ audioManager.mode = AudioManager.MODE_NORMAL
1082
+ }
1083
+ } catch (e: Exception) {
1084
+ Bridge.log("MIC: Error during audio mode cleanup: ${e.message}")
1085
+ }
1086
+
1087
+ // Unregister listeners
1088
+ phoneStateListener?.let { telephonyManager?.listen(it, PhoneStateListener.LISTEN_NONE) }
1089
+
1090
+ audioRecordingCallback?.let {
1091
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1092
+ audioManager.unregisterAudioRecordingCallback(it)
1093
+ }
1094
+ }
1095
+
1096
+ // Unregister receivers
1097
+ try {
1098
+ audioRouteReceiver?.let { context.unregisterReceiver(it) }
1099
+ bluetoothReceiver?.let { context.unregisterReceiver(it) }
1100
+ } catch (e: Exception) {
1101
+ Bridge.log("Error unregistering receivers: ${e.message}")
1102
+ }
1103
+
1104
+ // Cancel coroutines
1105
+ scope.cancel()
1106
+
1107
+ Bridge.log("MIC: Cleaned up")
1108
+ }
1109
+ }