Follow-up to TV_PLAN.md (architecture / phasing) and UI_UX_PLAN.md (mobile
audit). Scope: grow the shipping :tv app with new capability and 10-foot /
D-pad polish, reusing :core. Generated 2026-07-02 from a full read of the
current :tv + :core + :mobile sources.
Status legend: [ ] pending · [~] in progress · [x] done
Target audience decision: many target boxes are rooted → privileged install / manage (A2, A3) is weighted high.
- 🐛
ReceiveScreen.ktcollectsinstallResultbut never renders it — after an install the user gets no in-app success/failure feedback (only the system package-installer dialog). → A1. ⚠️ Manage detail pane (ManageScreen.kt) is aColumn().verticalScroll()with theInfoBlocks (storage/type/status) placed below the focusable actions; non-focusable content below the last focusable element is unreachable by D-pad. → B1.- No install progress while a large APK/bundle is written to the session — only a static "Installing…" label. → B2.
- TV only uses the non-privileged
PackageInstallersession;:mobile's Root (RootInstallController) / Shizuku (ShizukuInstallController) stack is unused on TV even though TV boxes are often rooted. → A2/A3. - No "Send to TV" QR scan from mobile (only browser upload); no URL downloader. → A4/A5.
- Theme has System/Light/Dark only; mobile has 5 accent presets. → B4.
- A1 — Install-result feedback (fix) (~0.5d) —
installResultis now a sealedInstallOutcome;InstallStatusOverlayrenders a bottom pill (Installing → Installed ✓ / Failed …) with Retry + Dismiss, success auto-dismisses after 3s, strings from resources. Received hero clears on success. - A2 — Privileged install backend on TV (Root) (~2d) — added Compose-free
RootShell(su -c, no libsu dep so the mobilestoreflavor stays libsu-free)RootInstaller(stage →pm install-create -r/install-write(+catpipe fallback)/install-commit) in:core.ReceiveViewModelprefers the silent root path when available + enabled, falls back to thePackageInstallersession. Settings gained a "Silent install (Root)" toggle (default on) + live root status; the result overlay shows "Installed silently ✓". (Shizuku path deferred — root covers the rooted-box target; Shizuku needs the rikka dep + reflection and is a smaller TV audience.)
- [~] A3 — Manage: privileged actions + multi-select (~1.5d) — single-app
privileged actions done: Force-stop · Enable/Disable · Clear-data · silent
Uninstall now render in the detail pane, gated on
uiState.rootAvailable, with destructive-confirm dialogs and a screen-level result pill (actionResult, which the VM already exposed but the UI never showed). Multi-select / batch uninstall still open (VM hasselectionMode/batchUninstall; no UI entry yet). - A4 — "Send to TV" QR scan from mobile (~1d) — mobile scans the TV QR → parses URL+token → multipart-uploads the chosen APK. Browser upload stays as fallback.
- A5 — Install from URL / link (~1d) — URL entry (or receive a URL from the
phone to avoid TV typing) →
DownloadManager→ into the install flow. - A6 — APK detail + permissions pre-install (~0.5d) — expand
ApkDetailsDialog: permissions list, minSdk-incompatibility warning, installed / update / downgrade badge.
- B1 — Focus fixes + default focus (~1d) — Manage detail-pane focus trap
fixed (metadata moved above the actions so the actions are the last focusable
block); first app row gets a one-shot
FocusRequesteron each Manage visit. (Cross-tab focus restoration still open — deferred to a later polish pass.) - B2 — Install progress bar (~0.5d) —
ApkInstaller.installgained anonProgress/totalBytescallback (manual buffered copy);ReceiveViewModelexposesinstallProgress: Float?; the A1 overlay shows a determinateLinearProgressIndicator(indeterminate when size is unknown). - B3 — Safe-area / overscan + 10-foot type (~0.5d) — whole
Type.ktramp lifted above the couch floor (added the missinglabelLargethat drove nav labels- buttons at 14sp, plus
titleSmall/bodySmall/headline*); nav-rail content inset past the ~5% overscan cut (rail widened to fit); stronger focus affordance on action rows (solid color-shift + 1.05 scale, destructive rows tintederror).
- buttons at 14sp, plus
- B4 — Accent color presets (~0.5d) — 5 mobile presets ported to TV via
ColorScheme.withAccent(reuses the sharedAppThemePreset), persisted to the sametheme_presetdataStore key (mobile↔TV parity); swatch picker in Settings. - [~] B5 — TV-friendly text entry (~0.5d) — Manage search field enlarged with a
strong filled/primary focus cue (was a color-only mobile
OutlinedTextField). Voice search / alpha quick-filter chips still open. - B6 — Empty/error states + Extract polish (~0.5d) — illustrated empty
states for Manage (empty + no-match w/ Clear-search) and Local files (empty +
scanning + Rescan/Receive actions); no-network state replaces the dead
0.0.0.0QR; Extract progress/result moved into the reachable status pill (was a focus trap below the actions). "Open Downloads" skipped — extract writes afile://DocumentFile and TV boxes lack a Downloads viewer. - B7 — Onboarding theme bridge + startup polish (this pass) — shared
:coreonboarding rendered in stock light-purple on TV (it reads mobile-material3); added a TV-onlyOnboardingThemeBridgeso it matches the orange/navy brand + honors dark mode. Single-instance splash (no double-fade, no replay on locale change), in-place locale switch viarecreate(), tab survives config change, LanguageScreenBackHandler.
Concrete, scoped follow-ups. Each is independent and ships on top of the current code.
- A3b — Multi-select / batch uninstall UI —
ManageViewModelalready has the whole engine (selectionMode,selectedPackages,toggleSelection,selectAllVisible,enterSelection/exitSelection,batchUninstall). Only the UI entry is missing. Add a "Select" action (or a long-press-free toggle) that flipsenterSelection(); render a checkbox/highlight perAppListRowdriven byselectedPackages; add a selection top-bar ("N selected" · Select all · Uninstall selected · Close) using the existingtv_manage_selected_count/tv_manage_select_all/tv_manage_batch_uninstall/tv_manage_selection_hintstrings; route Uninstall selected →batchUninstall()behind a destructiveConfirmDialog. Keep it D-pad friendly (no long-press). Files:presentation/manage/ManageScreen.kt. - B5b — Voice / quick text entry for Manage search — the field is already
enlarged with a strong focus cue; add a leanback voice-search affordance
(
RecognizerIntent.ACTION_RECOGNIZE_SPEECH→ feed result tosetSearchQuery) and/or alpha quick-filter chips, keeping the typed field as the secondary path. Files:presentation/manage/ManageScreen.kt(+ a mic launcher). - L2 — TV-native onboarding fork — the
OnboardingThemeBridgefixes the critical wrong-palette bug, but the shared:coreonboarding still uses mobileandroidx.compose.material3buttons + a forward-onlyHorizontalPager(weak focus at 3m, no initial focus, no D-pad Prev/Next). Build a TV-specific onboarding in:tvwithandroidx.tv.material3Surface/Button (built-in focus scale/border), an initialFocusRequesteron the primary CTA, explicit focusable Previous/Next, ≥18–24sp body, and ~5% overscan; routeMainActivity'sonboardingCompleted == falsebranch to it. Do NOT addandroidx.tv.material3to:coreand don't force-focus the shared screen — either would regress the mobile build. New files undertv/.../presentation/onboarding/; retires the need for the bridge.
A1 + B1 + B2 — fix real bugs + focus foundation.✅ done (:tv:assembleDebuggreen)A2 — privileged root backend (highest leverage on rooted boxes).✅ doneA3 (single-app) + B3 + B4 + B6 + B7 — privileged detail actions + 10-foot polish + onboarding/startup.✅ done (:tv:assembleDebuggreen)- A3 (multi-select) — batch uninstall UI on top of the existing VM support.
- A4 + A5 — send-to-TV + URL. (~2d)
- A6 + B5 (voice) — APK detail + TV-friendly input. (~1d)
Each item ships independently; keep :tv:assembleDebug green after each.