Shipping & Infra3 min read

I fixed half of it and got back a report saying it was fixed

A user said vibration was completely dead. I fixed three things, they said they could feel it now — and only two rungs of a three-rung signal ladder had actually changed.

#gotchas#android#verification#debugging
Concept diagram: two of three rungs on a haptic fallback ladder fixed, one left untouched
A diagram summarising the post.

I shipped a counter app. Each tap increments, and reaching certain boundaries and the goal produces different haptics. Three rungs of signal.

On launch day a report arrived: "I counted to 99 and felt nothing."

The in-app haptics toggle was on, system touch feedback was on, battery saver was off, and keyboard typing haptics worked fine.

When someone reports "no vibration," what do you check first?

You cannot see it

Haptics are invisible on screen and absent in an emulator. So I read the system dump.

dumpsys vibrator_manager

It records whether a request arrived, whether it played, whether it was ignored, and whether it fell back. The result: our requests reached the system, played, and zero were ignored.

The call path was fine. The problem was which effect we chose.

Three findings

First, the boundary haptic did not match the spec. The spec said two short clicks; the code used a weaker tick — and a unit test asserted that wrong value, pinning it in place.

Second, on devices without haptic primitive support we fell back to an 8-millisecond raw waveform. Most motors physically cannot produce 8ms.

Third, the per-tap amplitude looked low. I raised it from 0.6 to 0.85.

Shipped all three.

The user checked it

"The boundary and goal ones I can feel. Every tap is still dead."

That one sentence settled the cause.

That phone has no haptic primitive support, so it walks a ladder of predefined effects. What I fixed were two rungs of that ladder — boundary and goal. The per-tap rung was byte-for-byte what it had been.

And the third change, amplitude 0.6 → 0.85, only applies on devices that do support primitives. It never reached this user's phone at all.

The change I had labelled in the commit message as "a judgement, not evidence" turned out to explain nothing.

A fork in the road

When a report says "the boundary one works now," you can read it two ways: "it is fixed" or "the fixed parts and the unfixed parts just separated."

Which way do you read it?

Without this report I would have read the first and closed the issue. It split because the user told me what still did not work.

Fixing the last rung

On the fallback ladder, the per-tap haptic was using the weakest predefined effect. I moved it up one rung. The basis is not a guess but a fact about that device — keyboard typing haptics are felt on the same phone.

A test now pins that the three rungs are three distinct effects.

Self-check

  • For a feature with multiple signal levels, do you put every level in one table when fixing it?
  • In code that branches on device capability, is the branch you fixed the branch the reporter's device takes?
  • When asking a user to verify, do you ask "what works and what doesn't" instead of "does it work now"?

The honest part

I shipped four times in one day. Review turnaround of 30–40 minutes made that possible, but that does not mean I was accurate — it means I could iterate wrong fixes quickly.

A change made without evidence only contributes to the appearance of a fix. The amplitude change was exactly that.

Count how many stages of your last multi-stage fix you did not touch.

Related