Preserve Home Back viewport and recover offline person switches #84

Merged
justinzeus merged 2 commits from fm/tv-r2-walkfix into fm/tv-round2 2026-11-08 19:20:29 +01:00
Owner

Outcome

Fix the two actionable walk findings: Home returns from a title at its exact previous viewport; an offline person switch explains the failure and leaves a working Back exit without choosing another person.

Tier B/C: walk-fix; the Back/identity part needs the delta safety check before merge

Base: fm/tv-round2, candidate c21d17dfdfde1c6d8c86c13fe97e48183d2353f1. No merge, release or build/install gate is implied.

Home viewport: cause, reproduction, fix

Baseline wolfskin-design/.../ShellSurface.kt:99-101 restores native focus inside the content group but inherits Foundation's TV scrolling policy. HomeScreen.kt:62-72 correctly skips focus-follow when the remembered positioned key is unchanged; that preserves pointer-scrolled viewports but cannot stop a second, implicit scroll owner.

Leanback TVs use a 30% bring-into-view pivot. The card is already fully visible at (84dp pinned line + 42dp heading band) × 2 = 252px; the platform target is 1080px × 0.3 = 324px. That explains the 72px displacement without changing the selected card.

The missing native-test preconditions were TV capabilities and keyboard input: the resource qualifier alone does not advertise FEATURE_LEANBACK, and Compose key injection does not leave Robolectric's touch mode. The new system-animation-scale-zero case models the walk's animations-off case, separately from the app's reduced-motion preference. No Home data, fixture asset, list offset or focus-state fixture was patched.

TitleEntryBackTest.systemAnimationsOffHomeTitleBackKeepsTheExactPreOpenViewport failed before the production fix on SDK 34 and 36:

Native measurement Before opening title After Back, unfixed After Back, fixed
Focused card top, px 252 324 252
First visible item index 0 0 Same as pre-open
Scroll offset, px 652 580 Same as pre-open

The test runs production MainContent, SignedInContent, WolfskinApp, HomeDestination/HomeScreen and TitleDestination/TitleSurface, with navigation-owned ViewModels and real NavDisplay decorators. It sends real keys, native Back down/up and Activity Back completion—not a direct navigation pop. It checks exact card bounds and list state at 64ms and after settling. The normal-animation, reduced-motion, deep-row, Shared and initial-loading cases also pass after the fix; normal-scale native failures before the fix displaced the card by 25px, not the complete device endpoint.

ShellSurface.kt:107-114 now supplies Foundation's ordinary minimum-visibility BringIntoViewSpec implementation to shell-key-owned screens. Already-visible cards are not repivoted. Home's positioned-key guard and saved viewport remain unchanged. One singleton policy, no per-screen correction or animation-delay workaround.

Offline person switch: cause and behavior

Baseline app/src/wolfskin/.../WolfskinApp.kt:276-285 fell back to setup without a message. SetupNavigationManager.kt:27-29 replaces the setup root; it does not retain an old signed-in entry underneath. Baseline WolfskinSetupContent.kt:82-85 sent root-picker Back to the same picker, and ignored the shell Exit action.

  • Ordinary transport/server failure restores the picker on the original authenticated person, displays “The household server didn’t answer,” and reattaches only that person's household client. Back dismisses a reopened picker to navigation; the following Back exits. A launch picker exits immediately.
  • Setup root/loading/busy picker Back exits the Activity instead of exposing an old signed-in page. Sign-in/error/settings steps return to the picker first; PIN dismissal remains first.
  • HTTP 401/403 preserves the authentication-gate route. PIN, protected and require-login selection rules stay unchanged.
  • A ten-second commit deadline cancels an unanswered attempt and provides a usable setup exit and message; it never selects a replacement person.
  • Baseline ServerRepository.kt:107 installed attempted credentials before validating the target. Failed/cancelled switches now restore the original API token as well as preserving the selected identity. The transaction is serialized so a late older rollback cannot overwrite a newer authenticated token.

