Modules already created: :core (android-library), :mobile (the existing app,
renamed from :app), :tv (Compose-for-TV app scaffold).
Goal: a TV (10-foot, D-pad) version of the installer that reuses the install/
manage engine via :core, without breaking the shipping :mobile app.
:core ← UI-agnostic engine: domain models + data/engine (no Compose, no Koin)
:mobile ← existing phone UI (keeps working throughout; migrates onto :core later)
:tv ← new 10-foot UI (tv-material, D-pad) on top of :core
:mobile is NOT migrated in early phases — that's a large, risky rewrite of 149
files. We grow :core incrementally and let :tv consume it now; :mobile
adopts :core piecemeal once the API has proven out on TV.
:core:domain.InstalledApp+AppRepository.getInstalledApps()(PackageManager query: label, system flag, size, enabled, installer — permission-free, no UsageStats).:tv: depend on:core; replace the template Greeting with a D-padManageScreen(TvLazyColumn of focusable app cards) + uninstall via the systemACTION_DELETEdialog (no privileged perm needed). Manifest: QUERY_ALL_PACKAGES + leanback.- Acceptance: launches on a TV/emulator, lists user apps, focus moves with D-pad, selecting an app → system uninstall flow → list refreshes.
Why revised. Probes on an Android TV (API 31) emulator showed the easy install inputs are all dead on TV:
- No activity handles the apk
VIEW/INSTALL_PACKAGEintent → can't open a system installer by intent. Butcom.google.android.packageinstalleris present, so a PackageInstaller session works (its confirm UI is launched via the session'sPENDING_USER_ACTIONintent, D-pad navigable). - SAF file picker is a no-op stub (
frameworkpackagestubs) → can't pick a file. - No "All files access" settings screen on TV → can't read arbitrary Download/USB files under scoped storage.
MANAGE_UNKNOWN_APP_SOURCESsettings andDownloadManagerare present.
So getting the APK onto the TV is the real problem. Chosen direction (user): the TV runs a receiver server + shows a QR; a phone uploads the APK over LAN; the TV installs it via a PackageInstaller session. Plus a best-effort local scan.
Components
:coreApkInstaller— PackageInstaller session install from a content/fileUri(write bytes → commit → handlePENDING_USER_ACTION→ final status), suspend/Flow result. The one proven install path on TV.ApkReceiverServer— NanoHTTPD (already a dep) listening on TV: serves a phone-friendly upload page atGET /and acceptsPOST /upload(multipart) → saves to TV cache → hands off toApkInstaller. Token/PIN auth (reuse the mobile sync server's cookie/PIN pattern). MirrorsApkHttpServerbut receives instead of serving.LanAddress+QrEncoder(QR bitmap from the receiver URL+token).DownloadsApkScanner— MediaStore query for APKs already in Downloads.
:tv- Receive screen: QR + IP + PIN ("scan to send an app"); live "receiving… / install?" state; D-pad friendly.
- Local screen: list APKs found in Downloads → session install.
- Nav shell (Install | Manage) built from proven
Surface/Card(NOT tv-material3TabRow— same alpha cohort that crashed via tv-foundation; avoid the gamble). - Manifest:
REQUEST_INSTALL_PACKAGES,INTERNET, foreground-service for the server if it must outlive the screen.
:mobile(later sub-phase)- "Send to TV": QR scanner (camera) → parse TV URL/token → multipart upload the selected APK to the TV. (Any phone browser can already upload via the web page, so this is an enhancement, not a blocker.)
Sub-phases
- 2a: TV receiver server + web upload page + QR + session install (verify with any
phone browser; no
:mobilechange). - 2b: TV local Downloads scan → install.
- 2c:
:mobileQR-scan + "Send to TV" integration.
- Reuse
:mobile's sync HTTP-server idea in:coreso a phone can push an APK to the TV (text entry on TV is painful — this sidesteps it). Optional URL download.
- Move Shizuku / Root (libsu) install paths into
:core; expose in:tv. Many TV boxes are rooted, so this is high-value there.
- Replace
:mobile's duplicated engine pieces with:corecalls, module by module (models → repository → installers), keeping the app green at each step. End state: one engine, two UIs.
- TV input: no file-manager VIEW/SEND intents like phone — entry is USB browse, network push, or downloader (Phases 2–3).
- Text entry on TV is clunky → minimize text fields.
:corestays Compose-free and Koin-free so both UIs (and tests) can use it plainly.