Skip to content

fix(ble): Bound Android bonding wait#5967

Merged
jamesarich merged 5 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/ble-bond-timeout
Jun 26, 2026
Merged

fix(ble): Bound Android bonding wait#5967
jamesarich merged 5 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/ble-bond-timeout

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Overview

This pull request prevents Android BLE bonding from waiting indefinitely when the platform does not deliver a terminal bond-state broadcast.

The Android bonding flow waits for ACTION_BOND_STATE_CHANGED after calling createBond(). On some Android devices, that broadcast can be delayed, dropped, or missed when the pairing dialog is dismissed or bonding completes outside the receiver path. In that case the coroutine waiting for bonding can remain suspended, leaving the app stuck after PIN entry and preventing the selected device address from being armed.

This change keeps the receiver-based bonding path, but adds a 30-second upper bound around the wait. It also re-checks the platform bond state before starting a new bond, while waiting, and again before failing.

The periodic re-check matters because Android may already report the device as bonded even when the terminal broadcast was missed. In that case the app now completes the bond wait without sitting on the full timeout.

This is the base bonding hardening for the related follow-ups in #5969 and PR #. #5969 uses the bounded/final-state-checked bond result to avoid immediate duplicate UI-side bonding retries after pairing failure. PR #5973 handles the remaining transport-side case by stopping GATT setup when transport-side bonding fails and Android still reports the device as not bonded.

Key Changes

  • Added a 30-second timeout to AndroidBluetoothRepository.bond().
  • Re-checked bondState after receiver registration before calling createBond().
  • Periodically re-checked bondState while waiting for Android bonding to complete.
  • Completed bonding early when Android reports BOND_BONDED, even if the terminal broadcast was missed.
  • Re-checked bondState again when the timeout expires.
  • Treated timeout as success if Android reports the device is bonded.
  • Failed with a clear bonding error if the timeout expires and the device is still not bonded.
  • Preserved receiver cleanup on success, failure, cancellation, setup failure, and timeout.
  • Kept existing successful bonding behavior unchanged.

Testing

  • Added Android host-test coverage for a lost or missing terminal bond broadcast.
  • Added coverage for early completion when bondState becomes bonded without a broadcast.
  • Added coverage for timeout followed by a final bonded-state re-check.
  • Added coverage for the post-registration bond-state race.
  • Verified existing Android bonding behavior remains covered.

Migration Notes

  • No user data migration is required.
  • Existing BLE addresses and OS bonds are unchanged.
  • Bluetooth permission behavior is unchanged.
  • This only bounds and hardens the app-side wait for Android bonding to complete.

Add a hard timeout around Android BLE bonding so the app cannot remain suspended forever when the platform does not deliver a terminal ACTION_BOND_STATE_CHANGED broadcast.

After the timeout expires, re-check the platform bondState before failing. This preserves successful pairings that Android recorded even if the broadcast was missed, while still surfacing a clear failure when the device never becomes bonded.

Keep the BluetoothRepository API unchanged and continue refreshing repository state after the bonding attempt resolves.
Centralize bonding receiver completion and unregistration so the receiver is cleaned up across success, failure, timeout, cancellation, and setup failures after registration.

Route post-registration bondState/createBond exceptions through the suspended result instead of allowing them to bypass cleanup. This prevents receiver leaks when Android permission or Bluetooth APIs throw during the bonding setup path.

Preserve the existing in-flight bonding behavior: createBond() returning false while Android reports BOND_BONDING continues waiting for a terminal result instead of failing immediately.
Clarify the comments around the bounded bonding wait so the implementation reads as intentional rather than as a loose boolean sentinel.

Document why createBond() returning false is not always terminal on Android and why the code re-checks bondState directly for Kable meshtastic#111-style unreliable bond broadcasts.

No runtime behavior changes.
Rewrap the AndroidBluetoothRepository bonding comment so the branch stays within the repository lint and formatting limits.

No runtime behavior changes.
Poll Android bondState while the app waits for the terminal bonding signal so missed or delayed ACTION_BOND_STATE_CHANGED broadcasts no longer force users to sit through the full 30-second timeout after entering a PIN.

Use an interval timeout around the deferred receiver result instead of sleeping unconditionally. That lets receiver completion resume the coroutine immediately while still periodically checking the platform bond state when no broadcast arrives.

Grant BLUETOOTH_CONNECT in the post-registration race test and keep the repository implementation within detekt's method/function limits by extracting the bond setup and wait helpers without changing the public BluetoothRepository API.
@github-actions github-actions Bot added the bugfix PR tag label Jun 26, 2026
@jeremiah-k jeremiah-k marked this pull request as ready for review June 26, 2026 15:30
@jamesarich jamesarich added this pull request to the merge queue Jun 26, 2026
Merged via the queue into meshtastic:main with commit fe019d3 Jun 26, 2026
23 checks passed
@jeremiah-k jeremiah-k deleted the bugfix/ble-bond-timeout branch June 26, 2026 16:37
jeremiah-k added a commit to jeremiah-k/Meshtastic-Android that referenced this pull request Jun 26, 2026
When BleRadioTransport bonds before connecting and bond() throws,
re-check isBonded before continuing into GATT setup:
- bond() succeeds: continue (unchanged)
- bond() throws + isBonded true: continue (late/flaky terminal bond)
- bond() throws + isBonded false: fail fast with RadioNotConnectedException,
  let BleReconnectPolicy own retry/backoff instead of hitting cryptic
  GATT status 5/133 later

CancellationException is preserved (rethrown before the generic catch).

Follow-up to meshtastic#5967 (bounded bond wait) and meshtastic#5969 (UI retry path).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants