@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/android/build.gradle +11 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
  6. package/app.plugin.js +3 -1
  7. package/build/cjs/AudioDeviceManager.js +225 -40
  8. package/build/cjs/AudioDeviceManager.js.map +1 -1
  9. package/build/cjs/hooks/useAudioDevices.js +30 -5
  10. package/build/cjs/hooks/useAudioDevices.js.map +1 -1
  11. package/build/cjs/useAudioRecorder.js +52 -8
  12. package/build/cjs/useAudioRecorder.js.map +1 -1
  13. package/build/esm/AudioDeviceManager.js +225 -40
  14. package/build/esm/AudioDeviceManager.js.map +1 -1
  15. package/build/esm/hooks/useAudioDevices.js +31 -6
  16. package/build/esm/hooks/useAudioDevices.js.map +1 -1
  17. package/build/esm/useAudioRecorder.js +53 -9
  18. package/build/esm/useAudioRecorder.js.map +1 -1
  19. package/build/types/AudioDeviceManager.d.ts +78 -2
  20. package/build/types/AudioDeviceManager.d.ts.map +1 -1
  21. package/build/types/hooks/useAudioDevices.d.ts +1 -0
  22. package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
  23. package/build/types/useAudioRecorder.d.ts.map +1 -1
  24. package/ios/AudioDeviceManager.swift +21 -9
  25. package/ios/ExpoAudioStreamModule.swift +33 -1
  26. package/package.json +8 -6
  27. package/plugin/build/index.cjs +194 -0
  28. package/plugin/build/index.d.cts +1 -0
  29. package/plugin/build/index.js +7 -6
  30. package/plugin/src/index.ts +8 -8
  31. package/src/AudioDeviceManager.ts +286 -59
  32. package/src/hooks/useAudioDevices.ts +39 -6
  33. 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.12.3...HEAD
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
@@ -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
- // Only handle disconnect events and only for HEADSET profile (relevant for audio)
1217
- if (state == BluetoothAdapter.STATE_DISCONNECTED) {
1218
- val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1219
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
1220
- } else {
1221
- @Suppress("DEPRECATION")
1222
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
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
- // For M+ get the actual ID
1231
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1232
- val actualDevice = findBluetoothDevice(device)
1233
- if (actualDevice != null) {
1234
- deviceId = actualDevice.id.toString()
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
- LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected: ${device.name}, using ID: $deviceId")
1239
- }
1240
- // No device info, check if our last device was bluetooth
1241
- else if (lastSelectedDeviceId != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1242
- val lastDeviceInfo = findDeviceById(lastSelectedDeviceId!!)
1243
-
1244
- if (lastDeviceInfo?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
1245
- deviceId = lastSelectedDeviceId
1246
- LogUtils.d(CLASS_NAME, "Bluetooth profile disconnected, using last selected device ID: $deviceId")
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
- // If this is the currently selected device, notify delegate
1272
- if (deviceId != null && deviceId == lastSelectedDeviceId) {
1273
- LogUtils.d(CLASS_NAME, "Currently selected device disconnected: $deviceId")
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
- // Launch a coroutine to call the suspend function
1278
- coroutineScope.launch {
1279
- try {
1280
- handleDeviceDisconnection(deviceId)
1281
- } catch (e: Exception) {
1282
- LogUtils.e(CLASS_NAME, "Error handling device disconnection: ${e.message}", e)
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
- "reason" to "deviceDisconnected",
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
@@ -1 +1,3 @@
1
- module.exports = require('./plugin/build')
1
+ // Plugin for app.json usage (regular Node.js context)
2
+ const plugin = require('./plugin/build/index.js')
3
+ module.exports = plugin.default || plugin