Failing-before → passing-after evidence

OfflinePersonSwitchTest originally failed these five real-screen cases on both SDK 34 and 36; all now pass:

  • failedAvatarSwitchBackDismissesToOriginalNavThenExits
  • failedAvatarSwitchExplainsThatTheServerDidNotAnswer
  • failedLaunchPickerSelectionBackExitsWithoutChangingIdentity
  • coldSetupPickerBackExitsWithoutRestoringTheSignedInEntry
  • unansweredSwitchCannotKeepInputCoveredIndefinitely

Additional regressions caught and now pass: failedAvatarSwitchRestoresOnlyTheOriginalHouseholdSession, expiredTargetCredentialsStillOpenTheSignInGate; real-Room ServerSavedSessionTest.failedPersonSwitchPreservesTheAuthenticatedTokenAndSavedIdentity, cancelledPersonSwitchPreservesTheAuthenticatedTokenAndSavedIdentity, and cancelledOlderSwitchCannotReplaceTheNewerAuthenticatedToken (expected newer-token, originally observed saved-token). Setup Escape recovery also passes. The obsolete mock-only setup-fallback assertion was removed, not repinned.

Universal application / places checked

Place checked Decision
Home, Library and See all, Discover/subpages, Settings/Updates Same shell-owned viewport policy at the shared ShellSurface; inspected route dispatch and each manual scroller. Related Home, Library, Discover and root-Back classes exercised.
Title/person surfaces; legacy content handoffs handlesContentKeys=false retains inherited policy; inspected each caller. Existing title/person Back regressions pass.
Legacy Home/collection/Discover grids Existing explicit bring-into-view policies retained; no second convention added there.
Full-screen playback/external content; request overlay Bypass the changed content provider; untouched.
Launch picker, avatar picker, setup picker/error/login, session-recovery setup route Shared recovery/Back semantics; native launch/reopened/setup/error/timeout scenarios exercised.
Saved-user switch, saved launch restore, password-login overload, legacy SwitchUserViewModel and locale refresh All delegate to the same serialized repository credential transaction; no caller-specific rollback shim.
ShellAction.Person construction/handling Carries launch provenance through the sole production constructor; all callers migrated, no compatibility alias.

Verification and limits

Final focused docker/gradle.sh gate: 108 app tests + 58 design tests; zero failures, errors or skips. Reproduction and verification use native Robolectric graphics, not a patched UI fixture. Home and offline-picker captures were inspected. Exact viewport/card bounds are verified. Whole-image pixel identity is not claimed: the focused card's ratings overlay differs between native captures.

Proof directory: /root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-r2-walkfix/:

  • home-native-before-results/ and home-system-animations-off-before.log: immutable exact-72px failures and original test driver.
  • offline-before-results/, household-before.log, auth-gate-before-results/, token-race-before-results/: failing-before recovery/authority evidence.
  • combined-native-after-policy.log, combined-app-after-results/, combined-design-after-results/, home-native-after-captures/, offline-switch-final-sdk*.png: passing-after evidence.
  • focused-gate.sh: exact batched class filter; both Gradle test tasks are explicitly filtered.

One earlier Home probe inadvertently ran the unfiltered design dependency suite because the app test task depends on it. Subsequent gates explicitly filtered both tasks. No full app suite, APK build, emulator/device/ADB access, install, release or merge was performed. Real TV/Android 9 behavior and the delta identity/Back safety walk remain unverified and required before merge. Findings F3–F6 and review minors are out of scope and unchanged; no claim is made about title-load polish, episode focus or trailer-origin restoration.

