One Function, Two Truths: Why the Infobar Read a Stale Channel
The infobar on music channels was supposed to stay pinned—switching to a music channel should keep the mini-EPG on screen without auto-dismissing, while tuning away should restore the usual dismissal timeout. That was the behavior specified in the plan. But when I tested it on physical hardware with the remote, two of the three channel-change directions broke:
| Channel Change Direction | Expected Behavior | Device Behavior |
|---|---|---|
| Non-Music → Music | Infobar stays pinned | Dismisses (Incorrect) |
| Music → Music | Infobar stays pinned | Stays pinned (Correct) |
| Music → Non-Music | Dismisses after timeout | Stays pinned (Incorrect) |
It looked as if the infobar was using the previous channel to decide whether the current channel was a music channel.
Two Readings Inside a Single Function
Channel changes pass through a function named tuneTo. When Claude inspected the code, it found that in the unlocked-channel branch, showMiniEpg("zap") was being called before tvViewManager.tune(channel) (LiveTvViewModel.kt:494).
showMiniEpg manages the infobar presentation and relies on isAudioChannel() to determine whether to pin the bar. In turn, isAudioChannel() reads tvViewManager.currentChannel.value. The root issue was that currentChannel isn’t updated to the new channel until TvViewManager.tune() actually executes (TvViewManager.kt:163). Because showMiniEpg was invoked prior to tune, currentChannel still held the old channel, causing the audio check to evaluate stale data.
However, the ordering mistake wasn’t the most important takeaway from this bug. The real problem was that within the exact same tuneTo function, two different places read the single source of truth—whether the current channel is a music channel—and arrived at conflicting answers:
- The music channel background plate derived its logic from
channel—the new channel parameter passed intotuneTo. - The infobar guard clause read
currentChannel.value—the state that hadn’t updated yet.
One function, one moment in time, one truth—giving two different answers. The background plate was correct while the infobar was wrong. That contrast was the clue: the channel-change operation as a whole wasn’t referencing the wrong channel; the background plate read the correct data. The failure was isolated to the infobar because it was the only caller accessing the un-updated state. The “Music → Music” scenario in the test matrix only appeared correct by coincidence: the previous channel happened to be a music channel, so the stale state still passed validation. As soon as the transition was “Non-Music → Music”, the old channel failed the check, exposing the bug.
Two Ways to Fix It
There were two ways to fix this, and the difference between them is the point of this post.
The first option was to swap the execution order: move showMiniEpg("zap") after tvViewManager.tune(channel). Without altering a single line inside showMiniEpg, simply swapping the invocation order produced the correct behavior on the physical device. Furthermore, this matched the literal wording of the plan, which explicitly prescribed that sequence. I considered this route, but decided against it.
The second option—the one I chose—was to add an explicit parameter to showMiniEpg: audioChannel: Boolean = isAudioChannel(). Keeping isAudioChannel() as the default value preserves existing call sites that don’t trigger a channel change (such as pressing the DOWN arrow). For the actual channel-change zap point, the code explicitly passes the new channel’s evaluation: audioChannel = ChannelGroup.AUDIO in channel.groups. This reads directly from the incoming channel parameter instead of depending on the timing of currentChannel.
Both paths achieve correct runtime behavior. The difference is that the order-swap approach leaves the timing dependency hidden inside implicit state—the correctness of showMiniEpg becomes permanently dependent on an unwritten assumption that it happens to run after tune. If tune is ever made asynchronous or if someone moves the showMiniEpg call elsewhere, it will break silently in the exact same way: falling back to reading the old channel.
Choosing the explicit parameter meant deviating slightly from the literal text of the plan. But the plan only described the “correct order”; it didn’t explain why that order was correct. Anyone following the spec verbatim wouldn’t understand why their implementation worked, nor would the next contributor know what might break when refactoring.
A regression test was locked in around this design intent: invoke the real tuneTo, but deliberately populate currentChannel with the old channel state. If the zap call site ever gets reverted to omit the argument and fall back to the default isAudioChannel(), it will read the stale channel, schedule a dismissal timeout, and fail the test. Both channel-change directions—Non-Music to Music remaining pinned, and Music to Non-Music timing out—have been verified on physical hardware.
Conclusion
Following the literal wording of a specification often legitimizes fragility. The plan said “call tune before showMiniEpg,” and following that sequence produces correct behavior, but that correctness hinges on an unannounced precondition: call order. The spec specifies what correct looks like, not where correctness originates.
When two places inside a function require the exact same truth, pass them the same explicit source. Do not let one place read an argument while another reads mutable state. Swapping the invocation order fixes today’s symptom; passing an explicit parameter fixes how the symptom was created in the first place.