Skip to content

android(fix): crash loop on reconnect with heart rate alert enabled - #807

Open
QuerTeal wants to merge 2 commits into
librepods-org:android/rewritefrom
QuerTeal:fix/hr-crash-metadata-race
Open

QuerTeal wants to merge 2 commits into
librepods-org:android/rewritefrom
QuerTeal:fix/hr-crash-metadata-race

Conversation

@QuerTeal

Copy link
Copy Markdown

Summary

With Heart rate alert enabled, the app crashed every time the AirPods reconnected. I saw 6 crashes in about 4 minutes:

java.util.NoSuchElementException: Char sequence is empty.
    at kotlin.text.StringsKt___StringsKt.first(_Strings.kt:77)
    at me.kavishdevar.librepods.devices.AppleDevice.startHr(AppleDevice.kt:348)
    at me.kavishdevar.librepods.services.LibrePodsService$onDeviceConnected$1.invokeSuspend(LibrePodsService.kt:308)

Two bugs combine to cause this. Each is fixed in its own commit.

1. Device metadata was persisted empty (lost update)

AppleDao.updateSettings(), updateMetadata() and saveCache() each read the whole AppleEntity and upsert it back.

saveCache() runs on every state change. When the AirPods connect, about 20 control-command updates arrive together with the information packet, so these calls race. The metadata written after the information packet was almost always overwritten with the stale, empty AppleMetadata().

The log showed the pattern clearly: Saving AppleMetadata ... version3=9442752 was followed on the next load by Loaded metadata: AppleMetadata(model=UNKNOWN, ..., version3=).

Fix: annotate the three methods with @Transaction. Room generates performInTransactionSuspending wrappers, so each read-modify-write is serialized.

2. version3.first() throws when the firmware version isn't known

startHr()/stopHr() and startHeadTracking()/stopHeadTracking() pick the sensor service with metadata.value.version3.first().digitToInt(). onDeviceConnected() calls startHr() right after loadInitialState() whenever the alert is on. With the empty metadata from the DB, that call throws.

Fix:

  • Add a null-safe firmwareMajorVersion().
  • Skip starting or stopping the sensor service while the version is unknown, and log a warning.
  • Start heart rate monitoring from observeAppleMetadata() once the information packet supplies the version, if the alert is enabled and HRM is inactive.

Testing

Tested on a Galaxy S26 Ultra (One UI 9.0, SDK 37) with AirPods Pro 3 (fw 9442752), with the heart rate alert enabled.

  • Crashes: none after reconnecting. Before the fix there was one crash per reconnect.
  • Heart rate: the sensor request (04 00 04 00 17 ...) is sent as soon as the metadata arrives.
  • Persistence: after am force-stop and relaunch, Loaded metadata returns model=AIRPODS_PRO_3, version3=9442752 instead of an empty AppleMetadata.

This branch builds on its own and is independent of the other two PRs.

🤖 Generated with Claude Code

QuerTeal and others added 2 commits September 28, 2026 00:26
updateSettings(), updateMetadata() and saveCache() read the whole
AppleEntity and upsert it back. The cache is saved on every state change,
so these ran concurrently and overwrote each other with stale columns.
In practice the metadata written right after the information packet was
lost almost every time, and the db kept an empty AppleMetadata.

Mark them @transaction so each read-modify-write is serialized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startHr()/stopHr() and head tracking used metadata.version3.first(),
which throws on an empty string. With the heart rate alert enabled,
onDeviceConnected() calls startHr() right after loading the (empty, see
previous commit) metadata from the db, so the app crashed on every
reconnect.

Parse the firmware major version null-safely and skip starting the
sensor service while it is unknown; start heart rate monitoring once the
information packet provides it if the alert is enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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