Fix: AirPods data no longer freezes on Android 10/11#578
Merged
Conversation
Battery slot percent comparisons in mergeBatterySlot/hasStateChanged compiled to Intrinsics.areEqual on boxed Float; R8 optimization on Android 10/11 dropped a null check during inlining and the resulting NPE escaped onEach { persistLiveDevices }, cancelling the upstream combine and freezing every observer of DeviceMonitor.devices.
Comparisons now operate on primitive float (cmpg-float in dex) so no Intrinsics.areEqual call remains in the merge path. The persist loop also catches and reports per-profile, and AAP-only profiles with active DeviceInfo are now persisted even when no BLE pod is in range.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Fixed a bug where the app would silently stop updating AirPods data (battery levels, in-ear status, case-open popup, widgets) on some Android 10 and 11 devices. Once the bug triggered, every screen and widget would freeze on the last value seen until the app was force-quit.
Also rolled into the same fix:
Technical Context
existing.percent == livePercentinmergeBatterySlotcompiled toIntrinsics.areEqual(Float, Float)becauselivePercent: Float?retains JVM typejava.lang.Floateven after the smart cast. R8 inlinedareEqualand dropped the second-operand null check on Android 10/11's interpreter+JIT path, NPE'ing inside the supposedly null-safe helper.liveState.onEach { persistLiveDevices(...) }, cancelling the upstreamcombine.replayingShare'sWhileSubscribed(Duration.ZERO)then re-ran the deterministic crash on every restart, so observers were left stuck on the cachedreplay=1value.mergeBatterySlotnow Elvis-unboxesFloat?to primitivefloat; slot percent comparisons inhasStateChangedare routed through a newhasSlotChangedhelper that compares primitivefloatdirectly. Verified at the dex bytecode level viaapkanalyzer: both methods emitcmpg-floatwith noIntrinsics.areEqualinvocation in the merge path.hasStateChangedalso now comparesleftEarbudSerial/rightEarbudSerial/marketingVersion(added in #32ff7e5b but never wired into dedup), andtoCachedStateno longer skips when only DeviceInfo is fresh. To make that path actually fire,liveState.onEachnow also persists AAP-only profiles (active AAP, no BLE in this scan).try/catchinpersistLiveDevicesis defense-in-depth: any future merge-path bug logs and reports once perprofileIdinstead of cancelling the upstream flow.Review checklist
mergeBatterySlotandhasSlotChangedshowcmpg-floatand noIntrinsics.areEqualin the dex bytecode ofassembleFossReleaseliveState.onEachdoesn't double-write or feedback-loop against the existingnonLiveDevicessynthesis in thedevicesflowreportedPersistFailuresdedup is acceptable as a process-lifetime per-profile suppression