Improve Bluetooth device connection monitoring

Switched from monitoring generic ACL events to specific Headset profile state changes for more reliable device connection and disconnection detection.

This fixes a race condition where CAPod thinks no device is connected because we triggered too early, before "connectedDevices" on the HEADSET profile contains our target device.

This fixes #313
This commit is contained in:
darken
2025-10-29 15:14:45 +01:00
committed by Matthias Urhahn
parent bda0a20743
commit 1cfc4611cb
@@ -107,12 +107,11 @@ class BluetoothManager2 @Inject constructor(
profile: Int = BluetoothProfile.HEADSET profile: Int = BluetoothProfile.HEADSET
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile -> ): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
callbackFlow { callbackFlow {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" } log(TAG, VERBOSE) { "monitorDevices(): for profile=$profile starting" }
trySend(bluetoothProfile.connectedDevices) trySend(bluetoothProfile.connectedDevices)
val filter = IntentFilter().apply { val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED) addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
} }
val handlerThread = HandlerThread("BluetoothEventReceiver").apply { val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
@@ -122,30 +121,60 @@ class BluetoothManager2 @Inject constructor(
val receiver: BroadcastReceiver = object : BroadcastReceiver() { val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" } log(TAG, VERBOSE) { "monitorDevices(): Bluetooth event (intent=$intent, extras=${intent.extras})" }
val action = intent.action val action = intent.action
if (action == null) { if (action == null) {
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" } log(TAG, ERROR) { "monitorDevices(): Bluetooth event without action?" }
return return
} }
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE) val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
if (device == null) { if (device == null) {
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" } log(TAG, ERROR) { "monitorDevices(): Event is missing EXTRA_DEVICE" }
return return
} }
this@callbackFlow.launch { this@callbackFlow.launch {
val currentDevices = bluetoothProfile.connectedDevices val currentDevices = bluetoothProfile.connectedDevices.toMutableSet()
log(TAG) { "monitorDevices(): currentDevices: $currentDevices" }
when (action) { if (action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
BluetoothDevice.ACTION_ACL_CONNECTED -> { log(TAG, WARN) { "Unknown action: $action" }
log(TAG) { "Adding $device to current devices $currentDevices" } return@launch
trySend(currentDevices.plus(device)) }
// Profile connection changed - query actual state from proxy
val statePrevious = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1)
log(TAG) { "monitorDevices(): HEADSET profile state changed for $device - previous: $statePrevious" }
val stateNow = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)
log(TAG) { "monitorDevices(): HEADSET profile state changed for $device - now: $stateNow" }
when (stateNow) {
BluetoothProfile.STATE_CONNECTING -> {
log(TAG) { "monitorDevices(): Currently connecting $device" }
} }
BluetoothDevice.ACTION_ACL_DISCONNECTED -> { BluetoothProfile.STATE_CONNECTED -> {
log(TAG) { "Removing $device from current devices $currentDevices" } log(TAG) { "monitorDevices(): Device has connected $device" }
trySend(currentDevices.minus(device)) if (!currentDevices.contains(device)) {
log(TAG, WARN) { "monitorDevices(): $device was not in $currentDevices" }
currentDevices.add(device)
}
trySend(currentDevices)
}
BluetoothProfile.STATE_DISCONNECTING -> {
log(TAG) { "monitorDevices(): Currently DISconnecting $device" }
}
BluetoothProfile.STATE_DISCONNECTED -> {
log(TAG) { "monitorDevices(): Device has disconnected $device" }
if (!currentDevices.contains(device)) {
log(TAG, WARN) { "monitorDevices(): $device WAS in $currentDevices" }
currentDevices.remove(device)
}
trySend(currentDevices)
} }
} }
} }