Skip to content

android: fix head-gesture axis bias, add sensitivity slider - #1

Draft
ohuc wants to merge 2 commits into
feat/wearos-and-foss-unlockfrom
feat/head-gesture-reliability
Draft

android: fix head-gesture axis bias, add sensitivity slider#1
ohuc wants to merge 2 commits into
feat/wearos-and-foss-unlockfrom
feat/head-gesture-reliability

Conversation

@ohuc

@ohuc ohuc commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What

Head gestures (nod = answer, shake = decline) gave seemingly random results. This reworks the detector and adds a user-facing sensitivity slider.

Review note: only the second commit (android: fix head-gesture axis bias...) is the actual change — 3 files. The first commit is a checkpoint mirroring the working copy's uncommitted M3 Expressive redesign state, so this fix applies on top of the code the app is actually built from.

Why it was 'randomly biased'

Four compounding bugs in GestureDetector.kt:

  1. Cross-axis contaminationpeakIntervals/movementSpeedIntervals were shared between the vertical and horizontal axes, so motion on one axis corrupted the other's rhythm score.
  2. Index saturation — extremes were recorded with buffer.size - 1 as their index, which pins at 99 once the 100-sample ring fills; the chronological sort and peak/trough alternation check then ran on scrambled data (i.e. detection quality degraded the longer the phone rang).
  3. No expiry — peaks/troughs accumulated forever, so a stray early movement (glancing at the ringing phone) permanently primed one axis; whichever axis got primed first usually won.
  4. No baseline removal — the head-tracking accel pair carries a posture-dependent DC offset, so fixed absolute thresholds favoured whichever axis rested further from zero and broke sign-alternation scoring.

The rework

  • Strictly per-axis state (extremes, rhythm, direction tracking) behind a single lock.
  • 8-sample baseline warmup + slow EMA (α = 0.04) — all thresholds now apply to a centered signal, immune to posture offset/drift.
  • Extremes outside a 2.6 s window are pruned; only the gesture happening now counts.
  • Alternation checked on chronological peak/trough types instead of value signs.
  • Same confidence blend as before (amplitude 0.4 / rhythm 0.2 / alternation 0.2 / isolation 0.2, fire at ≥ 0.7), same both-axes tie-break.

Sensitivity slider

New pref head_gesture_sensitivity (0..1, default 0.5 ≈ historical thresholds), surfaced on the Head Tracking screen. It interpolates the peak threshold (550→150), full-score amplitude (950→350) and audio-feedback threshold (900→300). Read at every startDetection(), so 'Test Head Gestures' picks changes up immediately.

Testing

  • :app:compileFossDebugKotlin and full assembleFossDebug build green.
  • No device attached in this environment — on-device: adjust the slider, then use Test Head Gestures on the Head Tracking screen; adb logcat -s GestureDetector shows per-axis confidence. The GCAP capture broadcast is still in place for offline tuning if the endpoints need refinement.

🤖 Generated with Claude Code

ohuc and others added 2 commits July 15, 2026 13:26
…sign WIP)

Snapshot of the main checkout's uncommitted changes so the head-gesture
rework in the next commit is a clean, cherry-pickable delta on top of the
exact state the app is currently built from. Not authored in this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nod/shake detector produced seemingly random yes/no results. Four
compounding bugs biased it toward whichever axis happened to get primed:

- peakIntervals/movementSpeedIntervals were shared between the vertical
  and horizontal axes, so incidental motion on one axis corrupted the
  other's rhythm score.
- Extremes were recorded with buffer.size-1 as their index, which
  saturates at 99 once the 100-sample ring fills; chronological sorting
  and the peak/trough alternation check then operated on scrambled data.
- Peaks/troughs never expired, so a stray early movement (glancing at
  the ringing phone) permanently primed an axis for the whole call.
- No DC-baseline handling: the accel pair carries a posture-dependent
  offset, so fixed abs-value thresholds favoured whichever axis rested
  further from zero and broke sign-alternation scoring.

Rework: strictly per-axis state (peaks, rhythm, direction) behind one
lock; an 8-sample baseline warmup plus slow EMA so all thresholds apply
to a centered signal; extremes pruned outside a 2.6 s gesture window;
alternation checked on chronological peak/trough types.

Detection thresholds now derive from a user-tunable sensitivity
preference (head_gesture_sensitivity, 0..1, default 0.5 ~= previous
behaviour), exposed as a slider on the Head Tracking screen and read at
every startDetection() so the in-app gesture test reflects it
immediately.

Co-Authored-By: Claude Fable 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