@mentra/bluetooth-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/android/build.gradle +167 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +109 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/app.plugin.js +1 -0
- package/build/BluetoothSdk.types.d.ts +498 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +23 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/BluetoothSdkModule.d.ts +71 -0
- package/build/BluetoothSdkModule.d.ts.map +1 -0
- package/build/BluetoothSdkModule.js +106 -0
- package/build/BluetoothSdkModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BluetoothSdkModule.swift +665 -0
- package/ios/MentraBluetoothSDK.podspec +77 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
- package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
- package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
- package/ios/Packages/VAD/Data/Configuration.swift +68 -0
- package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
- package/ios/Packages/VAD/Data/Record.swift +13 -0
- package/ios/Packages/VAD/Data/Result.swift +22 -0
- package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
- package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/Source/Bridge.swift +353 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +1478 -0
- package/ios/Source/DeviceStore.swift +295 -0
- package/ios/Source/MentraBluetoothSDK.swift +2439 -0
- package/ios/Source/ObservableStore.swift +88 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/controllers/ControllerManager.swift +187 -0
- package/ios/Source/controllers/R1.swift +827 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Frame.swift +651 -0
- package/ios/Source/sgcs/G1.swift +2505 -0
- package/ios/Source/sgcs/G2.swift +3730 -0
- package/ios/Source/sgcs/Mach1.swift +543 -0
- package/ios/Source/sgcs/MentraLive.swift +4708 -0
- package/ios/Source/sgcs/MentraNex.swift +2479 -0
- package/ios/Source/sgcs/SGCManager.swift +243 -0
- package/ios/Source/sgcs/Simulated.swift +270 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
- package/ios/Source/stt/STTTools.swift +154 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/Constants.swift +74 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2067 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunker.swift +164 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
- package/ios/Source/utils/TarBz2Extractor.swift +206 -0
- package/package.json +78 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +4 -0
- package/plugin/build/withAndroid.js +80 -0
- package/plugin/build/withIos.d.ts +4 -0
- package/plugin/build/withIos.js +64 -0
- package/src/BluetoothSdk.types.ts +581 -0
- package/src/BluetoothSdkModule.ts +259 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,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
|
+
}
|