fix(ble): retrigger connection when bonding is interrupted (#5849)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-06-18 11:47:28 -05:00
committed by GitHub
parent eb2c422763
commit 975adce303
2 changed files with 52 additions and 23 deletions

View File

@@ -142,10 +142,31 @@ class AndroidBluetoothRepository(
}
if (!remoteDevice.createBond()) {
try {
context.unregisterReceiver(receiver)
} catch (ignored: Exception) {}
if (cont.isActive) cont.resumeWith(Result.failure(Exception("Failed to initiate bonding")))
// createBond() returns false when a bond is already in flight (initiated by the OS or
// triggered by a GATT operation hitting a secured characteristic) or already established.
// The ACTION_BOND_STATE_CHANGED broadcast is unreliable on some devices (see Kable #111),
// so re-check bondState directly rather than failing the whole flow.
when (remoteDevice.bondState) {
android.bluetooth.BluetoothDevice.BOND_BONDED -> {
try {
context.unregisterReceiver(receiver)
} catch (ignored: Exception) {}
if (cont.isActive) cont.resume(Unit)
}
android.bluetooth.BluetoothDevice.BOND_BONDING -> {
// Bond already in progress; leave the receiver registered to resolve it on the
// terminal BOND_BONDED / BOND_NONE transition instead of treating this as a failure.
Logger.d { "createBond() returned false but bonding is already in progress" }
}
else -> {
try {
context.unregisterReceiver(receiver)
} catch (ignored: Exception) {}
if (cont.isActive) cont.resumeWith(Result.failure(Exception("Failed to initiate bonding")))
}
}
}
}
updateBluetoothState()

View File

@@ -68,26 +68,34 @@ class AndroidScannerViewModel(
Logger.i { "Starting bonding for ${entry.device.address.anonymize}" }
viewModelScope.launch {
@Suppress("TooGenericExceptionCaught")
try {
bluetoothRepository.bond(entry.device)
Logger.i { "Bonding complete for ${entry.device.address.anonymize}, selecting device..." }
changeDeviceAddress(entry.fullAddress)
} catch (ex: SecurityException) {
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize} Permissions not granted" }
serviceRepository.setErrorMessage(
text = "Bonding failed: ${ex.message} Permissions not granted",
severity = Severity.Warn,
)
} catch (ex: Exception) {
// Bonding is often flaky and can fail for many reasons (timeout, user cancel, etc)
val message = ex.message ?: ""
if (message.contains("Received bond state changed 11")) {
// This is a known issue where bonding is still in progress, ignore as error
Logger.d { "Bonding still in progress for ${entry.device.address.anonymize}" }
} else {
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize}" }
serviceRepository.setErrorMessage(text = "Bonding failed: ${ex.message}", severity = Severity.Warn)
val armTransport =
try {
bluetoothRepository.bond(entry.device)
Logger.i { "Bonding complete for ${entry.device.address.anonymize}, selecting device..." }
true
} catch (ex: SecurityException) {
// No BLUETOOTH_CONNECT permission — connecting would fail the same way, so surface the
// error and do not arm the transport.
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize} Permissions not granted" }
serviceRepository.setErrorMessage(
text = "Bonding failed: ${ex.message} Permissions not granted",
severity = Severity.Warn,
)
false
} catch (ex: Exception) {
// Bonding is flaky and can fail for many reasons: user cancel/timeout, an unreliable
// ACTION_BOND_STATE_CHANGED broadcast, or an OS/GATT-initiated bond already in flight
// (see Kable #111). Don't treat any of these as terminal — arm the transport anyway so
// its persistent reconnect loop (which bonds on demand and retries with backoff) can
// converge, instead of leaving the device inert until the user re-selects it.
Logger.w(ex) {
"Bonding did not complete cleanly for ${entry.device.address.anonymize}; " +
"arming transport to retry"
}
true
}
if (armTransport) {
changeDeviceAddress(entry.fullAddress)
}
}
}