A One-Sentence Bug Took 12 Rounds to Find Because AI Won't Tell You to Add Logs
📌 Conclusion First
Here's the root cause, in one sentence:
The physical keyboard IME on the handheld device breaks a single "letter key" press into three key events:
DEL + SHIFT + letter, and the DEL actually deletes a character.
That's it. But to arrive at that sentence, I modified the code 12 times, compiled and deployed over a dozen times, and captured logcat three times.
This article isn't about showing off "I fixed it," but a review: Why did such a simple problem drag on for so long? Especially when a large language model (AI programming assistant) acts as a pair-programmer, which pitfalls can magnify a simple bug endlessly.
🎯 1. What the Bug Looked Like
Scenario: On a surveying handheld device, an "angle/distance" numeric input field. In physical keyboard English mode, pressing a letter key (e.g., S) would delete one digit from the existing number in the input field. Pressing it multiple times would decrement the number all the way down to 0.
The user's original requirement was as simple as it gets:
In a numeric input field, pressing a letter key should be ignored; it shouldn't delete the existing number.
That's it. Four lines of requirements, 12 rounds of fixing.
🔄 2. The 12-Round Tug-of-War: The Symptom Kept "Disguising Itself"
This is the most counter-intuitive and noteworthy part of the whole thing: The same bug presented completely different symptoms at different stages of the fix. Each round you think, "This time it must be right," and the next round it pops up in a new guise.
| Round | Symptom the User Saw | Root Cause I Assumed | Result |
|---|---|---|---|
| 1~8 | Rapid double-press "replaced" the first input | setSelectAllOnFocus(true) leaving a selection residue |
❌ All wrong |
| 9 | Letter key couldn't be blocked at all | Return value was inverted | ⚠️ Half right |
| 10 | Letter no longer input, but deleted one char each time | Interception logic conflict | ❌ Still wrong |
| 11 | Content decremented all the way to 0 | IME key splitting | ✅ Close to the truth |
| 12 | — | DEL + SHIFT + letter triple-hit | 🎉 Solved |
Core conclusion: Symptoms disguise themselves. Each layer of interception alters the event flow. Only when you fix the layer above does the real problem beneath get exposed. So the primary reason it "took so long to fix" isn't that the opponent was strong, but that the target kept moving.
💣 3. Three Major Pitfalls I Fell Into (AI Collaboration Amplifies Them)
Pitfall 1: Stuck in the "Selection/Select-All State" Mindset for the First 8 Rounds
I kept believing the culprit was a focus select-all residue caused by setSelectAllOnFocus(true), so I repeatedly tinkered with "collapsing the selection" in onTouchListener, TextWatcher, InputConnection.setSelection, and onSelectionChanged.
The problem was: I was fighting a problem I had imagined, instead of returning to the user's original requirement.
What was the requirement? "Letter keys should be ignored," not "the select-all state needs to be collapsed." These two things are worlds apart.
The amplifying effect of AI collaboration: AI assistants are particularly prone to follow your assumptions. Once you write "I think it's a select-all residue problem" in your prompt, it will help you go further and further down this wrong path, even fabricating mechanism explanations that "seem plausible."
⚠️ If your assumption is wrong, the AI won't pull you back; it will only help you argue your wrong assumption more convincingly.
This is my biggest lesson: First, stand back at the requirement's origin yourself; don't feed the wrong presupposition to the model.
Pitfall 2: Tripped Up by the Return Value Semantics of return false
Inside dispatchKeyEvent / onKeyDown, I wrote return false.
In Android's event dispatch:
return false // ❌ Don't consume, continue passing down → event still handed to TextView to insert text, interception is useless
return true // ✅ Consume, event stops here
A single character's difference rendered a whole round of "interception" void. This is the most basic mistake one shouldn't make, but also the easiest to make—because large models generally don't get this default semantic wrong when generating code, but it's easy to slip up when you're copying, pasting, and modifying branches.
Pitfall 3: The Real Culprit Was "Device-Specific Non-Standard IME Behavior"
A standard IME pressing a letter key = one commitText. But this handheld device's physical keyboard IME split a single letter key press into a triple-hit:
onKeyDown: keyCode=67 ← DEL (backspace, actually deletes the last character)
onKeyDown: keyCode=59 ← SHIFT
dispatchKeyEvent: KEYCODE_W ← Letter key (intercepted, not inserted)
Net effect = Each press of a letter key deletes one character.
This kind of device-specific behavior cannot be predicted by any reasoning or any experience. Large models don't know it either, because it's not standard behavior documented anywhere. The only way to discover it is through logs.
⏱️ 4. "Add Logs Earlier" — Why This Obvious Advice Is So Costly
Looking back at the whole process, the most painful part wasn't the technical difficulty, but the methodological sluggishness:
- The first 4 rounds were basically "blind fixing": No log evidence, relying entirely on guesswork. Modify, compile, deploy to the handheld, physically test, fail, guess again. All 4 rounds were wasted.
- Only at round 5 did I start comprehensive instrumentation: Adding
Log.dat every event entry point, printingkeyCode,sel=[start,end], timestamps, and call stacks. From that point on, I truly began to "see" the problem, rather than "guess" it.
A simple rule I only summarized afterward:
If two consecutive fixes still fail, immediately stop and add logs; don't continue guessing a third time.
Why call it "costly"? Because the verification loop cost for each round was extremely high:
Modify code → Compile → Deploy to handheld → Physically test with keys → Capture logcat
One round could take ten to twenty minutes. 12 rounds meant most of a day. If I had stopped to instrument when the second round failed, I likely could have cut out more than half the rounds.
In the era of large models, this advice is even more important: AI can make the cost of "changing code" extremely low, so low that you're more prone to the impulse of "let's try a few more versions" and forget to "see clearly before acting." The faster you can change things, the more you must guard against "blind changes."
✅ 5. The Final Solution (Actually Just Two Layers)
The core idea in one sentence: Delay the decision on the DEL key, using timestamps to distinguish between "an IME-mistakenly-sent leading DEL" and "a user's genuine backspace."
- When
onKeyDownintercepts aDEL(only in the numeric input field): first snapshot the current text, record the timestamp, execute the real deletion as usual, but mark it as "suspected mistaken dispatch." - When
dispatchKeyEventintercepts a letter key: if the previous DEL and this letter key are within 300ms, it means this DEL was a leading dispatch mistakenly sent by the IME, use the snapshot to undo the deletion; if the interval exceeds 300ms, it means the user genuinely pressed backspace, so don't undo.
The real timestamp difference of 30~40ms is the basis for distinguishing "mistaken dispatch" from "real operation."
📝 6. Notes to My Future Self When Troubleshooting with AI Again
- Instrument first, then act. If two consecutive fixes fail, stop and add logs; don't guess a third time.
- Return to the essence of the requirement. What was needed here was "letter keys should be ignored," not "the select-all state should be collapsed." Don't feed an imagined root cause to the AI; it will only help you argue the wrong point more convincingly.
return false≠ interception. In Android event dispatch, onlyreturn truecounts as consumption.- Device-specific behavior cannot be predicted by reasoning. The handheld IME splits a letter key into a "DEL + SHIFT + letter" triple-hit. This type of problem can only be approached layer by layer through logs, using a "time window + snapshot undo" to handle it.
- Symptoms disguise themselves. Each time you fix one layer, the real problem of the next layer is exposed. Don't be fooled by the "changed guise"; persist in returning to the logs to find the truth.
🎬 Final Words
Looking back, all the agonizing of these 12 rounds ultimately converged into a one-sentence root cause and a two-layer fix.
What's complex is never the problem itself, but the process of "discovering it." And what determines this process isn't technical skill, but whether you chose from the very beginning to "see clearly with logs" rather than "guess hard with your brain."
Nail this sentence to your workstation:
The faster you can change things, the more you must first see clearly.
If this review was helpful to you, give it a 👍 and bookmark it. Feel free to share in the comments your experiences of being "led astray" by an AI programming assistant~
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Thanks for sharing