Skip to content

fix(tv): stop a For You detail return leaving the screen without focus - #183

Merged
RXWatcher merged 3 commits into
Silo-Server:mainfrom
RXWatcher:fix/tv-foryou-detail-return-focus
Aug 6, 2026
Merged

fix(tv): stop a For You detail return leaving the screen without focus#183
RXWatcher merged 3 commits into
Silo-Server:mainfrom
RXWatcher:fix/tv-foryou-detail-return-focus

Conversation

@RXWatcher

@RXWatcher RXWatcher commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Returning from a detail screen could leave For You with no focus owner at all, so the D-pad stopped responding until you navigated away.

The dead end

When the relocation loop cannot reach the launch card it reports Exhausted. The entire recovery for that case was one unchecked call:

if (result == ForYouReturnFocusResult.Exhausted) {
    requestFocusSafely { forYouFocusRequester.requestFocus() }
}
latestOnDetailReturnFocusConsumed(detailReturnFocusRequest)

requestFocusSafely returns Handled / Rejected / Disposed, and the result was discarded. If that request did not take — the pill not attached on the frame we happened to ask — nothing retried, no other candidate was tried, and nothing was logged.

The silence is the reason this was hard to find. requestFocusSafely deliberately converts the FocusRequester is not initialized throw into a value rather than letting it surface, so a failed last-resort claim produced no warning in logcat at all.

The no-target branch a few lines above already does the right thing — it retries forYouFocusRequester across frames and only gives up after TvFrameRelocationMaxAttempts. This path just never got the same treatment.

The fix

Retry across frames, and fall through the filter pills (For You / Watchlist / Favorites). Those are composed for the life of the screen, so one can take focus even while the feed is still settling. A candidate that throws is treated as not yet attached rather than fatal, since the node can attach on a later frame. If nothing takes focus, say so.

Why it matches the report

Reported as intermittent, roughly one in three, on Shield after Watchlist → item → Play → Back:

  • intermittent — needs the relocation loop to exhaust and the fallback to miss
  • Shield-specific — slower device, more frames before the row attaches
  • after playback specifically — the most disruptive recomposition on that screen
  • genuinely stuck rather than misplaced — focus lands nowhere, not on the wrong element

Verification

  • new ForYouFallbackFocusTest covers first-candidate claim, fall-through, later-frame attachment, a throwing candidate, total failure, and no candidates
  • :androidTvApp:testDebugUnitTest green (forced rerun)
  • :androidTvApp:compileDebugKotlinAndroid clean

Not reproduced directly. Four cycles on a Google TV Streamer stayed clean — this was found by reading the path, not by catching it live. Worth a Shield test before merging.

Summary by CodeRabbit

  • Bug Fixes
    • Improved focus restoration when returning from content details.
    • Added fallback focus handling across For You, Watchlist, and Favorites controls.
    • Focus recovery now retries safely across frames and skips unavailable controls.
  • Tests
    • Added coverage for successful recovery, fallback selection, retries, exceptions, exhausted candidates, and empty lists.

Returning from a detail screen could leave For You with no focus owner at all,
so the D-pad stopped doing anything until the user navigated away.

When the relocation loop cannot reach the launch card it reports Exhausted. The
entire recovery for that was a single unchecked call:

    if (result == ForYouReturnFocusResult.Exhausted) {
        requestFocusSafely { forYouFocusRequester.requestFocus() }
    }

requestFocusSafely returns Handled / Rejected / Disposed, and the result was
discarded. If that last request did not take - the pill not attached on the
frame it was asked - nothing retried, nothing else was tried, and nothing was
logged, because requestFocusSafely deliberately converts the "FocusRequester is
not initialized" throw into a value rather than letting it surface. The failure
was silent as well as unhandled, which is why it never appeared in a log.

The no-target branch a few lines above already retries across frames; this path
did not. It now walks the filter pills, which are composed for the life of the
screen, so one of them can take focus even while the feed is still settling, and
reports when none of them could.

Reported as intermittent (roughly one in three) on Shield after
Watchlist -> item -> Play -> Back. Consistent with a slower device spending more
frames before the row attaches, and with playback return being the most
disruptive recomposition on that screen.

Not reproduced directly: four cycles on a Google TV Streamer stayed clean, and
this was found by reading the path rather than by catching it live.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46e601d0-a24b-4a75-8832-fefd3d14c708

📥 Commits

Reviewing files that changed from the base of the PR and between 75ec979 and 7d2f0cc.

📒 Files selected for processing (3)
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/TvRecommendationsFocusBridge.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/TvRecommendationsScreen.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/ForYouFallbackFocusTest.kt

📝 Walkthrough

Walkthrough

The TV recommendations screen now performs bounded, frame-spaced focus recovery across the For You, Watchlist, and Favorites controls. The new focus bridge handles rejected, unavailable, and throwing focus requests. Tests cover success, retries, and exhaustion.

Changes

Recommendations focus recovery

Layer / File(s) Summary
Fallback focus routine
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/TvRecommendationsFocusBridge.kt
The focus bridge retries candidates across frames, skips rejected or throwing requests, and returns after the first successful request or after exhaustion.
Screen integration and validation
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/TvRecommendationsScreen.kt, androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/recommendations/ForYouFallbackFocusTest.kt
Detail-return recovery now tries all three filter controls and logs a warning when recovery fails. Tests cover candidate selection, frame retries, exceptions, exhausted candidates, and empty input.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TvRecommendationsScreen
  participant TvRecommendationsFocusBridge
  participant FocusRequester
  TvRecommendationsScreen->>TvRecommendationsFocusBridge: request fallback focus
  TvRecommendationsFocusBridge->>TvRecommendationsFocusBridge: wait for the next frame
  TvRecommendationsFocusBridge->>FocusRequester: safely request focus for each candidate
  FocusRequester-->>TvRecommendationsFocusBridge: accept or reject request
  TvRecommendationsFocusBridge-->>TvRecommendationsScreen: return recovery result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: restoring focus after returning from a For You detail screen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RXWatcher

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@RXWatcher

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 6 minutes.

@RXWatcher

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@RXWatcher
RXWatcher merged commit 5e8c2a2 into Silo-Server:main Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant