Skip to content

Reaction: Fix false auto-pause from Bluetooth interference#559

Merged
d4rken merged 3 commits into
mainfrom
fix/playpause-debounce-ble-interference
Apr 29, 2026
Merged

Reaction: Fix false auto-pause from Bluetooth interference#559
d4rken merged 3 commits into
mainfrom
fix/playpause-debounce-ble-interference

Conversation

@d4rken

@d4rken d4rken commented Apr 29, 2026

Copy link
Copy Markdown
Member

What changed

Fixed a bug where music could auto-pause even though your AirPods are still in your ears, when you're in a high-interference environment (e.g. crowded city, dense Wi-Fi, lots of nearby Bluetooth devices). A single corrupt Bluetooth scan was enough to flip the wear state and trigger a pause; the app now requires multiple consistent readings before reacting.

Refs #557.

Technical Context

  • Classified ear-detection sources by trust: AAP (L2CAP, error-corrected), BLE_IRK_MATCH (RPA verified against the profile's identity key), BLE_PROFILE_FALLBACK (signal-quality fallback assignment), BLE_ANONYMOUS (no profile match), NO_LIVE_BLE (cache-only). Only the bottom two are debounced — identity-authenticated paths can't be misattributed to a different pair, so trusting them is safe and avoids extra latency for paired AirPods.
  • The debounce uses a sample-counter (3 consecutive not-worn readings before pause fires). PlayPauseMonitorKey carries seenLastAt as debounceFreshness for debounce-eligible sources only, so distinctUntilChangedBy doesn't collapse identical not-worn BLE samples and the counter can advance.
  • NO_LIVE_BLE (cached state with no live evidence) explicitly suppresses shouldPause. Otherwise a worn → cache-only transition can derive a not-worn state from null fields and fire pause without any real removal evidence.
  • Tightened toEarDetectionState() to prefer AAP aggregate whenever AAP EarDetection is present, even if per-side resolution falls through to BLE bits. Previously a defensive case (resolvedPrimaryPod == null while ble provides per-side bits) could let BLE bits drive the decision while AAP had the authoritative aggregate state.
  • Added INFO-level diagnostic line on every pause emission and DEBUG-level lines on every debounce state transition (STARTED / ADVANCED / RESET / COMMITTED), with the source classification, pod-count delta, bleKeyState, AAP connection state, and pauseSent outcome from MediaControl. Future user reports become easier to triage.
  • Caveat: BlePodMonitor.preferCaseContextPod can keep an existing case-context snapshot in place over an incoming non-case-context snapshot, preserving the old seenLastAt. This is fail-closed (delays a legitimate unauthenticated-BLE pause rather than firing a false one), documented inline.

Review checklist

  • Verify auto-pause still fires within ~1.5s when removing both pods on a stable BT connection (paired pair, AAP active — source=AAP)
  • For an unpaired pair (source=BLE_ANONYMOUS), verify auto-pause now requires ~3 advert cycles before firing
  • Verify a single momentary BLE glitch with pods still in ear no longer triggers a false pause
  • Confirm the new EarDetectionSource classification doesn't regress any existing reaction tests

Classifies the ear-detection source (AAP / BLE_IRK_MATCH / BLE_PROFILE_FALLBACK / BLE_ANONYMOUS / NO_LIVE_BLE) and applies a 3-sample debounce only to unauthenticated BLE paths. AAP and IRK-authenticated BLE pass through unchanged.

Also tightens toEarDetectionState() to prefer AAP aggregate over BLE per-side bits whenever AAP EarDetection is present, and suppresses pause on NO_LIVE_BLE (cache-only state) to avoid firing without live evidence.
@d4rken d4rken added bug Something isn't working labels Apr 29, 2026
d4rken added 2 commits April 29, 2026 17:11
- Commit pending pause when a trusted source (AAP / BLE_IRK_MATCH) corroborates the not-worn condition mid-debounce, instead of dropping pending silently.

- Scope debounceFreshness to not-worn samples only; identical both-in samples no longer pass distinctUntilChangedBy and can't accidentally trigger BLE-only auto-play confirmation.

- Add resetTolerance to PendingPauseDebounce so a single corrupt count-up advert no longer kills a legitimate pending pause; reorder reset checks so rawDecision.shouldPlay resets immediately.

- Drop bleKeyState from the INFO autoPause log; source already encodes trust without leaking key-configuration state to logcat.

- Add flow-level MonitorFlowTests verifying the distinctUntilChangedBy interaction with seenLastAt freshness, plus the #557-direction test (AAP-worn vs corrupt-BLE-not-worn) and rebound-tolerance test.

- Clarify in BLE_ANONYMOUS KDoc that the path is unreachable in production via DeviceMonitor.primaryDevice.
…app start

- Apply seenLastAt freshness to all unauthenticated BLE samples (worn and not-worn). The earlier scoping to not-worn-only collapsed the second worn sample for BLE-only autoplay confirmation, so the staged play never fired.

- Replace distinctUntilChangedBy with a manual filter so worn samples that need to reset an active pause debounce (count went up) can pass through even when the monitor key is otherwise identical.

- Skip BLE-only autoplay confirmation for trusted sources. With BLE_IRK_MATCH and AAP, autoplay now fires on the first not-worn -> worn transition, mirroring the pause-debounce skip on the same sources.

- Skip the reaction entirely when the previous emission had no live evidence (NO_LIVE_BLE). Prevents app-process-start from synthesising a fake not-worn -> worn transition and firing autoplay while the user is already wearing the pods. Same guard handles mid-session BLE gap recoveries.

- Add MonitorFlowTests covering process-start-worn, genuine-insertion-after-startup, mid-session BLE-gap recovery, IRK-matched immediate autoplay, BLE-only autoplay confirmation, 3-sample pause debounce, and rebound-tolerated debounce reset.
@d4rken d4rken merged commit e6dbd2d into main Apr 29, 2026
10 checks passed
@d4rken d4rken deleted the fix/playpause-debounce-ble-interference branch April 29, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant