Conversation
Platforms only reported "paired / not paired", which conflated two very different outcomes: the user explicitly refusing the system pairing dialog, and the peripheral being unable to bond at all. Consumers could not tell them apart, so an app had no way to abort a connection when the user said no, while still tolerating devices that cannot bond. `pairingStateStream`, `onPairingStateChange` and the native `onPairStateChange` callback now emit a `PairingState` (paired / pairing / rejectedByUser / failed / unpaired). - Android: read `BluetoothDevice.EXTRA_REASON` from ACTION_BOND_STATE_CHANGED and map UNBOND_REASON_AUTH_REJECTED, AUTH_CANCELED and AUTH_TIMEOUT to `rejectedByUser`. - Android: BOND_BONDING no longer completes a pending `pair()` future; it is surfaced as `PairingState.pairing` progress. - Linux: map BlueZ AuthenticationRejected/Canceled/Timeout to `rejectedByUser`. BREAKING CHANGE: `pairingStateStream`, `onPairingStateChange` and `onPairStateChange` expose `PairingState` instead of `bool`.
The initial PairingState work only classified the outcome on Android and Linux. Windows kept passing a bool into the regenerated `OnPairStateChange(..., PairingState, ...)` signature, which no longer compiles, and Apple reported no pairing outcome at all. - Windows: map `DevicePairingResultStatus` to `PairingState`, so a cancelled prompt or a handler rejection becomes `rejectedByUser` while unpairable hardware stays `failed`. - Apple: CoreBluetooth has no bonding API — pairing is triggered by touching an encrypted characteristic, so classify the error of that operation and publish the resulting `PairingState`. Errors unrelated to the ceremony report nothing instead of a misleading state.
fotiDim
left a comment
There was a problem hiding this comment.
Two correctness issues in the Android reason mapping, plus one minor note.
Minor: lib/src/universal_ble.g.dart is committed as raw Pigeon output (} else if (...), } catch (e) {) rather than formatted code. build_pigeon.sh runs dart format lib/src/universal_ble.g.dart after generation; re-running it would keep the file consistent and remove most of the diff churn. Not a correctness issue.
|
|
||
| /// `BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT` (@hide) — the dialog was | ||
| /// shown and simply ignored until it expired, which is a refusal too. | ||
| private const val UNBOND_REASON_AUTH_TIMEOUT = 7 |
There was a problem hiding this comment.
BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT is 6, not 7 (7 is UNBOND_REASON_REPEATED_ATTEMPTS). As written, a pairing dialog that times out (reason 6) matches no branch and falls through to else -> FAILED, which is the case the enum doc and the comment above intend to surface as rejectedByUser; conversely a repeated-attempts failure (7) is misreported as rejectedByUser. The comment describes value 6, so the literal should be 6.
| UNBOND_REASON_AUTH_TIMEOUT, | ||
| -> PairingState.REJECTED_BY_USER | ||
|
|
||
| BluetoothDevice.ERROR -> PairingState.UNPAIRED |
There was a problem hiding this comment.
BluetoothDevice.EXTRA_REASON is populated whenever EXTRA_BOND_STATE == BOND_NONE, and an intentional unpair (removeBond() or the user removing the bond in system settings) sends UNBOND_REASON_REMOVED (9). Since only the BluetoothDevice.ERROR sentinel maps to UNPAIRED, a normal unpair now hits else -> FAILED, emits the "Failed to Pair" error, and completes any pending pair future with false. This is a behavior regression versus the previous "not paired" result and contradicts the unpaired doc ("the bond was removed ... without an attempt being in flight"). Consider mapping UNBOND_REASON_REMOVED (and reason 0, which the stable-state broadcast can carry) to UNPAIRED.
- UNBOND_REASON_AUTH_TIMEOUT is 6, not 7 (7 is REPEATED_ATTEMPTS), so a timed out dialog maps to rejectedByUser and repeated attempts to failed. - Map UNBOND_REASON_REMOVED (9) and reason 0 to unpaired so an intentional removeBond()/system-settings unpair is no longer reported as a failure.
Android surfaces a removed bond through ACTION_BOND_STATE_CHANGED and Linux through the BlueZ Paired property, but Windows has no equivalent broadcast, so pairingStateStream never reported a successful UnPair().
removeDevice() drops the BlueZ object, so the Paired property change never arrives and pairingStateStream missed the removed bond.
updatePairingState deduped every state, so on Windows and Apple, which have no intermediate 'pairing' event, a second identical refusal was swallowed and a retried pair() produced no result. Dedupe only the steady paired/unpaired states.
toFlutterError() ran the NSError's numeric code through mapErrorCodeToEnum(), which only matches textual codes we raise ourselves, so every CoreBluetooth failure reached Dart as unknownError. This made the Apple half of PairingState inert: pairingStateFromError() keys off codes such as insufficientAuthentication, which could never arrive, so a refused pairing dialog was never reported as rejectedByUser. Inspect the CBATTError/CBError domains first and fall back to the string mapping.
|
@arxdeus are you done with the changes? Should the PR be in draft? |
universal_bleresolves user's connect cancellation of Bluetooth pairing as just "not paired", instead of give us exact statepairingStateStream,onPairingStateChangeand the nativeonPairStateChangecallback now emit aPairingState(paired / pairing / rejectedByUser / failed / unpaired).BluetoothDevice.EXTRA_REASONfromACTION_BOND_STATE_CHANGEDand mapUNBOND_REASON_AUTH_REJECTED,AUTH_CANCELEDandAUTH_TIMEOUTtorejectedByUser.pair()future; it is surfaced asPairingState.pairingprogress.rejectedByUser.BREAKING CHANGE:
pairingStateStream,onPairingStateChangeandonPairStateChangeexposePairingStateinstead ofbool.