## Outcome Fix the two actionable walk findings: Home returns from a title at its exact previous viewport; an offline person switch explains the failure and leaves a working Back exit without choosing another person. **Tier B/C: walk-fix; the Back/identity part needs the delta safety check before merge** Base: `fm/tv-round2`, candidate `c21d17dfdfde1c6d8c86c13fe97e48183d2353f1`. No merge, release or build/install gate is implied. ## Home viewport: cause, reproduction, fix Baseline `wolfskin-design/.../ShellSurface.kt:99-101` restores native focus inside the content group but inherits Foundation's TV scrolling policy. `HomeScreen.kt:62-72` correctly skips focus-follow when the remembered positioned key is unchanged; that preserves pointer-scrolled viewports but cannot stop a second, implicit scroll owner. Leanback TVs use a 30% bring-into-view pivot. The card is already fully visible at `(84dp pinned line + 42dp heading band) × 2 = 252px`; the platform target is `1080px × 0.3 = 324px`. That explains the 72px displacement without changing the selected card. The missing native-test preconditions were TV capabilities and keyboard input: the resource qualifier alone does not advertise `FEATURE_LEANBACK`, and Compose key injection does not leave Robolectric's touch mode. The new system-animation-scale-zero case models the walk's animations-off case, separately from the app's reduced-motion preference. No Home data, fixture asset, list offset or focus-state fixture was patched. `TitleEntryBackTest.systemAnimationsOffHomeTitleBackKeepsTheExactPreOpenViewport` failed **before** the production fix on SDK 34 and 36: | Native measurement | Before opening title | After Back, unfixed | After Back, fixed | |---|---:|---:|---:| | Focused card top, px | 252 | 324 | 252 | | First visible item index | 0 | 0 | Same as pre-open | | Scroll offset, px | 652 | 580 | Same as pre-open | The test runs production MainContent, SignedInContent, WolfskinApp, HomeDestination/HomeScreen and TitleDestination/TitleSurface, with navigation-owned ViewModels and real NavDisplay decorators. It sends real keys, native Back down/up and Activity Back completion—not a direct navigation pop. It checks exact card bounds and list state at 64ms and after settling. The normal-animation, reduced-motion, deep-row, Shared and initial-loading cases also pass after the fix; normal-scale native failures before the fix displaced the card by 25px, not the complete device endpoint. `ShellSurface.kt:107-114` now supplies Foundation's ordinary minimum-visibility [BringIntoViewSpec implementation](https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec) to shell-key-owned screens. Already-visible cards are not repivoted. Home's positioned-key guard and saved viewport remain unchanged. One singleton policy, no per-screen correction or animation-delay workaround. ## Offline person switch: cause and behavior Baseline `app/src/wolfskin/.../WolfskinApp.kt:276-285` fell back to setup without a message. `SetupNavigationManager.kt:27-29` **replaces** the setup root; it does not retain an old signed-in entry underneath. Baseline `WolfskinSetupContent.kt:82-85` sent root-picker Back to the same picker, and ignored the shell Exit action. - Ordinary transport/server failure restores the picker on the original authenticated person, displays “The household server didn’t answer,” and reattaches only that person's household client. Back dismisses a reopened picker to navigation; the following Back exits. A launch picker exits immediately. - Setup root/loading/busy picker Back exits the Activity instead of exposing an old signed-in page. Sign-in/error/settings steps return to the picker first; PIN dismissal remains first. - HTTP 401/403 preserves the authentication-gate route. PIN, protected and require-login selection rules stay unchanged. - A ten-second commit deadline cancels an unanswered attempt and provides a usable setup exit and message; it never selects a replacement person. - Baseline `ServerRepository.kt:107` installed attempted credentials before validating the target. Failed/cancelled switches now restore the original API token as well as preserving the selected identity. The transaction is serialized so a late older rollback cannot overwrite a newer authenticated token. ### Failing-before → passing-after evidence `OfflinePersonSwitchTest` originally failed these five real-screen cases on **both SDK 34 and 36**; all now pass: - `failedAvatarSwitchBackDismissesToOriginalNavThenExits` - `failedAvatarSwitchExplainsThatTheServerDidNotAnswer` - `failedLaunchPickerSelectionBackExitsWithoutChangingIdentity` - `coldSetupPickerBackExitsWithoutRestoringTheSignedInEntry` - `unansweredSwitchCannotKeepInputCoveredIndefinitely` Additional regressions caught and now pass: `failedAvatarSwitchRestoresOnlyTheOriginalHouseholdSession`, `expiredTargetCredentialsStillOpenTheSignInGate`; real-Room `ServerSavedSessionTest.failedPersonSwitchPreservesTheAuthenticatedTokenAndSavedIdentity`, `cancelledPersonSwitchPreservesTheAuthenticatedTokenAndSavedIdentity`, and `cancelledOlderSwitchCannotReplaceTheNewerAuthenticatedToken` (expected `newer-token`, originally observed `saved-token`). Setup Escape recovery also passes. The obsolete mock-only setup-fallback assertion was removed, not repinned. ## Universal application / places checked | Place checked | Decision | |---|---| | Home, Library and See all, Discover/subpages, Settings/Updates | Same shell-owned viewport policy at the shared ShellSurface; inspected route dispatch and each manual scroller. Related Home, Library, Discover and root-Back classes exercised. | | Title/person surfaces; legacy content handoffs | `handlesContentKeys=false` retains inherited policy; inspected each caller. Existing title/person Back regressions pass. | | Legacy Home/collection/Discover grids | Existing explicit bring-into-view policies retained; no second convention added there. | | Full-screen playback/external content; request overlay | Bypass the changed content provider; untouched. | | Launch picker, avatar picker, setup picker/error/login, session-recovery setup route | Shared recovery/Back semantics; native launch/reopened/setup/error/timeout scenarios exercised. | | Saved-user switch, saved launch restore, password-login overload, legacy SwitchUserViewModel and locale refresh | All delegate to the same serialized repository credential transaction; no caller-specific rollback shim. | | ShellAction.Person construction/handling | Carries launch provenance through the sole production constructor; all callers migrated, no compatibility alias. | ## Verification and limits Final focused `docker/gradle.sh` gate: **108 app tests + 58 design tests; zero failures, errors or skips**. Reproduction and verification use native Robolectric graphics, not a patched UI fixture. Home and offline-picker captures were inspected. Exact viewport/card bounds are verified. Whole-image pixel identity is not claimed: the focused card's ratings overlay differs between native captures. Proof directory: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-r2-walkfix/`: - `home-native-before-results/` and `home-system-animations-off-before.log`: immutable exact-72px failures and original test driver. - `offline-before-results/`, `household-before.log`, `auth-gate-before-results/`, `token-race-before-results/`: failing-before recovery/authority evidence. - `combined-native-after-policy.log`, `combined-app-after-results/`, `combined-design-after-results/`, `home-native-after-captures/`, `offline-switch-final-sdk*.png`: passing-after evidence. - `focused-gate.sh`: exact batched class filter; both Gradle test tasks are explicitly filtered. One earlier Home probe inadvertently ran the unfiltered design dependency suite because the app test task depends on it. Subsequent gates explicitly filtered both tasks. No full app suite, APK build, emulator/device/ADB access, install, release or merge was performed. Real TV/Android 9 behavior and the delta identity/Back safety walk remain unverified and required before merge. Findings F3–F6 and review minors are out of scope and unchanged; no claim is made about title-load polish, episode focus or trailer-origin restoration.
justinzeus added 2 commits 2026-11-08 18:42:27 +01:00
Preserve TV Back viewports and person-switch authority
All checks were successful
review/delta-opus Tier C delta review: 0 blockers; 4 non-blocking notes (N1-N4)
gate/round2-candidate Full suite 1308/1308 and Opt 1.1.2/71 checks on this exact head (data/tv-r2-gate5/result.json)
72849f0734
justinzeus merged commit 017ad0268e into fm/tv-round2 2026-11-08 19:20:29 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#84
No description provided.