Skip to content

Fix: Prevent random crashes on Android 10 devices#588

Merged
d4rken merged 1 commit into
mainfrom
fix/battery-float-boxing-art-crash
May 6, 2026
Merged

Fix: Prevent random crashes on Android 10 devices#588
d4rken merged 1 commit into
mainfrom
fix/battery-float-boxing-art-crash

Conversation

@d4rken

@d4rken d4rken commented May 6, 2026

Copy link
Copy Markdown
Member

What changed

Fixed random crashes that could happen on Android 10 devices when the case-open popup tried to render battery levels, or when the app persisted last-known battery state in the background. Both crashes were a known build-optimizer (R8) interaction with the Android 10 runtime.

Technical Context

  • Root cause: R8 + Android 10/11 ART JIT miscompiles boxed Float? receiver-arg unboxes when R8 horizontally merges small extension classes into stdlib hosts (kotlin.io.CloseableKt, kotlin.text.HexFormatKt in our traces). Each prior fix (6e9b2759, 7d93e772, 5aa36b14) reduced the surface but only relocated the unbox — the crash followed it. 5.1.4-rc0 still leaked through toBatteryFloat+4 (popup) and mergeBatterySlot+40 (cache merge); the +4 and +40 PC offsets are the first instructions, where the receiver-arg unbox lives.
  • The durable fix is to eliminate Float? boxing from the entire battery display and persistence path. PodDevice.battery* getters return primitive Float with a BATTERY_UNKNOWN = -1f sentinel; mergeBatterySlot takes primitive Float; toBatteryFloat and toBatteryOrNull are deleted. New isKnownBattery(Float) / batteryProgress(Float) helpers replace scattered nullable-percent checks across cards, popup, widget, and notification renderers.
  • Cache extraction in toCachedState deliberately keeps raw aap? / ble? reads — it must NOT use the unified PodDevice.batteryLeft getter, which falls back to cache. Using the unified getter would re-stamp stale cached readings as fresh live data and refresh them indefinitely. Added a regression test asserting per-slot updatedAt and lastSeenAt don't move when only DeviceInfo changed.
  • The try/catch around persistLiveDevices from 7d93e772 stays. It's a JVM-level safety net only — it cannot catch the native SIGSEGV that's the actual crash mode here, but it's still useful for any Java-level exception that might escape toCachedState.
  • Verified post-R8 dex (bundleGplayRelease minify output): toBatteryFloat is absent (deleted, dead-stripped), mergeBatterySlot(F, …, …) body has zero Ljava/lang/Float;->floatValue:()F calls and uses primitive cmpg-float for the percent comparison. Both fault sites from the crash traces structurally cannot recur. The remaining floatValue()F calls in the dex are confined to the AAP/BLE protocol boundary inside toCachedState, where they're scattered through the body rather than at function entry.

Two SIGSEGV native crashes recurred on Android 10 in 5.1.4-rc0 inside JIT-cached code at toBatteryFloat+4 (popup) and mergeBatterySlot+40 (cache merge). Both functions had a boxed Float? unbox at function entry that R8 horizontally merged into stdlib host classes, where Android 10 ART JIT miscompiled the unbox.

Convert PodDevice battery getters to non-null Float with BATTERY_UNKNOWN sentinel and propagate primitive Float through every display/persistence consumer. mergeBatterySlot now takes primitive Float; toBatteryFloat and toBatteryOrNull are deleted. Add isKnownBattery and batteryProgress helpers used everywhere instead of scattered nullable checks. Raw live extraction in toCachedState avoids touching the unified getter so cached values aren't refreshed as live.
@d4rken d4rken added bug Something isn't working device support labels May 6, 2026
@d4rken d4rken merged commit 985b737 into main May 6, 2026
11 checks passed
@d4rken d4rken deleted the fix/battery-float-boxing-art-crash branch May 6, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working device support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant