perf(android): exponential BLE reconnect backoff for out-of-range retry loops

Merged
GI
1d ago
+621-5

What changed and why

Android ordinary BLE reconnects used a fixed 3 second delay and retried for as long as the pendant stayed out of range, which keeps the radio and CPU awake. Failed attempts now wait 3s, 6s, 12s, 30s, then 60s. A successful connection, the app returning to the foreground, or an explicit connect request returns the schedule to 3 seconds, and an explicit connect runs immediately instead of sitting out the current wait.

CCCD timeout recovery is unchanged: it still backs off and stops after its existing two-reconnect budget. Packet handling, subscriptions, and WAL buffering are unchanged. Reconnect still happens; only the wait between failed attempts changes.

Product invariants affected

INV-CAP-1

This paces the gap between failed connect attempts. It does not unsubscribe a live uplink, close a connected socket, or drop frames the phone is already receiving. Update (d4f5775a, review P1): the escalated schedule is now gated on verified offline retention. Service discovery records whether the device advertises the storage-drain GATT service (Omi-family firmware built with CONFIG_OMI_ENABLE_OFFLINE_STORAGE); only those devices escalate 3/6/12/30/60s, because their firmware retains audio while disconnected. Devices without the advertised capability (Friend Pendant included) keep the legacy flat 3s retry, so stream-only out-of-range audio is never parked behind a battery wait.

How it was verified

cd app/android
gradle :app:testDevDebugUnitTest \
  --tests 'com.friend.ios.ble.BleReconnectBackoffTest' \
  --tests 'com.friend.ios.ble.CccdReconnectPolicyTest'

Gradle 8.14.2, JDK 21. BUILD SUCCESSFUL.

  • BleReconnectBackoffTest: 5 tests, 0 failures. Covers the 3s→6s→12s→30s→60s sequence and the cap, per-device isolation, reset on success and explicit retry, foreground pull-forward of a long pending wait, and CCCD recovery leaving the ordinary schedule alone.
  • CccdReconnectPolicyTest: 5 tests, 0 failures. The existing CCCD budget and backoff are unchanged.

No Flutter widget tests were run because the reconnect UI was not changed. A physical out-of-range pendant was not exercised here.

Tests

app/android/app/src/test/kotlin/com/friend/ios/ble/BleReconnectBackoffTest.kt

UI evidence

UI-Evidence: none -- Android retry pacing only; no widget or screen change.

Failure class (fixes)

Failure-Class: none