Every Layer Connected Perfectly, Yet BACK Still Died: The Culprit Lived Only in 34 Milliseconds of Logs


Halfway through real-device testing, a bug surfaced with almost provocative cleanliness: with the popup open, pressing BACK did nothing. It didn’t crash, it didn’t freeze, it just did nothing—as if the key didn’t exist.

The logs detailed “did nothing” much more precisely. During this round of testing, the log recorded four instances of the capacity-limit message, and the closing reason for all of them was reason=timeout—the 3000 milliseconds elapsed and it closed itself. There were zero entries for reason=back. The BACK key was pressed, but to the system, it was as if it never happened.

How This Project is Tested

This shares the same background as other stories from this project: rewriting an Android TV app from the legacy version (Android 9, Java) to a new version (Android 16, Kotlin, Jetpack Compose). After completing each phase, real-device testing is run on a TV box in the lab—I operate the remote control, Claude parses the logs, and verification is done item by item against the checklist.

This phase involved rebuilding the legacy “Favorites List” feature: pressing the yellow key on the remote while watching TV brings up a popup to add the current channel to the favorites list. The list has a capacity limit, and when full, a “limit reached” message pops up, which auto-closes after 3000 milliseconds. Per the spec, the BACK key must be able to close the popup and the message.

I discovered the symptom. Pressing BACK while the popup was open did nothing; pressing BACK when the limit message appeared also did nothing. Two checklist items failed on the spot.

Round 1: Static Analysis, Four Layers Perfect

Claude’s first round involved static analysis—reading through every layer the BACK event would pass through from the physical press to “closing the popup”:

  • The main screen’s Box: returns return false to pass the event during the popup and message display—directional keys cannot be intercepted, otherwise focus within the popup wouldn’t move;
  • The popup’s own onPreviewKeyEvent: passes the Back event;
  • The root container of the overlay: not focusable, does not consume keys;
  • BackHandler: contains the branch handling the message-to-popup flow.

Four layers, and each one looked correct in isolation. On the actual device, everything died.

However, laying out this checklist revealed one thing: when this BACK key is pressed, multiple actors along the path are watching it. Staring at this list, I asked a question: Can this be unified into a single entry point? This question would return later—the shape of the final fix grew directly out of its answer.

Round 2: Empirical Bisection

Static analysis yielded no answers, so the approach shifted to empirical testing. Three steps, narrowing the scope with each.

Step one: injection also fails. Using adb to inject a BACK event into the popup I had opened via the remote—it still died. The physical remote and adb injection are two distinct key-delivery paths; since both failed, the issue definitively resided inside the app. The remote control side required no further investigation.

Step two: the neighbor is alive. Pressing BACK on another overlay (the channel list) on the same screen—it worked, and the log showed trigger=back. The defect wasn’t “BACK is dead across this entire screen,” but was narrowed down to the specific scenario of “focus resting inside the popup grid.”

Step three: confirming delivery. dumpsys input showed the event was indeed dispatched to the app. The app received the BACK event, and then lost it.

The Verdict: 34 Milliseconds

The decisive evidence was a 34-millisecond snippet of logs. The instant the BACK event was injected (08:44:59.423–.457):

FOCUS_CHANGED mainBox focused=true
OVERLAY_FOCUS_RECLAIM

The main screen’s Box gained focus, and within 34 milliseconds, the overlay reclaimed focus back to the popup. But BACK is a paired event consisting of “press + release”—after the DOWN event, focus changed hands, breaking the tracking for this keystroke, so when the UP event arrived, it could no longer form a complete press. OnBackPressedDispatcher was never triggered from start to finish.

The BACK event wasn’t undelivered; it was tripped up and killed during this 34-millisecond focus tug-of-war.

There is an additional footnote: this FOCUS_CHANGED observation log wasn’t added for this bug. It was added three weeks ago when fixing another focus issue (a keystroke black hole after LiveTv re-locked), purely to see where the focus was going. This time, it became the sole eyewitness.

The Project Had Hit This Wall Before

The control group narrowed the scope another ring: the settings page dialog—which had the same grid holding focus and the same overlay root container—had a working BACK key, proven empirically in a previous phase. The defect wasn’t in the “grid focus + overlay” combination itself, but in the specific configuration of this screen.

Moreover, this wall had a criminal record. The only precedent on this TV screen involving a “grid holding focus,” the PIN unlock dialog, had historically intercepted the BACK key itself in the preview phase—bypassing this exact wall. Except, after bypassing it, the experience remained isolated as “how that dialog does it” and never upgraded into “this type of configuration always behaves this way,” leading to hitting it a second time.

The Fix: Bypass the Wall, Don’t Demolish It

Returning to my single-entry-point question. Claude’s judgment after analysis was: the integration effort would be too massive, and by aligning with Android’s own mechanisms, this defect could actually be fixed.

The fix followed this judgment—do not refactor the entry point, but align with the mechanism at the exact point of failure: the popup’s onPreviewKeyEvent intercepts the BACK key itself, consuming it on KeyDown (blocking that focus dance from ever starting), and only executing the action on KeyUp. This “block on KeyDown, execute on KeyUp” rhythm is the same approach used by another existing BACK interception on the same screen; the BackHandler layer was kept as a fallback.

Implementation was handed off to Gemini 3.1 Pro, and qwen’s independent review resulted in a PASS. Re-verification followed a dual path: I pressed it once with the remote, and Claude injected it once via adb. Both sides received a reason=back log entry. The BACK key had returned.

Conclusion

This incident leaves two reminders.

First, static analysis is ineffective for issues like “focus/key dispatch.” The wiring in all four layers was correct because the defect wasn’t in any single layer’s wiring—it resided in the interaction between layers, existing only on the runtime timeline. The verdict didn’t come from any line of code, but from two lines of logs within 34 milliseconds. Yet, the static analysis wasn’t in vain: it didn’t find the culprit, but it did lay out “how many actors are watching this key along its path”—and the question of a single entry point grew out of that unfolded map.

Second, the right fix does not equate to the right architecture. A single entry point is architecturally the cleanest; it was proposed, evaluated, and concluded to be too much effort. The actual fix aligned with platform mechanisms for a targeted single-point solution, identical in shape to how the PIN dialog bypassed it years ago. Sometimes, “bypassing the wall” is the correct way to handle the wall—provided you know where the wall is, and knowing where it was relied entirely on those 34 milliseconds.

Incidental Findings

  • Filtering in logcat: When assigning multiple levels to the same tag (SV_LiveTv:I SV_LiveTv:W SV_LiveTv:E), the latter overrides the former, completely filtering out the I level. A single :I per tag is sufficient—I automatically encompasses W/E.
  • adb injection and human interaction will step on each other’s toes: This happened three times this round (swallowed keys during transitions, injections hitting the home screen after HOME was pressed). Before running injection tests, always give a heads-up to whoever is holding the remote.