Skip to content

Reaction: Fix case popup flickering when one AirPod is out of the case#606

Merged
d4rken merged 3 commits into
mainfrom
worktree-fix-598-popup-flicker
Jun 8, 2026
Merged

Reaction: Fix case popup flickering when one AirPod is out of the case#606
d4rken merged 3 commits into
mainfrom
worktree-fix-598-popup-flicker

Conversation

@d4rken

@d4rken d4rken commented Jun 7, 2026

Copy link
Copy Markdown
Member

What changed

Fixes the case‑open popup misbehaving when you're wearing one AirPod and the other is in the case. In that situation the popup would flicker — re‑appearing about half a second after you closed the lid — and sometimes stay on screen until you dismissed it by hand. It now shows once when you open the case and dismisses reliably when you close it (or when the AirPods go out of range while open).

Closes #598

Technical Context

  • Root cause: with one pod out of the case, the out‑of‑case pod broadcasts advertisement frames whose case‑lid byte is stale and decodes to a phantom "open" even while the case is physically shut. These are interleaved ~1:1 with the correct in‑case‑pod frames, so the derived lid state flapped OPEN↔CLOSED. Confirmed on AirPods Pro 1 (reporter's debug log) and reproduced on AirPods Pro 3 via live BLE logcat — the out‑of‑case frame is bit4‑only (status bit6=0), the in‑case frame is bit6=1.
  • The lid bit is now only trusted from a frame broadcast by a pod that is itself in the case (status bit 6) or while both pods are in the case (bit 2). A bit4‑only "one pod in case, this pod out" frame decodes to UNKNOWN, and the real state is recovered from a recent in‑case broadcast. This matches LibrePods' decoder, which gates on isThisPodInTheCase.
  • The history recovery is bounded by time (~2s) rather than a fixed frame count, so a missed CLOSED across a BLE scan gap can't keep resurrecting a stale OPEN.
  • The "won't dismiss" symptom needs a separate backstop: if the AirPods leave BLE range while the lid reads OPEN, no CLOSED frame ever arrives. A freshness check dismisses the popup ~4s after the OPEN broadcast stops refreshing. It tracks BLE freshness specifically (ble.seenLastAt), not overall device freshness, so a live AAP socket can't keep a stale lid on screen.
  • The popup and auto‑connect flows now key their de‑duplication on the derived lid state, not just the raw advertisement bytes — the effective lid can change (recovered from history) while the selected frame's bytes stay identical, which would otherwise swallow the show/hide transition.
  • Not addressed here (deliberately, to keep the change scoped): BlePodMonitor's snapshot selection can still pick a bit4‑only frame as the visible snapshot. The derived lid state is correct regardless, so this is now cosmetic (it can briefly surface that frame's battery/RSSI). The popup overlay is shared with the connection popup, so a stale‑close can dismiss a visible connection popup when both toggles are enabled — this is pre‑existing overlay behavior, not new here.

Review checklist

  • Decoder: bit4‑only (one‑pod‑out) frames → UNKNOWN; bit6/bit2 frames unchanged (DualApplePodsTest)
  • getLatestCaseLidState recovers CLOSED from in‑case history for a phantom current frame and never surfaces a phantom OPEN
  • Stale‑close keys off device.ble?.seenLastAt (BLE), not AAP/cache
  • No regression for AutoConnect CASE_OPEN or the overview lid display now that UNKNOWN can be returned
  • De‑dupe keys on caseLidState (popup + auto‑connect) so derived‑lid transitions aren't swallowed

When one pod is out of the case, the out-of-case pod broadcasts bit4-only
frames whose lid byte is stale and decodes to a phantom OPEN even while the
case is shut, interleaved ~1:1 with the correct in-case-pod (bit6) frames.
The derived lid state flapped OPEN<->CLOSED, so the case-open popup re-popped
~0.5s after closing and sometimes lingered. Verified on AirPods Pro 1 (issue
log) and Pro 3 (live BLE capture).

- Trust the lid bit only from in-case-pod (bit6) or both-in-case (bit2) frames;
  bit4-only frames now decode to UNKNOWN, and the real state is recovered from a
  recent in-case broadcast (matches LibrePods).
- getLatestCaseLidState recovers from history within a 2s age window instead of a
  fixed frame count, so a missed CLOSED can't keep a stale OPEN.
- Don't refresh the show-cooldown on a non-CLOSED hide, so a transient UNKNOWN
  can't suppress a genuine re-open.
- Add a freshness backstop: dismiss the popup if a fresh OPEN broadcast stops
  arriving (device left BLE range while open).
- Key popup/auto-connect de-duplication on the derived lid state, not just raw
  advertisement bytes, since the effective lid can change while bytes don't.

Closes #598
mvanhorn and others added 2 commits June 8, 2026 07:53
@mvanhorn independently fixed #598 in #605 by no longer resetting the
case cooldown on close, throttling the re-pop. We cherry-picked that
commit above to keep his authorship/credit, but this PR instead removes
the underlying lid-state flapping at its source (out-of-case pod's stale
lid byte -> UNKNOWN), so the cooldown can keep resetting on close and a
genuine close->reopen still shows the popup. Reverting his change here so
the two approaches don't stack; thanks @mvanhorn for the parallel work.
@d4rken d4rken merged commit af1b1b4 into main Jun 8, 2026
11 checks passed
@d4rken d4rken deleted the worktree-fix-598-popup-flicker branch June 8, 2026 06:05
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.

Case popup inconsistency.

2 participants