@siteed/expo-audio-studio 2.12.3 → 2.13.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/CHANGELOG.md +5 -1
- package/android/build.gradle +11 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
- package/app.plugin.js +3 -1
- package/build/cjs/AudioDeviceManager.js +225 -40
- package/build/cjs/AudioDeviceManager.js.map +1 -1
- package/build/cjs/hooks/useAudioDevices.js +30 -5
- package/build/cjs/hooks/useAudioDevices.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +52 -8
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/AudioDeviceManager.js +225 -40
- package/build/esm/AudioDeviceManager.js.map +1 -1
- package/build/esm/hooks/useAudioDevices.js +31 -6
- package/build/esm/hooks/useAudioDevices.js.map +1 -1
- package/build/esm/useAudioRecorder.js +53 -9
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioDeviceManager.d.ts +78 -2
- package/build/types/AudioDeviceManager.d.ts.map +1 -1
- package/build/types/hooks/useAudioDevices.d.ts +1 -0
- package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/ios/AudioDeviceManager.swift +21 -9
- package/ios/ExpoAudioStreamModule.swift +33 -1
- package/package.json +8 -6
- package/plugin/build/index.cjs +194 -0
- package/plugin/build/index.d.cts +1 -0
- package/plugin/build/index.js +7 -6
- package/plugin/src/index.ts +8 -8
- package/src/AudioDeviceManager.ts +286 -59
- package/src/hooks/useAudioDevices.ts +39 -6
- package/src/useAudioRecorder.tsx +102 -9
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.13.0] - 2025-06-09
|
|
12
|
+
### Changed
|
|
13
|
+
- feat(expo-audio-studio): enhance device detection and management system ([97ceef0](https://github.com/deeeed/expo-audio-stream/commit/97ceef003ddb8eb5246cda8a5a00ddc75bf665a0))
|
|
11
14
|
## [2.12.3] - 2025-06-07
|
|
12
15
|
### Changed
|
|
13
16
|
- refactor(expo-audio-studio): adjust audio focus request timing in AudioRecorderManager ([317367c](https://github.com/deeeed/expo-audio-stream/commit/317367cb29fa09016aa73884f2f51e9cfdee1086))
|
|
@@ -304,7 +307,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
304
307
|
- Feature: Audio features extraction during recording.
|
|
305
308
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
306
309
|
|
|
307
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
310
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.0...HEAD
|
|
311
|
+
[2.13.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.3...@siteed/expo-audio-studio@2.13.0
|
|
308
312
|
[2.12.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.2...@siteed/expo-audio-studio@2.12.3
|
|
309
313
|
[2.12.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.1...@siteed/expo-audio-studio@2.12.2
|
|
310
314
|
[2.12.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.0...@siteed/expo-audio-studio@2.12.1
|
package/android/build.gradle
CHANGED
|
@@ -32,6 +32,7 @@ buildscript {
|
|
|
32
32
|
|
|
33
33
|
repositories {
|
|
34
34
|
mavenCentral()
|
|
35
|
+
google()
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
dependencies {
|
|
@@ -91,12 +92,22 @@ android {
|
|
|
91
92
|
|
|
92
93
|
repositories {
|
|
93
94
|
mavenCentral()
|
|
95
|
+
google()
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
dependencies {
|
|
97
99
|
implementation project(':expo-modules-core')
|
|
98
100
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
99
101
|
|
|
102
|
+
// Add AndroidX dependencies
|
|
103
|
+
implementation 'androidx.core:core-ktx:1.10.1'
|
|
104
|
+
implementation 'androidx.annotation:annotation:1.6.0'
|
|
105
|
+
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
106
|
+
|
|
107
|
+
// Add Kotlinx Coroutines for main code
|
|
108
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
|
109
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
|
110
|
+
|
|
100
111
|
// Add testing dependencies
|
|
101
112
|
testImplementation 'junit:junit:4.13.2'
|
|
102
113
|
testImplementation 'org.jetbrains.kotlin:kotlin-test:1.8.10'
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
|
7
7
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
8
8
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
|
9
|
+
|
|
10
|
+
<!-- Bluetooth permissions for device detection -->
|
|
11
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
12
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
13
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
14
|
+
|
|
15
|
+
<!-- USB permission for USB device detection -->
|
|
16
|
+
<uses-permission android:name="android.permission.USB_PERMISSION" />
|
|
9
17
|
|
|
10
18
|
<application>
|
|
11
19
|
<receiver
|
|
@@ -21,6 +21,7 @@ import net.siteed.audiostream.LogUtils
|
|
|
21
21
|
import kotlinx.coroutines.CoroutineScope
|
|
22
22
|
import kotlinx.coroutines.Dispatchers
|
|
23
23
|
import kotlinx.coroutines.launch
|
|
24
|
+
import kotlinx.coroutines.delay
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Constants not available in all Android versions
|
|
@@ -62,6 +63,12 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
62
63
|
// Delegate for handling device disconnection
|
|
63
64
|
var delegate: AudioDeviceManagerDelegate? = null
|
|
64
65
|
|
|
66
|
+
// Simple callback for device connections
|
|
67
|
+
var onDeviceConnected: ((String) -> Unit)? = null
|
|
68
|
+
|
|
69
|
+
// Simple callback for device disconnections
|
|
70
|
+
var onDeviceDisconnected: ((String) -> Unit)? = null
|
|
71
|
+
|
|
65
72
|
// Audio manager for accessing device information
|
|
66
73
|
private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
67
74
|
|
|
@@ -1113,6 +1120,9 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1113
1120
|
addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)
|
|
1114
1121
|
addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED)
|
|
1115
1122
|
}
|
|
1123
|
+
|
|
1124
|
+
// Bluetooth SCO state changes - to detect when microphone becomes available
|
|
1125
|
+
addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
|
|
1116
1126
|
}
|
|
1117
1127
|
|
|
1118
1128
|
deviceReceiver = object : BroadcastReceiver() {
|
|
@@ -1153,6 +1163,28 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1153
1163
|
}
|
|
1154
1164
|
}
|
|
1155
1165
|
}
|
|
1166
|
+
} else if (state == 1) { // Plugged in
|
|
1167
|
+
LogUtils.d(CLASS_NAME, "Wired headset connected: $name")
|
|
1168
|
+
|
|
1169
|
+
// For M+ find the actual new device
|
|
1170
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1171
|
+
// Find the newly connected wired device
|
|
1172
|
+
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
|
|
1173
|
+
val wiredDevice = audioDevices.firstOrNull {
|
|
1174
|
+
(it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
|
|
1175
|
+
it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) &&
|
|
1176
|
+
getDeviceName(it).contains(name, ignoreCase = true)
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (wiredDevice != null) {
|
|
1180
|
+
val connectedDeviceId = wiredDevice.id.toString()
|
|
1181
|
+
LogUtils.d(CLASS_NAME, "Found connected wired device: $name (ID: $connectedDeviceId)")
|
|
1182
|
+
handleDeviceConnection(connectedDeviceId)
|
|
1183
|
+
}
|
|
1184
|
+
} else {
|
|
1185
|
+
// Legacy handling for older Android
|
|
1186
|
+
handleDeviceConnection("1")
|
|
1187
|
+
}
|
|
1156
1188
|
}
|
|
1157
1189
|
}
|
|
1158
1190
|
|
|
@@ -1184,6 +1216,59 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1184
1216
|
}
|
|
1185
1217
|
}
|
|
1186
1218
|
|
|
1219
|
+
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
|
1220
|
+
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1221
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
1222
|
+
} else {
|
|
1223
|
+
@Suppress("DEPRECATION")
|
|
1224
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (device != null) {
|
|
1228
|
+
LogUtils.d(CLASS_NAME, "Bluetooth device connected: ${device.name}")
|
|
1229
|
+
|
|
1230
|
+
// For M+ find the actual new device
|
|
1231
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1232
|
+
val actualDevice = findBluetoothDevice(device)
|
|
1233
|
+
if (actualDevice != null) {
|
|
1234
|
+
val connectedDeviceId = actualDevice.id.toString()
|
|
1235
|
+
LogUtils.d(CLASS_NAME, "Found connected Bluetooth device: ${device.name} (ID: $connectedDeviceId)")
|
|
1236
|
+
handleDeviceConnection(connectedDeviceId)
|
|
1237
|
+
} else {
|
|
1238
|
+
LogUtils.d(CLASS_NAME, "Bluetooth device ${device.name} connected but not found in audio device list - attempting to activate SCO")
|
|
1239
|
+
|
|
1240
|
+
// Try to activate Bluetooth SCO to make microphone available
|
|
1241
|
+
if (!audioManager.isBluetoothScoOn) {
|
|
1242
|
+
LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
|
|
1243
|
+
audioManager.startBluetoothSco()
|
|
1244
|
+
|
|
1245
|
+
// Give SCO time to activate, then check again
|
|
1246
|
+
coroutineScope.launch {
|
|
1247
|
+
delay(2000) // Wait 2 seconds
|
|
1248
|
+
|
|
1249
|
+
val scoDevice = findBluetoothDevice(device)
|
|
1250
|
+
if (scoDevice != null) {
|
|
1251
|
+
val activatedDeviceId = scoDevice.id.toString()
|
|
1252
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
|
|
1253
|
+
handleDeviceConnection(activatedDeviceId)
|
|
1254
|
+
} else {
|
|
1255
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
|
|
1256
|
+
// Send generic connection event anyway
|
|
1257
|
+
handleDeviceConnection("bluetooth:${device.address}")
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
// SCO already on, send generic event
|
|
1262
|
+
handleDeviceConnection("bluetooth:${device.address}")
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} else {
|
|
1266
|
+
// Legacy handling for older Android
|
|
1267
|
+
handleDeviceConnection("2")
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1187
1272
|
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
|
|
1188
1273
|
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1189
1274
|
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
@@ -1213,42 +1298,117 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1213
1298
|
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)
|
|
1214
1299
|
logAudioState()
|
|
1215
1300
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (device != null) {
|
|
1226
|
-
deviceId = "2" // Legacy ID for bluetooth
|
|
1227
|
-
deviceName = device.name
|
|
1228
|
-
deviceType = DEVICE_TYPE_BLUETOOTH
|
|
1301
|
+
when (state) {
|
|
1302
|
+
BluetoothAdapter.STATE_CONNECTED -> {
|
|
1303
|
+
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1304
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
1305
|
+
} else {
|
|
1306
|
+
@Suppress("DEPRECATION")
|
|
1307
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
1308
|
+
}
|
|
1229
1309
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1310
|
+
if (device != null) {
|
|
1311
|
+
LogUtils.d(CLASS_NAME, "Bluetooth profile connected: ${device.name}")
|
|
1312
|
+
|
|
1313
|
+
// For M+ find the actual new device
|
|
1314
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1315
|
+
val actualDevice = findBluetoothDevice(device)
|
|
1316
|
+
if (actualDevice != null) {
|
|
1317
|
+
val connectedDeviceId = actualDevice.id.toString()
|
|
1318
|
+
LogUtils.d(CLASS_NAME, "Found connected Bluetooth profile device: ${device.name} (ID: $connectedDeviceId)")
|
|
1319
|
+
handleDeviceConnection(connectedDeviceId)
|
|
1320
|
+
} else {
|
|
1321
|
+
LogUtils.d(CLASS_NAME, "Bluetooth profile ${device.name} connected but not found in audio device list - attempting to activate SCO")
|
|
1322
|
+
|
|
1323
|
+
// Try to activate Bluetooth SCO to make microphone available
|
|
1324
|
+
if (!audioManager.isBluetoothScoOn) {
|
|
1325
|
+
LogUtils.d(CLASS_NAME, "Starting Bluetooth SCO to activate microphone for ${device.name}")
|
|
1326
|
+
audioManager.startBluetoothSco()
|
|
1327
|
+
|
|
1328
|
+
// Give SCO time to activate, then check again
|
|
1329
|
+
coroutineScope.launch {
|
|
1330
|
+
delay(2000) // Wait 2 seconds
|
|
1331
|
+
|
|
1332
|
+
val scoDevice = findBluetoothDevice(device)
|
|
1333
|
+
if (scoDevice != null) {
|
|
1334
|
+
val activatedDeviceId = scoDevice.id.toString()
|
|
1335
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO activated for device: ${device.name} (ID: $activatedDeviceId)")
|
|
1336
|
+
handleDeviceConnection(activatedDeviceId)
|
|
1337
|
+
} else {
|
|
1338
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO didn't activate microphone for ${device.name}")
|
|
1339
|
+
// Send generic connection event anyway
|
|
1340
|
+
handleDeviceConnection("bluetooth:${device.address}")
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
} else {
|
|
1344
|
+
// SCO already on, send generic event
|
|
1345
|
+
handleDeviceConnection("bluetooth:${device.address}")
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
// Legacy handling for older Android
|
|
1350
|
+
handleDeviceConnection("2")
|
|
1235
1351
|
}
|
|
1236
1352
|
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
BluetoothAdapter.STATE_DISCONNECTED -> {
|
|
1356
|
+
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1357
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
1358
|
+
} else {
|
|
1359
|
+
@Suppress("DEPRECATION")
|
|
1360
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
1361
|
+
}
|
|
1237
1362
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1363
|
+
if (device != null) {
|
|
1364
|
+
deviceId = "2" // Legacy ID for bluetooth
|
|
1365
|
+
deviceName = device.name
|
|
1366
|
+
deviceType = DEVICE_TYPE_BLUETOOTH
|
|
1367
|
+
|
|
1368
|
+
// For M+ get the actual ID
|
|
1369
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1370
|
+
val actualDevice = findBluetoothDevice(device)
|
|
1371
|
+
if (actualDevice != null) {
|
|
1372
|
+
deviceId = actualDevice.id.toString()
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected: ${device.name}, using ID: $deviceId")
|
|
1377
|
+
}
|
|
1378
|
+
// No device info, check if our last device was bluetooth
|
|
1379
|
+
else if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1380
|
+
val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
|
|
1381
|
+
|
|
1382
|
+
if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
|
|
1383
|
+
deviceId = lastSelectedDeviceId
|
|
1384
|
+
LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected, using last selected device ID: $deviceId")
|
|
1385
|
+
}
|
|
1247
1386
|
}
|
|
1248
1387
|
}
|
|
1249
1388
|
}
|
|
1250
1389
|
}
|
|
1251
1390
|
|
|
1391
|
+
UsbManager.ACTION_USB_DEVICE_ATTACHED, UsbManager.ACTION_USB_ACCESSORY_ATTACHED -> {
|
|
1392
|
+
LogUtils.d(CLASS_NAME, "USB device attached")
|
|
1393
|
+
|
|
1394
|
+
// For M+ find newly connected USB audio devices
|
|
1395
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1396
|
+
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
|
|
1397
|
+
val usbDevice = audioDevices.firstOrNull {
|
|
1398
|
+
it.type == AudioDeviceInfo.TYPE_USB_DEVICE ||
|
|
1399
|
+
it.type == AudioDeviceInfo.TYPE_USB_HEADSET ||
|
|
1400
|
+
it.type == AudioDeviceInfo.TYPE_USB_ACCESSORY
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (usbDevice != null) {
|
|
1404
|
+
val connectedDeviceId = usbDevice.id.toString()
|
|
1405
|
+
val deviceName = getDeviceName(usbDevice)
|
|
1406
|
+
LogUtils.d(CLASS_NAME, "Found connected USB audio device: $deviceName (ID: $connectedDeviceId)")
|
|
1407
|
+
handleDeviceConnection(connectedDeviceId)
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1252
1412
|
UsbManager.ACTION_USB_DEVICE_DETACHED, UsbManager.ACTION_USB_ACCESSORY_DETACHED -> {
|
|
1253
1413
|
LogUtils.d(CLASS_NAME, "USB device detached")
|
|
1254
1414
|
|
|
@@ -1266,28 +1426,74 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1266
1426
|
}
|
|
1267
1427
|
}
|
|
1268
1428
|
}
|
|
1429
|
+
|
|
1430
|
+
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
|
|
1431
|
+
val scoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
|
|
1432
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO state changed: $scoState")
|
|
1433
|
+
|
|
1434
|
+
when (scoState) {
|
|
1435
|
+
AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
|
|
1436
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO connected - microphone available")
|
|
1437
|
+
|
|
1438
|
+
// Check if any new Bluetooth SCO devices appeared
|
|
1439
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1440
|
+
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
|
|
1441
|
+
val bluetoothScoDevices = audioDevices.filter {
|
|
1442
|
+
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (bluetoothScoDevices.isNotEmpty()) {
|
|
1446
|
+
// Found Bluetooth SCO device(s), notify about the first one
|
|
1447
|
+
val scoDevice = bluetoothScoDevices.first()
|
|
1448
|
+
val scoDeviceId = scoDevice.id.toString()
|
|
1449
|
+
val scoDeviceName = getDeviceName(scoDevice)
|
|
1450
|
+
LogUtils.d(CLASS_NAME, "Found Bluetooth SCO device: $scoDeviceName (ID: $scoDeviceId)")
|
|
1451
|
+
handleDeviceConnection(scoDeviceId)
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
|
|
1457
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO disconnected - microphone no longer available")
|
|
1458
|
+
// Note: Device disconnection will be handled by other broadcasts
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
AudioManager.SCO_AUDIO_STATE_CONNECTING -> {
|
|
1462
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO connecting...")
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
else -> {
|
|
1466
|
+
LogUtils.d(CLASS_NAME, "Bluetooth SCO state: $scoState")
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1269
1470
|
}
|
|
1270
1471
|
|
|
1271
|
-
//
|
|
1272
|
-
if (deviceId != null
|
|
1273
|
-
LogUtils.d(CLASS_NAME, "
|
|
1472
|
+
// Handle any device disconnection - send events to React Native for device list updates
|
|
1473
|
+
if (deviceId != null) {
|
|
1474
|
+
LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId (selected: ${deviceId == lastSelectedDeviceId})")
|
|
1475
|
+
if (deviceName != null) {
|
|
1476
|
+
LogUtils.d(CLASS_NAME, "Device name: $deviceName, type: $deviceType")
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1274
1479
|
// Log the disconnection for debugging
|
|
1275
1480
|
logDeviceDisconnection(deviceId, action ?: "unknown")
|
|
1276
1481
|
|
|
1277
|
-
//
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1482
|
+
// Send device disconnection event to React Native (for UI updates)
|
|
1483
|
+
handleDeviceDisconnectionEvent(deviceId)
|
|
1484
|
+
|
|
1485
|
+
// If this was the currently selected device, also notify delegate for recording interruption
|
|
1486
|
+
if (deviceId == lastSelectedDeviceId) {
|
|
1487
|
+
LogUtils.d(CLASS_NAME, "Currently selected device disconnected - notifying delegate: $deviceId")
|
|
1488
|
+
// Launch a coroutine to call the suspend function
|
|
1489
|
+
coroutineScope.launch {
|
|
1490
|
+
try {
|
|
1491
|
+
handleDeviceDisconnection(deviceId)
|
|
1492
|
+
} catch (e: Exception) {
|
|
1493
|
+
LogUtils.e(CLASS_NAME, "Error handling device disconnection: ${e.message}", e)
|
|
1494
|
+
}
|
|
1283
1495
|
}
|
|
1284
1496
|
}
|
|
1285
|
-
} else if (deviceId != null) {
|
|
1286
|
-
// Even if not our current device, log for debugging
|
|
1287
|
-
LogUtils.d(CLASS_NAME, "Device disconnected but not currently selected: $deviceId")
|
|
1288
|
-
if (deviceName != null) {
|
|
1289
|
-
LogUtils.d(CLASS_NAME, "Device name: $deviceName, type: $deviceType")
|
|
1290
|
-
}
|
|
1291
1497
|
}
|
|
1292
1498
|
|
|
1293
1499
|
// Force refresh the device list
|
|
@@ -1498,4 +1704,22 @@ class AudioDeviceManager(private val context: Context) {
|
|
|
1498
1704
|
LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId. Pausing recording.")
|
|
1499
1705
|
delegate?.onDeviceDisconnected(deviceId)
|
|
1500
1706
|
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Handles audio device connection
|
|
1710
|
+
*/
|
|
1711
|
+
private fun handleDeviceConnection(deviceId: String) {
|
|
1712
|
+
LogUtils.d(CLASS_NAME, "Device connected: $deviceId")
|
|
1713
|
+
onDeviceConnected?.invoke(deviceId)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Handles audio device disconnection (for React Native events)
|
|
1718
|
+
*/
|
|
1719
|
+
private fun handleDeviceDisconnectionEvent(deviceId: String) {
|
|
1720
|
+
LogUtils.d(CLASS_NAME, "Device disconnected: $deviceId")
|
|
1721
|
+
onDeviceDisconnected?.invoke(deviceId)
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
|
|
1501
1725
|
}
|
|
@@ -7,6 +7,7 @@ import android.os.Bundle
|
|
|
7
7
|
import android.util.Log
|
|
8
8
|
import android.content.pm.PackageManager
|
|
9
9
|
import androidx.annotation.RequiresApi
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
10
11
|
import androidx.core.os.bundleOf
|
|
11
12
|
import expo.modules.kotlin.Promise
|
|
12
13
|
import expo.modules.kotlin.modules.Module
|
|
@@ -30,6 +31,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
30
31
|
private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
|
|
31
32
|
private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
|
|
32
33
|
private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
|
|
34
|
+
private var enableDeviceDetection: Boolean = false // Default to false until we check manifest
|
|
33
35
|
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
34
36
|
|
|
35
37
|
private val audioFileHandler by lazy {
|
|
@@ -65,14 +67,19 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
65
67
|
// Check if background audio is enabled by looking for FOREGROUND_SERVICE_MICROPHONE permission
|
|
66
68
|
enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) ?: false
|
|
67
69
|
|
|
70
|
+
// Check if device detection is enabled by looking for BLUETOOTH_CONNECT permission
|
|
71
|
+
enableDeviceDetection = packageInfo.requestedPermissions?.contains(Manifest.permission.BLUETOOTH_CONNECT) ?: false
|
|
72
|
+
|
|
68
73
|
LogUtils.d(CLASS_NAME, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
69
74
|
LogUtils.d(CLASS_NAME, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
70
75
|
LogUtils.d(CLASS_NAME, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
|
|
76
|
+
LogUtils.d(CLASS_NAME, "Device detection ${if (enableDeviceDetection) "enabled" else "disabled"} based on manifest permissions")
|
|
71
77
|
} catch (e: Exception) {
|
|
72
78
|
LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
|
|
73
79
|
enablePhoneStateHandling = false
|
|
74
80
|
enableNotificationHandling = false
|
|
75
81
|
enableBackgroundAudio = false
|
|
82
|
+
enableDeviceDetection = false
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
Events(
|
|
@@ -91,6 +98,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
91
98
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
// Helper function to check if device detection is enabled
|
|
102
|
+
fun isDeviceDetectionEnabled(): Boolean {
|
|
103
|
+
return enableDeviceDetection
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
// Add device-related functions to the module
|
|
95
107
|
|
|
96
108
|
// Gets available audio input devices with an optional refresh parameter
|
|
@@ -169,6 +181,8 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
169
181
|
return@Function mapOf("success" to success)
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
|
|
185
|
+
|
|
172
186
|
AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
173
187
|
try {
|
|
174
188
|
// If notifications are requested but permission not in manifest, modify options
|
|
@@ -263,6 +277,15 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
263
277
|
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
264
278
|
}
|
|
265
279
|
|
|
280
|
+
// Add device detection permissions if device detection is enabled
|
|
281
|
+
if (isDeviceDetectionEnabled()) {
|
|
282
|
+
// BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
|
|
283
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
284
|
+
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
|
|
285
|
+
LogUtils.d(CLASS_NAME, "Adding BLUETOOTH_CONNECT permission request for device detection")
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
266
289
|
LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
|
|
267
290
|
Permissions.askForPermissionsWithPermissionsManager(
|
|
268
291
|
appContext.permissions,
|
|
@@ -290,6 +313,14 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
290
313
|
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
291
314
|
}
|
|
292
315
|
|
|
316
|
+
// Add device detection permissions if enabled
|
|
317
|
+
if (isDeviceDetectionEnabled()) {
|
|
318
|
+
// BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
|
|
319
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
320
|
+
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
293
324
|
Permissions.getPermissionsWithPermissionsManager(
|
|
294
325
|
appContext.permissions,
|
|
295
326
|
promise,
|
|
@@ -909,7 +940,10 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
909
940
|
val audioDataEncoder = AudioDataEncoder()
|
|
910
941
|
|
|
911
942
|
// Initialize AudioDeviceManager
|
|
943
|
+
LogUtils.d(CLASS_NAME, "🔧 Initializing AudioDeviceManager...")
|
|
944
|
+
LogUtils.d(CLASS_NAME, "🔧 Device detection enabled: $enableDeviceDetection")
|
|
912
945
|
audioDeviceManager = AudioDeviceManager(context)
|
|
946
|
+
LogUtils.d(CLASS_NAME, "🔧 AudioDeviceManager initialized")
|
|
913
947
|
|
|
914
948
|
// Initialize AudioRecorderManager with AudioDeviceManager integration
|
|
915
949
|
audioRecorderManager = AudioRecorderManager.initialize(
|
|
@@ -936,7 +970,7 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
936
970
|
|
|
937
971
|
// Notify JS about the disconnection
|
|
938
972
|
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
939
|
-
"
|
|
973
|
+
"type" to "deviceDisconnected",
|
|
940
974
|
"deviceId" to deviceId
|
|
941
975
|
))
|
|
942
976
|
} catch (e: Exception) {
|
|
@@ -946,6 +980,26 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
946
980
|
}
|
|
947
981
|
}
|
|
948
982
|
|
|
983
|
+
// Set up connection callback
|
|
984
|
+
audioDeviceManager.onDeviceConnected = { deviceId ->
|
|
985
|
+
LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
|
|
986
|
+
// Notify JS about the connection
|
|
987
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
988
|
+
"type" to "deviceConnected",
|
|
989
|
+
"deviceId" to deviceId
|
|
990
|
+
))
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Set up disconnection callback
|
|
994
|
+
audioDeviceManager.onDeviceDisconnected = { deviceId ->
|
|
995
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
996
|
+
// Notify JS about the disconnection
|
|
997
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
998
|
+
"type" to "deviceDisconnected",
|
|
999
|
+
"deviceId" to deviceId
|
|
1000
|
+
))
|
|
1001
|
+
}
|
|
1002
|
+
|
|
949
1003
|
audioProcessor = AudioProcessor(filesDir)
|
|
950
1004
|
}
|
|
951
1005
|
|
package/app.plugin.js
CHANGED