fix(tv): guard focus restorer against uninitialized FocusRequester - #613
Open
ReichiMD wants to merge 2 commits into
Open
fix(tv): guard focus restorer against uninitialized FocusRequester#613ReichiMD wants to merge 2 commits into
ReichiMD wants to merge 2 commits into
Conversation
Wrap composable FocusRequester.requestFocus() calls in runCatching so a requester that fires before its focusable node is attached no longer crashes the app (IllegalStateException: FocusRequester is not initialized). The live tv package already followed this pattern; apply it to the remaining overlays/menus that become visible on TV playback and search navigation.
…ester
Compose's focusRestorer(onRestoreFailed = { requester }) captures the
requester at modifier-application time and, on a focus enter whose
automatic restore fails, calls requestFocus() on it ITSELF — deep inside
the modifier, without a try/catch. When the restore target lives in a
LazyColumn row that is not yet composed/attached (the case right after
categories finish loading on a fresh TV entry into Live TV), that
internal requestFocus() throws IllegalStateException: FocusRequester is
not initialized and crashes the app — unreachable by the runCatching
guards wrapping direct requestFocus() calls (commit 3fffc69).
arvioDpadFocusGroup now keeps the safe, default focusRestorer() (which
only saves/restores Compose's own focus history and never calls a
caller-supplied requester) and, when a restore target is requested,
drives its focus itself on focus enter via onFocusChanged with a
runCatching guard. Behaviour for all other callers is unchanged.
Co-authored-by: openhands <openhands@all-hands.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes an
IllegalStateException: FocusRequester is not initializedcrash that occurs on a fresh TV entry into Live TV while categories are still loading.This is a pre-existing issue in shared TV focus code (
ArvioDpadFocus.kt, originally authored April/Mai 2026), not introduced by any Stalker/portal work. It was reported separately and kept out of the Stalker live-config PR (#607) per Prodigy's request, so this PR contains only the two focus fixes and nothing else.Symptom
On the first D-pad navigation into Live TV right after categories finish loading, the app crashes:
Reproducible on a real TV (D-pad) when entering Live TV while the category sidebar is still populating.
Root cause
Modifier.focusRestorer { restoreFocusRequester }(used byCategorySidebarviaarvioDpadFocusGroup) captures the caller-suppliedFocusRequesterat modifier-application time. When a focus enter event's automatic restore fails (the common case on the very first entry, where there is no saved focus history), Compose'sFocusRestorerNodecallsfallback.requestFocus()itself, deep inside the modifier, with no try/catch. The fallback is theselectedCategoryFocusRequester, which is only bound to the LazyColumn row whereselectedId == cat.id. Right aftercategoriesLoadedflips false→true, that row is often not yet composed/attached →requestFocus()throws → crash.This internal
requestFocus()lives in Compose code, so it is unreachable by therunCatchingguards that wrap directrequestFocus()calls in app code.Fixes (2 commits)
1.
fix(tv): guard focus requests against uninitialized FocusRequesterWraps the remaining unguarded direct
FocusRequester.requestFocus()calls inrunCatching { ... }, consistent with the pattern the Live TV package already followed. Affected files: overlays/menus (MediaContextMenu,ContextMenu,QuickActionMenu,StreamSelector,AppUpdateModal,TextInputModal) andSearchScreen. This is defensive hardening — it does not by itself fix the crash above, but it removes other latent crash paths on the same theme.2.
fix(tv): guard focusRestorer fallback against uninitialized FocusRequester(the actual root fix)Reworks
arvioDpadFocusGroupso Compose's unguarded internalfallback.requestFocus()path is never taken:Modifier.focusRestorer()(which only saves/restores Compose's own focus history and never calls a caller-supplied requester).restoreFocusRequesteris requested, drives its focus itself on focus enter viaonFocusChanged { if (state.hasFocus) runCatching { restoreFocusRequester.requestFocus() } }— i.e. in app code, where therunCatchingguard catches an uninitialized requester instead of crashing.restoreFocusRequester, orenableFocusRestorer = false) is unchanged.Verification
runCatchingguards alone did not fix it; only thefocusRestorerrework did.):app:testSideloadDebugUnitTest) pass locally with no regressions. Focus-restorer behavior itself is Compose UI/integration runtime and not unit-testable.Test request
As discussed: please test Live TV entry, category loading and guide navigation. Happy to adjust if any focus-restore behavior looks off.
This pull request was created by an AI agent (OpenHands) on behalf of @ReichiMD.