Skip to content

Smartbar quick-action taps are dropped when the release event is already consumed (Dictate key needs two presses) #257

Description

@andrius-tr

Symptom

Every other tap on the Dictate key does nothing at all — no recording starts, no feedback, nothing in the log. Pressing again works. It affects the mic state and the send state equally, so a single dictation costs four taps instead of two.

It reads as a dead button rather than a slow one: the press is simply gone.

Reproduction

  1. Open the keyboard with the Dictate action in the Smartbar.
  2. Tap the Dictate key repeatedly, at a normal pace.
  3. Roughly every second tap does nothing.

Observed on a Pixel 8 Pro (Android 16), v5.3.0 built from source. Not sensitive to press duration or position — the dead taps land well inside the key.

What the log shows

Dead taps emit sendDown followed by sendCancel 12–27 ms later; working taps emit sendDownsendUp. The pattern alternates cleanly:

50.871 sendDown -214 → 50.930 sendUp      ✅
52.193 sendDown -214 → 52.209 sendCancel  ❌ (16ms)
54.064 sendDown -214 → 54.136 sendUp      ✅
56.285 sendDown -214 → 56.303 sendCancel  ❌ (18ms)

12–27 ms is far too fast to be a finger lifting, and QuickActionButton never leaves composition (I instrumented the DisposableEffect(action, isEnabled) — it does not fire for -214 around these taps). So the cancellation comes from the gesture itself: waitForUpOrCancellation() returns null.

Instrumenting that return shows the event responsible:

cancel@MAIN type=Release n=1 id=7 pressed=false consumed=true pos=Offset(6.0, 14.8)

The release — the finger genuinely lifting on the key — arrives already consumed.

Root cause

changedToUp() is defined as !isConsumed && previousPressed && !pressed. A consumed release therefore isn't recognised as an "up" at all, so waitForUpOrCancellation() falls through to its cancellation path and a completed tap is discarded.

Both gesture branches in QuickActionButton are affected — the long-press-aware one the Dictate key takes when idle, and the plain else branch it takes in its send state — which is why the mic and send buttons fail identically.

What I could not determine: what consumes the release. I fixed it at the button, but you may prefer to fix it at the consumer, which is why this is an issue rather than a PR.

Fix I'm running

A drop-in replacement for waitForUpOrCancellation() that

  • detects the lift with changedToUpIgnoreConsumed(), so a consumed release still counts as a tap, and
  • cancels only when a change is consumed while still pressed — the genuine "another handler took the drag" case.

It also drops the isOutOfBounds cancellation, so a thumb that rolls a few pixels doesn't cost a tap. Used by both branches.

Verified on device: 6 presses → 6 sendDown, 6 sendUp, 0 sendCancel, where the same test previously alternated.

Happy to open a PR if the approach looks right to you — and equally happy to test a fix at the consumer end instead, since that may be the more correct layer.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions