Fix: Prevent random crashes on Android 10 devices#588
Merged
Conversation
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.
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 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
Float?receiver-arg unboxes when R8 horizontally merges small extension classes into stdlib hosts (kotlin.io.CloseableKt,kotlin.text.HexFormatKtin 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 throughtoBatteryFloat+4(popup) andmergeBatterySlot+40(cache merge); the+4and+40PC offsets are the first instructions, where the receiver-arg unbox lives.Float?boxing from the entire battery display and persistence path.PodDevice.battery*getters return primitiveFloatwith aBATTERY_UNKNOWN = -1fsentinel;mergeBatterySlottakes primitiveFloat;toBatteryFloatandtoBatteryOrNullare deleted. NewisKnownBattery(Float)/batteryProgress(Float)helpers replace scattered nullable-percent checks across cards, popup, widget, and notification renderers.toCachedStatedeliberately keeps rawaap?/ble?reads — it must NOT use the unifiedPodDevice.batteryLeftgetter, 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-slotupdatedAtandlastSeenAtdon't move when onlyDeviceInfochanged.persistLiveDevicesfrom7d93e772stays. 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 escapetoCachedState.bundleGplayReleaseminify output):toBatteryFloatis absent (deleted, dead-stripped),mergeBatterySlot(F, …, …)body has zeroLjava/lang/Float;->floatValue:()Fcalls and uses primitivecmpg-floatfor the percent comparison. Both fault sites from the crash traces structurally cannot recur. The remainingfloatValue()Fcalls in the dex are confined to the AAP/BLE protocol boundary insidetoCachedState, where they're scattered through the body rather than at function entry.