Skip to content

Android parity: stop reporting a disarm that never reached the strap (#730) - #785

Merged
ryanbr merged 2 commits into
mainfrom
fix/android-alarm-disarm-honesty-730
Jul 24, 2026
Merged

ryanbr merged 2 commits into
mainfrom
fix/android-alarm-disarm-honesty-730

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Parity follow-up to #784, which fixed this on iOS/macOS. Android had the identical defect.

disableStrapAlarm() logged "Alarm: disarmed" unconditionally, while send() drops the write and logs send(...) ignored — not connected. So the app claimed the firmware alarm was cleared when the command never reached the strap — and if one is armed, the strap still buzzes.

Mirrors the Swift fix exactly: a commandChannelReady twin (the same conditions send() guards on — gatt != null && cmdCharacteristic != null) decides which line gets logged.

What Android did NOT need

The second iOS defect doesn't exist here, and it's worth recording why: reconcileStrapAlarm() is the sole caller of arm/disarm, handles the disarm case internally (epochSec == null → disableStrapAlarm()), and is called ungated on the bond edge (state.bonded && !lastBonded) plus the daily tick. So a deferred disarm was already re-issued on reconnect. iOS's smartAlarmEnabled guard was the outlier — #784 brought iOS into line with Android, not the other way round.

Minor mechanism difference, feature-level parity intact: Android re-asserts the disarm on every bond edge; iOS (#784) latches disarmPending and only re-issues a dropped one, so it doesn't add a DISABLE_ALARM to every connect. Observable behaviour is the same — the strap ends up disarmed.

The #730 restored-connect diagnostic also doesn't port: Android has no CoreBluetooth state restoration, so that code path has no analogue.

Verification

⚠️ App-target Android — not compiled here (aapt2 is x86-only on this arm64 host) and not hardware-tested. WhoopBleClient is Android-coupled, so the pure-Kotlin test route that verified Whoop5Variant doesn't reach it. Small and self-contained (+14/−2); wants a ./gradlew compileFullDebugKotlin.

ryanbr added 2 commits July 24, 2026 15:03
…730)

#784 fixed this on iOS/macOS; Android had the identical defect.
disableStrapAlarm() logged 'Alarm: disarmed' unconditionally while send()
drops the write with 'ignored — not connected', so the app claimed the
firmware alarm was cleared when the command never reached the strap. If one
IS armed the strap still buzzes.

Mirrors the Swift fix: a commandChannelReady twin (the same conditions send()
guards on) decides which line is logged.

Android did NOT have the second iOS defect. reconcileStrapAlarm() is the sole
caller of arm/disarm, handles the disarm case internally, and is called
UNGATED on the bond edge (state.bonded && !lastBonded) plus the daily tick -
so a deferred disarm was already re-issued on reconnect. iOS's
smartAlarmEnabled guard was the outlier, and #784 brought it into line.

The #730 restored-connect diagnostic does not port: Android has no
CoreBluetooth state restoration, so that path has no Android analogue.

App-target Android: not compiled here (aapt2 is x86-only on this host) and
not hardware-tested.
Nothing outside WhoopBleClient reads it, and the macOS twin is private -
keep the API surface identical on both platforms.
@ryanbr
ryanbr merged commit 8227ae2 into main Jul 24, 2026
1 check passed
@ryanbr
ryanbr deleted the fix/android-alarm-disarm-honesty-730 branch July 24, 2026 22:06
ryanbr added a commit that referenced this pull request Jul 25, 2026
MARKETING_VERSION/versionName move together per the versioning rule; build numbers
increment independently (207->208 Apple, 298->299 Android).

The staging workflow reads MARKETING_VERSION for its title and asset names, so main
has to carry 9.1.1 for the rolling testing-latest release to be labelled as such —
the previous 9.1.1 staging build was cut from a commit that never landed on main.

This is also the first compile of the app-target changes merged today (#783/#784/#785,
#787, #788, #793, #795, #519/#798, #523, #778), which CI does not cover: app-build.yml
and android.yml are disabled and only the Swift packages and the i18n gate run on PRs.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…yanbr#730) (ryanbr#785)

* Android parity: stop reporting a disarm that never reached the strap (ryanbr#730)

ryanbr#784 fixed this on iOS/macOS; Android had the identical defect.
disableStrapAlarm() logged 'Alarm: disarmed' unconditionally while send()
drops the write with 'ignored — not connected', so the app claimed the
firmware alarm was cleared when the command never reached the strap. If one
IS armed the strap still buzzes.

Mirrors the Swift fix: a commandChannelReady twin (the same conditions send()
guards on) decides which line is logged.

Android did NOT have the second iOS defect. reconcileStrapAlarm() is the sole
caller of arm/disarm, handles the disarm case internally, and is called
UNGATED on the bond edge (state.bonded && !lastBonded) plus the daily tick -
so a deferred disarm was already re-issued on reconnect. iOS's
smartAlarmEnabled guard was the outlier, and ryanbr#784 brought it into line.

The ryanbr#730 restored-connect diagnostic does not port: Android has no
CoreBluetooth state restoration, so that path has no Android analogue.

App-target Android: not compiled here (aapt2 is x86-only on this host) and
not hardware-tested.

* Make commandChannelReady private, matching the Swift twin

Nothing outside WhoopBleClient reads it, and the macOS twin is private -
keep the API surface identical on both platforms.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
MARKETING_VERSION/versionName move together per the versioning rule; build numbers
increment independently (207->208 Apple, 298->299 Android).

The staging workflow reads MARKETING_VERSION for its title and asset names, so main
has to carry 9.1.1 for the rolling testing-latest release to be labelled as such —
the previous 9.1.1 staging build was cut from a commit that never landed on main.

This is also the first compile of the app-target changes merged today (ryanbr#783/ryanbr#784/ryanbr#785,
ryanbr#787, ryanbr#788, ryanbr#793, ryanbr#795, ryanbr#519/ryanbr#798, ryanbr#523, ryanbr#778), which CI does not cover: app-build.yml
and android.yml are disabled and only the Swift packages and the i18n gate run on PRs.
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