Skip to content

fix(ble): the radio's word is the only report; Bluetooth off clears what it ended - #1023

Merged
cryptskii merged 2 commits into
mainfrom
fix/ble-radio-state-truthful
Sep 27, 2026
Merged

cryptskii merged 2 commits into
mainfrom
fix/ble-radio-state-truthful

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

The BLE coordinator reported advertising and scanning as started without asking the radio, and nothing handled Bluetooth going off. So after an off/on toggle, advertising never came back while the logs said it had. Both were found while mapping #1021's device run; this fixes them.

What changed

  1. The radio's word is the only report.
    • startAdvertising and startScanning used to discard the advertiser's and scanner's answers, then send the started event and return true.
    • Advertising is now reported started when the stack confirms the set, and stopped when it confirms the stop. The advertiser's confirmation and failure callbacks were wired to nobody; they now reach the coordinator.
    • A refused start returns false and reports nothing.
    • A scan is reported started only when it started, stopped only when a running scan stopped, and stopped when the stack fails it later.
  2. Bluetooth off clears what it ended.
    • Before, the advertiser kept STARTED, the scanner its flag and the GATT server its registration. The STATE_ON refresh found everything "running" and started nothing.
    • A stop in flight when the radio went off left the advertiser refusing every later start.
    • The coordinator now listens for the adapter for the life of the process. On TURNING_OFF/OFF it clears the advertiser, the scanner, the GATT server's registration and every peer's links, on the lifecycle lane. The next start opens a new server and requests a new set.
    • GattServerHost.stop now also forgets a registration in flight. A late onServiceAdded for a closed server is ignored.
  3. Pairing follows Bluetooth. MainActivity publishes the session facts when Bluetooth turns on or off. Before, a toggle from quick settings left Rust's facts stale until some other event.

Radio events go through an injected sink; production relays them to Rust. Success is still derived only from the radio, so the sink is not a bypass.

Verification

Check Result
Unit tests BleCoordinatorRadioTest 7/0 and BleRadioComponentsTest 4/0 (framework simulated at its boundary); Android unit suite 263/0
Mutation controls 12, each red on a named test: coordinator ignoring the advertiser or the scanner; receiver ignoring STATE_OFF; reset keeping the advertiser, GATT server or peers' links; failed scan not reported stopped; every stop reported; radioOff keeping state or always answering on-air; advertiser not reporting the stack's start; scanner keeping its scan
Device (Samsung A16, 8XK) Startup reports started only after Advertising set started. Bluetooth off clears the set and GATT server and reports stopped once; the stack's late stop is ignored as stale. Bluetooth on opens a new server, registers the service and starts a new set (id=2); before, nothing started. Session facts published on both.
Other Kotlin main and androidTest compile; all 10 repo gates pass
Review A read-only adversarial review found one defect in the first commit (peers' links left behind; fixed in the second) and two test gaps (closed). Its other findings were older than this change and are recorded Open in §6.29.

Not covered

  • The peer-link clearing landed after the device run; it is unit-tested only.
  • GattServerHost's registration reset is exercised only on the device, because Robolectric's addService answers false.
  • The stale-onServiceAdded branch is not exercised anywhere.
  • The device run was cut short: another adb session force-stopped and relaunched the wallet on 8XK, after the off/on cycle had completed.

Still open (recorded in §6.29), all older than this change:

  • the PairingConfirmWritten handler's 15 s wait on its own dispatcher;
  • resumePairingScan scanning in the background;
  • a stop during a requested start orphaning a set;
  • the downshift runnable racing the lifecycle lane;
  • the scanner-role pairing scan waiting for the loop's 90 s stale cycle after Bluetooth on;
  • onScanFailed's unframed error envelope;
  • the dead BlePermissionsGate receiver;
  • a START_STICKY restart without the storage dir.

…hat it ended

BleCoordinator reported advertising and scanning started without asking the
radio: startAdvertising and startScanning discarded the advertiser's and
scanner's answers, then sent the started event and returned true. The
advertiser's own confirmation and failure callbacks were wired to nobody.
Advertising is now reported started when the stack confirms the set and
stopped when it confirms the stop; a refused start returns false and reports
nothing; a scan is reported started only when it started, stopped only when a
running scan stopped, and stopped when the stack fails it afterwards.

Nothing handled Bluetooth going off. The advertiser kept its STARTED state, the
scanner its scanning flag and the GATT server its registration, so the
STATE_ON refresh found everything "running" and started nothing. The
coordinator now listens for the adapter for the life of the process and, on
TURNING_OFF/OFF, clears all three on the lifecycle lane; the next start opens a
new server and requests a new set and scan. GattServerHost.stop also forgets a
registration in flight, and a late onServiceAdded for a closed server is
ignored. MainActivity publishes the session facts when Bluetooth turns on or
off, so pairing follows it.

Radio events go through an injected sink (production relays to Rust), so the
unit tests can observe them; success is still derived only from the radio.
Review of the previous commit: onRadioOff closed the GATT server but left each
peer that was connected to it marked as a live, subscribed server client. The
closed server can no longer report those disconnects, so after Bluetooth came
back a send to such a peer took the server-notify path and failed until the
peer reconnected. The reset now clears every peer's client and server links
and drops peers left with nothing.

Tests pin BleAdvertiser.radioOff's answer when nothing was on the air, and the
coordinator test checks no peer is left reachable. §6.29 records the change,
its tests, mutation controls and device run, and the pre-existing radio issues
the review found (recorded Open, not changed here).
@cryptskii
cryptskii merged commit 7a64685 into main Sep 27, 2026
20 checks passed
@cryptskii
cryptskii deleted the fix/ble-radio-state-truthful branch September 27, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant