From d6042ab2e22b1cbe498559f5755f585628c923d0 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Thu, 22 Oct 2026 23:20:47 -0500 Subject: [PATCH 1/3] Docs: drop historical desktop and repo docs, keep current state only Delete the cross-project shutdown index, the mobile-main integration receipt, the ADB quit hardening follow-on, the original Android app plan and the Logs tab design note. Their still-true facts move into v2/HANDOFF.md (quit and Logs rules, pairing, adb cwd and launcher rationale) and the mobile handoff. Rename the release decision packet to docs/DEVICE-TEST-SCRIPT.md and keep only the physical test script. Rewrite v2/HANDOFF.md, the parity table and the remote input notes in the present tense, drop shipped roadmap items, and bring CLAUDE.md's in-flight section up to date. docs/ARCHIVE-INDEX.md now lists every removed doc with the commit that last held it. --- CLAUDE.md | 34 +-- docs/ARCHIVE-INDEX.md | 41 +++- docs/DEVICE-LOGS-2026-10-08.md | 28 --- docs/DEVICE-TEST-SCRIPT.md | 93 ++++++++ docs/FEATURES.md | 43 ++-- docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md | 27 --- docs/RELEASE-DECISION-2026-09-09.md | 253 --------------------- docs/WHERE-EVERYTHING-IS.md | 90 -------- v2/ADB-QUIT-HARDENING.md | 37 --- v2/ATV-OPTIMIZER-ANDROID-PLAN.md | 68 ------ v2/ATVTOOLS-PARITY.md | 62 +---- v2/HANDOFF.md | 157 +++++++------ v2/README.md | 2 +- v2/REMOTE-LATENCY-PLAN.md | 190 ++++++---------- v2/screenshots/README.md | 2 +- v2/src/fonts/README.md | 9 +- 16 files changed, 340 insertions(+), 796 deletions(-) delete mode 100644 docs/DEVICE-LOGS-2026-10-08.md create mode 100644 docs/DEVICE-TEST-SCRIPT.md delete mode 100644 docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md delete mode 100644 docs/RELEASE-DECISION-2026-09-09.md delete mode 100644 docs/WHERE-EVERYTHING-IS.md delete mode 100644 v2/ADB-QUIT-HARDENING.md delete mode 100644 v2/ATV-OPTIMIZER-ANDROID-PLAN.md diff --git a/CLAUDE.md b/CLAUDE.md index 7962df73..3bb95cdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Two products live in this tree: -- **v1** — PowerShell debloater (`Shield-Optimizer.ps1` at the root). Released by running `release.sh` at the repo root, which tags the commit and calls `gh release create` directly. No release workflow — only `tests.yml` (CI tests). **Legacy** since 2026-10-22: serious bug fixes only, no new features. User docs live in `docs/LEGACY-V1.md`, not the README. +- **v1** — PowerShell debloater (`Shield-Optimizer.ps1` at the root). Released by running `release.sh` at the repo root, which tags the commit and calls `gh release create` directly. No release workflow — only `tests.yml` (CI tests). **Legacy**: serious bug fixes only, no new features. User docs live in `docs/LEGACY-V1.md`, not the README. - **v2** — Tauri 2 + Rust + Svelte 5 desktop app (`v2/`), the official product, shipped as **ATV Optimizer**. Released on `v2-*` tags via `.github/workflows/v2-release.yml`. The root `README.md` is the user-facing page for it; `v2/README.md` is the developer guide. The two release tracks are intentionally separate. Don't mix tag namespaces and don't edit the v2 workflow when shipping v1 (or vice versa). @@ -13,27 +13,27 @@ Before touching v2, skim `v2/HANDOFF.md` — it carries the current roadmap, the - **The backlog is GitHub issues labeled [`gastown`](https://github.com/bryanroscoe/shield_optimizer/issues?q=is%3Aissue+is%3Aopen+label%3Agastown)** in `bryanroscoe/shield_optimizer`. Each is titled `[] ` and carries the original - description, acceptance criteria and dependencies. Issues without that label are user reports; - some of them duplicate a labeled issue (`#88`, `#89`, `#91` are the known pairs). -- **[`docs/RELEASE-DECISION-2026-09-09.md`](docs/RELEASE-DECISION-2026-09-09.md)** has the release - plan and the step-by-step physical device test script. Read it before tagging anything. + description, acceptance criteria and dependencies. Issues without that label are user reports. +- **[`docs/DEVICE-TEST-SCRIPT.md`](docs/DEVICE-TEST-SCRIPT.md)** is the step-by-step physical + device test script. Run it before tagging a v2 release. - **[`docs/gastown-shield_optimizer-beads-export.json`](docs/gastown-shield_optimizer-beads-export.json)** is the full historical task archive including closed items — useful for "why was this done this way", not a live queue. - **[`docs/ARCHIVE-INDEX.md`](docs/ARCHIVE-INDEX.md)** points at bulk working material kept outside - this repo on Bryan's machine. + this repo on Bryan's machine, and at the historical docs removed from the repo (each with the + commit that last held it). -### What is in flight right now (2026-10-04) +### What is in flight right now -`v2-2.3.0` shipped 2026-09-30 (the ATV Optimizer rename plus #88, #119–#124). `v2-2.3.1` is -prepared on `release-prep-2.3.1`: the Google TV Setup Wraith fixes (#122, #157, #158), Report this -app (#100), app storage details (#98), restart after updating and patch notes on demand, the -right-click menu (#129), and the #97/#99/#135/#138/#139/#143/#147 fixes. It waits on the owner's -go before `v2/release.sh --set 2.3.1`. +`v2-2.3.0` (2026-09-30) is the latest release. `main` carries a prepared but untagged `v2-2.3.1` +section in `v2/CHANGELOG.md` (the Google TV Setup Wraith fixes #122/#157/#158, Report this app +#100, app storage details #98, restart after updating, patch notes on demand, the right-click menu +#129, and the #97/#99/#135/#138/#139/#143/#147 fixes) and an `Unreleased` section above it (the +Logs tab, Stop ADB when quitting, more fixes). The next tag waits on the owner's go. -Still open and not code work: #122 needs a reporter on Google TV to confirm the Setup Wraith fix -once 2.3.1 ships; #123 needs Glance's package id from its reporter; #93 and #105 need a real -phone and TV. +Still open and not code work: #122 is closed on the fix, but no Google TV reporter can confirm it +until that release ships; #123 needs Glance's package id from its reporter; #93 and #105 need a +real phone and TV. The mobile companion app (`v2/mobile/`) has its own authoritative handoff at [`v2/mobile/HANDOFF.md`](v2/mobile/HANDOFF.md). Its safety story depends on never claiming more @@ -90,7 +90,7 @@ The Linux runner needs `libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana The README walkthrough (`v2/screenshots/gallery.gif`) is generated, not hand-captured. **When you change v2 UI in a way that alters any captured screen, regenerate it and commit the result** — don't let the gallery drift from the real app: ``` -cd v2 && npm run screenshots # captures 15 screens in both themes and rebuilds both GIFs +cd v2 && npm run screenshots # captures 16 screens in both themes and rebuilds both GIFs ``` It runs offline against the demo fixture layer (`src/lib/demo-mock.ts`, gated behind `VITE_DEMO=1`) — no device needed. Most changes (CSS, layout, copy, rows) flow through with no tooling edits. Two cases need a touch-up: @@ -151,7 +151,7 @@ The macOS distribution channel is a Homebrew tap at [`bryanroscoe/homebrew-shiel repo-relative path (`v2/evidence/…`): in a PR it resolves under `/pull/<n>/` and 404s. Branch links break when the branch is deleted. Committing evidence to the repo is optional; attaching it is not. - **Roadmap markers**: closing a roadmap or parity item updates its status in the same commit - (`v2/ATVTOOLS-PARITY.md`, `v2/ATV-OPTIMIZER-ANDROID-PLAN.md`, `v2/mobile/BACKLOG.md`). Docs that + (`v2/ATVTOOLS-PARITY.md`, `v2/mobile/BACKLOG.md`). Docs that claim shipped work is unbuilt send the next reader to rebuild it — that has happened here. - **Comments**: only when the *why* is non-obvious. Don't add docstrings/comments to code you didn't change. No banner / section-divider comments. - **Spelling**: the company is "Truemed" — silently correct other casings (TrueMed, TRUEMED, truemed) in writing, except in verbatim quotes, URLs, and code identifiers. diff --git a/docs/ARCHIVE-INDEX.md b/docs/ARCHIVE-INDEX.md index 4e2c3c1b..1b5de6d3 100644 --- a/docs/ARCHIVE-INDEX.md +++ b/docs/ARCHIVE-INDEX.md @@ -1,4 +1,36 @@ -# Archive index — material kept outside this repository +# Archive index + +Two kinds of material live outside the current tree: historical docs that were removed from the +repository (recoverable from git history, listed first), and bulk working material copied to a +local directory that is not in git at all. + +## Docs removed from the repository + +Each of these described a moment that has passed: a dated audit, a feedback log, a completed +plan, a merge receipt, or a decision that is now simply how the code works. Their still-true facts +were moved into the live docs named in parentheses. Read any of them with the `git show` command +given. + +| Path | What it was | Last version | +|---|---|---| +| `docs/RELEASE-DECISION-2026-09-09.md` | The 2026-09-09 release decision packet: candidate inventory, commit plan, draft 2.2.0-beta changelog and CI proposals, plus the physical test script (the script is now `docs/DEVICE-TEST-SCRIPT.md`) | `git show 74c659f:docs/RELEASE-DECISION-2026-09-09.md` | +| `docs/WHERE-EVERYTHING-IS.md` | Cross-project index written when the Gas Town coordination workspace was shut down (other projects, on-disk archives, state at shutdown) | `git show a015c8a:docs/WHERE-EVERYTHING-IS.md` | +| `docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md` | Merge and validation receipt for integrating `main` into the mobile parity branch (PR #201) | `git show 0c78c06:docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md` | +| `docs/DEVICE-LOGS-2026-10-08.md` | Design note for the desktop Logs tab (#162) (now `v2/HANDOFF.md`) | `git show ab8148b:docs/DEVICE-LOGS-2026-10-08.md` | +| `v2/ADB-QUIT-HARDENING.md` | Follow-on to PR #168 (Stop ADB when quitting): installer lease regression scope, the closed draft PRs #174–179, pinned Tauri exit limits (now `v2/HANDOFF.md`) | `git show ae0436e:v2/ADB-QUIT-HARDENING.md` | +| `v2/ATV-OPTIMIZER-ANDROID-PLAN.md` | Original M0–M6 plan for the Android app, written before the transport change (now `v2/mobile/HANDOFF.md`, `v2/mobile/BACKLOG.md`, `v2/mobile/BILLING-COMMAND-MATRIX.md`) | `git show f4e2513:v2/ATV-OPTIMIZER-ANDROID-PLAN.md` | +| `v2/mobile/ARCHITECTURE-REVIEW.md` | July 2026 mobile architecture review (F/T/B findings, all since addressed) (now `v2/mobile/HANDOFF.md`) | `git show 660ffb9:v2/mobile/ARCHITECTURE-REVIEW.md` | +| `v2/mobile/FEATURES.md` | July 2026 mobile screen ↔ command status matrix | `git show 4e054c1:v2/mobile/FEATURES.md` | +| `v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md` | Nine-finding mobile correctness audit and its fix pass, all fixed | `git show 0be7312:v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md` | +| `v2/mobile/FEEDBACK-UX-2026-09-07.md` | Pre-implementation UX brief for saved-TV identity, Unknown risk, app details and copy (now `v2/mobile/HANDOFF.md`) | `git show a015c8a:v2/mobile/FEEDBACK-UX-2026-09-07.md` | +| `v2/mobile/USER-FEEDBACK-2026-09-05.md` | First phone feedback list; items 1–6 shipped, item 7 became #105 (now `v2/mobile/LIFECYCLE-EVIDENCE.md`) | `git show a015c8a:v2/mobile/USER-FEEDBACK-2026-09-05.md` | +| `v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md` | Pre-implementation contract for the unknown-app diagnostics collector, since shipped | `git show 0be7312:v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md` | +| `v2/mobile/TRANSPORT-LICENSING-RESEARCH.md` | Research behind replacing the GPLv3 libadb-android transport with MIT `adb_client` (now `v2/mobile/HANDOFF.md` §2) | `git show a015c8a:v2/mobile/TRANSPORT-LICENSING-RESEARCH.md` | +| `v2/mobile/DEPENDENCY-REMEDIATION-20261016.md` | Receipt for the 2026-10-16 npm dependency audit and pruning (the desktop advisory is now in `v2/HANDOFF.md`) | `git show 1c3f2b1:v2/mobile/DEPENDENCY-REMEDIATION-20261016.md` | +| `v2/mobile/RELEASE-POLISH.md` | Mobile release-polish checklist with per-PR merge status (open items now in `v2/mobile/BACKLOG.md`) | `git show a9f07d7:v2/mobile/RELEASE-POLISH.md` | +| `v2/mobile/RELEASE-POLISH-RECEIPTS.md` | Per-PR review/CI/merge receipts for PRs #181–#204 and #206 | `git show a9f07d7:v2/mobile/RELEASE-POLISH-RECEIPTS.md` | + +## Bulk material outside the repository Some working material from the retired coordination tree was too large, too build-output-heavy, or too internal to belong in the repository, but is worth keeping. It was **copied** (never moved) to a @@ -16,7 +48,7 @@ last resort when you need to know *why* a merged change looks the way it does. (665 entries, manifest sha256 `5155bf2996e453fca39ff5bc7f9985d01c9a85ceaf9582acccc4b63fcc00967b`). Verify with `cd <archive> && shasum -a 256 -c MANIFEST-sha256.txt`. -## What is in it +### What is in it - **`mayor-artifacts/`** — per-task briefs, acceptance contracts, source manifests and patches for the changes that were merged into `main` in September 2026. Useful if you need the acceptance @@ -25,8 +57,7 @@ Verify with `cd <archive> && shasum -a 256 -c MANIFEST-sha256.txt`. screenshots and applied patches. Both are heavy with internal process vocabulary from the retired tracker. The durable, readable -versions of this material are already in the repo: -[`RELEASE-DECISION-2026-09-09.md`](RELEASE-DECISION-2026-09-09.md), +versions of this material are the release decision packet listed above, [`gastown-shield_optimizer-beads-export.json`](gastown-shield_optimizer-beads-export.json) and [`../v2/mobile/evidence/`](../v2/mobile/evidence/). @@ -74,7 +105,7 @@ versions of this material are already in the repo: | `mayor-artifacts/shield-so-x02-launcher-correction` | 384K | 26 files | | `mayor-artifacts/shield-so-x02-launcher-failures` | 296K | 19 files | -## Deliberately excluded from the copy +### Deliberately excluded from the copy - Cargo `target/` trees and `node_modules/` (build output and dependency caches; ~26 MB, the bulk of it one `rust-fixture` build directory). diff --git a/docs/DEVICE-LOGS-2026-10-08.md b/docs/DEVICE-LOGS-2026-10-08.md deleted file mode 100644 index 8bd3f25b..00000000 --- a/docs/DEVICE-LOGS-2026-10-08.md +++ /dev/null @@ -1,28 +0,0 @@ -# Desktop device logs - -The Logs tab addresses #162 with finite, read-only logcat snapshots. It is not a lossless -stream or a persistent recording. The most recent snapshot replaces the preceding one; -entries can repeat between reads and high-volume traffic can roll out of the selected window. - -- Reads start only on an explicit Read logs or Auto-refresh click. -- Severity, one exact tag and an optional Android package constrain the device-side read. - Package filtering resolves the running main PID for each snapshot. Missing, ambiguous or - unsupported PID resolution fails without requesting unfiltered logs. Helper processes are - excluded; Android PID reuse means historical entries can belong to an earlier process. -- Every shell invocation uses the existing bounded driver: 30 seconds and 256 KiB per stream. - App filtering adds a separate bounded PID lookup. Only one read is pending per view. -- Auto-refresh waits three seconds after a completed read. It stops on error, tab departure, - device-page reset, document hiding or explicit Stop. A native read already in flight may - finish within its bounds, but its late result is discarded. Returning does not resume it. -- A tag cannot start with a hyphen or contain shell/filter syntax. The typed command exposes - no log-clearing, buffer resizing, system-setting or arbitrary-shell option. -- Raw text is escaped on screen. Find filters only the displayed snapshot. This feature does - not save, upload or append logs to bug reports; the shared bounded driver redacts payloads - from its own debug/session records. Review sensitive text before manually sharing it. -- Timeout/output-cap results are labeled partial. Android versions may reject logcat flags; - device errors remain visible rather than falling back to broader capture. - -Validation covers pure filter/command construction, mocked driver failures and PID scope, -polling lifecycle, and browser interaction. Those checks do not substitute for a physical-TV -compatibility pass. The screenshot gallery also needs regeneration before release because -the device navigation includes a new tab. Mobile has not been wired to this command. diff --git a/docs/DEVICE-TEST-SCRIPT.md b/docs/DEVICE-TEST-SCRIPT.md new file mode 100644 index 00000000..ed20920a --- /dev/null +++ b/docs/DEVICE-TEST-SCRIPT.md @@ -0,0 +1,93 @@ +# Physical device test script + +Run this on real hardware before tagging a v2 release. `npm run e2e` (see +[`v2/e2e/README.md`](../v2/e2e/README.md)) covers the same flows against simulated devices; this +script covers what simulation and CI cannot: vendor firmware behavior, macOS permission (TCC) +prompts, installer upgrades, and the Android runtime lifecycle of the mobile app. + +Step IDs are stable so other docs can cite them: **D1–D9** desktop basics, **A1–A10** upgrade, +launcher and device-type checks, **M1–M8** the mobile app. + +Abort rule for every section: if a step leaves the TV without a launcher or with an unexpected +disabled app, use Restore / Emergency Recovery (desktop) or More → Emergency recovery (mobile), +stop, and record the step. + +## Preconditions + +- An Nvidia Shield TV (or another supported TV) and the computer on the same LAN. +- On the TV: Settings → Device Preferences → Developer options → Network debugging on. +- Note the current default launcher, and pick one app you are willing to disable and re-enable + (a game, for example). +- Desktop: a build of the commit you intend to tag (`cd v2 && npm run tauri dev`, or + `npm run tauri build`). For the upgrade checks (A1, A2) use the staging build below. +- Mobile: the debug APK sideloaded on an Android phone on the same LAN (the app runs on the phone + and drives the TV; it is not installed on the TV). See `v2/mobile/HANDOFF.md` for the build. + +## Desktop (D1–D9, about 25 minutes) + +| Step | Action | Expected | +|------|--------|----------| +| D1 | Launch the app from the DMG with a USB drive mounted, do not click anything | Devices page loads; the boot-time scan may run; no macOS removable-volume permission prompt appears, and none recurs after quitting | +| D2 | Click Scan LAN | The Shield appears with a [NET] tag; a single scan message | +| D3 | On the TV enable Wireless debugging → Pair device with pairing code; in the app click Pair PIN, enter that IP:port and PIN | Message says paired; the PIN field clears; the app does not report a device on port 5555. Connect IP is pre-filled with the paired host plus a trailing colon and takes focus, with a one-line prompt for the separate port | +| D4 | Enter the port from the main Wireless debugging screen in Connect IP | Device becomes `device`, row clickable | +| D5 | Open the device → Apps. Find an app that is not in the catalog (a sideloaded APK) and a known bloat entry | The sideloaded app shows Unknown with a reason; catalog bloat shows Caution; nothing Unknown is pre-ticked in Optimize | +| D6 | Optimize tab: leave defaults, click Apply, read the confirmation | Confirmation lists only Caution items; Unknown items are absent unless you ticked them; apply completes; disabled apps re-enable from the Restore path | +| D7 | Launchers: switch the default launcher to another installed launcher, then switch back | Each attempt reports its stage; on any failure the previous launcher is restored and the report says so; Home on the TV matches the app's report | +| D8 | Sideload: pick a folder with APKs, quit and relaunch the app | The folder is remembered; nothing scans until Scan folder is clicked; on macOS no repeated volume prompt | +| D9 | Files → App file backups for SmartTube (if installed) after SmartTube's Settings → Backup/Restore → Local backup | The `.zip` under `Documents/SmartTubeBackup` is found | + +## Upgrade, launcher and device-type checks (A1–A10) + +| Step | Action | Expected | +|------|--------|----------| +| A1 | Windows: install the previous release's MSI, then run a **staging** MSI of the candidate over it (see "Staging build") | The installer treats it as an upgrade (no "uninstall the existing version first"); the app relaunches with the same data dir, saved devices and snapshots intact | +| A2 | macOS: with the previous release's app installed from its DMG, install a staging build of the candidate | Launching it shows the same saved devices and snapshots (shared data dir). Upgrading from a pre-2.3.0 `Shield Optimizer.app` installs `ATV Optimizer.app` alongside it; the old app is safe to trash afterward | +| A3 | Launcher tab → Advanced → Set another app as Home… with an installed custom launcher (e.g. Projectivy) | The picker lists installed apps; choosing one asks Android to make it Home and reports honestly. On a Shield or Google TV the stock launcher usually keeps Home: expect `ok = false` with a message that stock is in the way. Stock is never disabled by this step | +| A3b | Same, picking an app that declares no Home screen (e.g. YouTube) | Refused honestly ("doesn't declare a Home screen"); nothing is disabled; Disable stock launcher stays unavailable | +| A4 | **Precondition: A3 reported stock is in the way.** Launcher tab → Advanced → Disable stock launcher, taking the offered snapshot first | The confirmation names the risk. The backend disables stock, verifies the target now holds Home, and reports success; pressing Home on the remote opens the target. The TV has a Home screen throughout | +| A4b | Repeat A4 with a target that cannot actually take Home, keeping ADB connected throughout (the rollback is itself an ADB command, so a disconnected TV cannot be restored until it reconnects) | The takeover reports failure and **re-enables the stock launcher**; pressing Home opens stock. Picking TV Settings as the target is refused outright. If a real disconnect ever leaves stock disabled, reconnect and use Emergency Recovery on the Overview tab | +| A5 | A readable device whose TV evidence is **unknown** (reports neither `tv` nor a phone/tablet class, e.g. the Xiaomi TV Box S from #120) | The row carries an amber **UNCONFIRMED TV** tag and opens directly, with no Open anyway prompt. Unknown is never treated as "not a TV" | +| A6 | A device that **positively reports** it is not a TV (a phone or tablet on the same adb) | The row shows **NOT AN ANDROID TV** and is not a link; Open anyway is offered behind a short confirm. Confirming opens the tools; after reconnecting later, the row opens directly with a muted NOT A TV tag (remembered by hardware id, never by address) | +| A6b | An **unauthorized** device (accept the prompt later) | Neither tag appears; the row explains the authorization prompt. Nothing is claimed about device type | +| A7 | Wireless debugging → Pair device with pairing code; in the app use Pair PIN | On success, Connect IP is pre-filled with the paired host plus a trailing colon and takes focus, with a one-line prompt for the separate port from the Wireless debugging screen; entering that port connects | +| A8 | Tweaks → Screensaver | Reads the device's current `secure.screensaver_components`; offers AOSP Basic Daydream; Restore puts back whatever was configured when the tab loaded, without needing to know a vendor screensaver's component name | +| A9 | Snapshots: take a snapshot without changing the launcher, then open Preview restore | Preview proposes no launcher change (the Now column reads the TV's actual current Home); restoring does not touch the launcher | +| A10 | Header → Report a bug | Opens the GitHub bug-report issue form; the diagnostics field is pre-filled when the bundle fits in the link, otherwise the dialog says to paste it; nothing is sent automatically | + +### Staging build (for A1 and A2) + +Tagging a `v2-*` release (including `-rc`) publishes to every installed copy through the updater +manifest and bumps the Homebrew cask, so the upgrade test must use a local build that is never +tagged or committed: + +1. On the target OS, check out the commit you intend to tag. +2. Set the version in the working tree only, in every file `release.sh` edits: `version` in + `v2/src-tauri/tauri.conf.json` and `v2/package.json`; the `[package]` version in + `v2/src-tauri/Cargo.toml` and `v2/crates/core/Cargo.toml` (the running app reports + `CARGO_PKG_VERSION`, so skipping these builds an installer that reports the old version); and + `bundle.windows.wix.version` using the band scheme in `CLAUDE.md` (a stable `X.Y.Z` is + `X.Y.(Z*1000+999)`). The build refreshes `Cargo.lock` itself. +3. `cd v2 && npm ci && npm run tauri build`. The MSI lands under + `v2/target/release/bundle/msi/`, the DMG under `.../bundle/dmg/`. +4. Run A1 / A2 against those artifacts, then `git checkout -- .` to discard the version edits. + +If no Windows machine is available before tagging, run A1 immediately after the release workflow +finishes, on the published MSI, and treat a failure as a stop-ship: pull the release and cut a +patch. + +## Mobile (M1–M8, about 20 minutes, phone on the same LAN) + +| Step | Action | Expected | +|------|--------|----------| +| M1 | Fresh install, connect a new device | Discovery shows the Shield as Found (no Saved label yet) | +| M2 | Connect and open the Dashboard, then disconnect and rescan | The Shield shows as Saved with its name; exactly one saved row | +| M3 | Force a new DHCP lease (or change the TV's static IP), rescan | The same single saved row moves to the new address; no duplicate | +| M4 | Dashboard | "X of Y installed recommended apps are enabled" with real numbers; no score ring; Review app choices opens Optimize | +| M5 | Optimize → Recommended / Optional apps | Optional rows default to Keep; a sideloaded app shows UNKNOWN with a reason; no Select all on Optional | +| M6 | Choose Disable on one optional app, switch tabs, Apply | The summary shows 1 optional; the confirmation names the app and the 0.5× animation post-pass; Cancel leaves the TV untouched; Apply disables only that app | +| M7 | Apps → system filter; Backups | System apps hidden by default and toggle on; Backups lists every installed app, not only the catalog | +| M8 | Diagnostics → unknown-app log | Entries exist for the Unknown apps seen in M5 | + +The mobile app's own open physical gates (saved-TV reconnect #93, the restart report #105, code +pairing, fast-remote Phase 4) are listed in [`v2/mobile/BACKLOG.md`](../v2/mobile/BACKLOG.md). diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1ad94d2d..a842ef7e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,20 +1,12 @@ -# Shield Optimizer — Feature Catalog +# Shield Optimizer v1 — feature catalog -> **This is the catalog for the legacy v1 (PowerShell) script.** It was intended to become the cross-version spec and -> never did — it has no v2 coverage and no parity column, and has not been updated since -> 2026-05-20. For v2 behaviour use `v2/README.md`, `v2/ATVTOOLS-PARITY.md` and `v2/CHANGELOG.md`. -> Line references into `Shield-Optimizer.ps1` have drifted; re-check before trusting one. +> This catalogs the **legacy v1 PowerShell script** (`Shield-Optimizer.ps1`): every behavior, ADB +> command and edge case it handles. It does not cover the v2 desktop app; for v2 behavior use +> `v2/README.md`, `v2/ATVTOOLS-PARITY.md` and `v2/CHANGELOG.md`. v2 code comments cite its section +> numbers (for example §12 recovery and §13.1 detection) for the v1 behavior v2 was built to match. -This document is the **source of truth** for what Shield Optimizer does. It exists for two reasons: - -1. **Reference for the v2 (Tauri/Rust) rewrite** — a language-agnostic spec of every behavior, every ADB command, and every edge case the current v1 PowerShell implementation handles, so v2 can reach behavioral parity without reverse-engineering. -2. **Living changelog of capabilities** — every PR that adds, removes, or changes a feature is expected to update this doc in the same commit. If a feature isn't here, it doesn't exist as far as the project is concerned. - -> **Maintenance rule:** when you add a feature to v1 *or* v2, you update this doc. When the two implementations diverge, that's a regression — flag it in the **Parity** column. - -> **Source-of-truth precedence:** if the script and this doc disagree, the script wins for v1 behavior; the doc wins for v2 specification. Resolve drift by updating whichever side is wrong. - -Catalog tracks v1's current `main`. Function and line references point at `Shield-Optimizer.ps1` at HEAD — re-verify them when touching this doc to catch drift. +If the script and this doc disagree, the script wins. Function and line references point at +`Shield-Optimizer.ps1` and drift as the script changes; re-check one before trusting it. --- @@ -533,7 +525,7 @@ The two paths can disagree on edge cases. v2 should pick the union of inputs (ma ## 15. Data catalogs -These are the structured arrays that drive Optimize behavior. **In v2 they should be runtime-fetched JSON, not embedded code.** +These are the structured arrays that drive Optimize behavior. v2 keeps the same catalogs as JSON in `v2/crates/core/data/app-lists/`, embedded at compile time. ### 15.1 CommonAppList — universal Android TV bloat @@ -644,16 +636,13 @@ The script supports multiple connected devices simultaneously. The main menu (§ --- -## Maintenance checklist (for PR authors) - -When opening a PR that affects user-visible behavior, **at minimum**: +## Maintenance checklist (for v1 PRs) -- [ ] Add or update the corresponding section here -- [ ] If you added a new menu item, update §2 (main menu) or §3 (device action menu) -- [ ] If you added a new ADB call, add it to the relevant feature's "ADB calls" row -- [ ] If you added a new setting key, update §7 (Tweaks) and §9.1 (snapshot setting keys) -- [ ] If you added a new app or list, update §15 (data catalogs) -- [ ] If you fixed a previously-broken edge case, update the "Edge cases" row to reflect the fix -- [ ] Cross-link from v1 implementation to this doc by referencing section numbers in commit messages where useful +When a v1 fix changes user-visible behavior, **at minimum**: -If v2 (Rust/Tauri) work is in flight, also note which sections have parity vs. v1 in a status table here (to be added once v2 has structure). +- [ ] Update the corresponding section here +- [ ] If you added a menu item, update §2 (main menu) or §3 (device action menu) +- [ ] If you added an ADB call, add it to the relevant feature's "ADB calls" row +- [ ] If you added a setting key, update §7 (Tweaks) and §9.1 (snapshot setting keys) +- [ ] If you added an app or list, update §15 (data catalogs) +- [ ] If you fixed a broken edge case, update the "Edge cases" row to reflect the fix diff --git a/docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md b/docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md deleted file mode 100644 index 0235bb20..00000000 --- a/docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md +++ /dev/null @@ -1,27 +0,0 @@ -# Reviewed main integration into mobile - -The original integration draft combined mobile `60a1934c5e4c575a81bd124698b9a9f6203e5fe8` with reviewed main `ae0436e4e2809e4fa677542b41d0e09b29532f60`, including PR168 desktop quit behavior and PR200 installer admission hardening. Original contributor commits remain in the ancestry. No release, device installation, signing configuration change, or physical device operation occurred. - -The first normal merge is `13973d823c162815b46321442e9b1de636c7858d`, with parents mobile `60a1934` and main `40ba983`. Its only textual conflict was the Unreleased changelog: both branches' entries were retained, and the quit setting was explicitly labeled Desktop. The second normal merge of `ae0436e4` was automatic. Installer and quit production sources match reviewed main exactly. The mobile tracked tree remains unchanged from its parent. - -Shared driver compatibility was inspected: leased drivers retain mobile byte-stream forwarding; unsupported spawn remains the default; WirelessAdb inherits the no-server stop default. A core regression checks both state and snapshot stop paths skip dispatch for a driver without a server. Desktop owns the quit hook and setting; Android has no shared-server stop hook. - -## Historical local validation - -On the combined source, workspace formatting and clippy with warnings denied passed. Full serial workspace tests passed: core 489, desktop 109 (one preexisting live test ignored), mobile Rust 26, license 2, registry 9. Mobile Svelte check had zero errors/warnings, production build passed, and all 326 tests passed. Desktop Svelte check had zero errors/warnings, production build passed, and all 24 frontend test scripts passed. All 34 real-command-layer E2E scenarios passed against SimulatedAdb only; opener/install/update effects are stubbed. No physical devices were queried. - -The ARM64 Android debug APK compiled with existing JDK 17, SDK, NDK 28.2.13676358 and offline cached dependencies. APK SHA256: `9688e6a76902bca3bf0bef445693ba3396c341b72906c0c730e56da554c8d139`. The ignored local APK is not published. Upstream Kotlin/Gradle deprecation warnings remain. - -Rust used two build jobs, incremental compilation off, and dev/test debug info disabled after the initial parallel build exhausted disk. Only this checkout's generated target cache was removed. Two existing fake-process PID preconditions failed under initial parallel load; the final combined workspace passed unchanged serially, including all eight `adb::driver::tests::process_lifetimes` fixtures recorded in [`rust.txt`](evidence/mobile-main-integration-2026-10-17/rust.txt). No deadlines were relaxed. Vendor tests passed 54 with one ignored after allowing their loopback mock listeners; vendor code is unchanged by the final main merge. - -Desktop validation used an existing installed cache copied locally without installing dependencies: Kit 2.61.1 versus locked 2.70.3 and Vite 6.4.2 versus locked 6.4.4. Svelte 5.55.9 and TypeScript 5.6.3 match. Mobile cache versions match its lock. At the time of local validation, exact-lock CI and cross-platform checks remained required; these local results alone did not establish Windows/Linux success. The subsequent exact-head CI receipt below closes that gate without changing the local environment receipt. - -Both gallery GIFs were regenerated (16 frames per theme) for the desktop setting. Representative [dark Devices](evidence/mobile-main-integration-2026-10-17/devices-dark.png), [light Devices](evidence/mobile-main-integration-2026-10-17/devices-light.png), and [Logs](evidence/mobile-main-integration-2026-10-17/logs-dark.png) frames use simulated data. PR200 changes no frontend source, so these remain current. [Validation logs](evidence/mobile-main-integration-2026-10-17/) contain the combined checks; inherited PR200 raw evidence is preserved verbatim, including trailing blank lines. - -## Subsequent review and merge receipt - -The preceding local validation is a historical receipt for integration head `93e9d46bed9935cd60ac84f0f0535fae320f58ea`, not a fresh run on later mobile commits. [Final independent source review](https://github.com/bryanroscoe/shield_optimizer/pull/201#issuecomment-6233713669) approved that exact head; [all six exact-lock/platform CI jobs](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39030576353) passed. [PR201](https://github.com/bryanroscoe/shield_optimizer/pull/201) merged as `d14363bc76c05f2342289d9503e95175b2d44da2`. This closes its review/exact-lock gate while preserving the disclosed local-cache provenance. Documentation reconciliation does not rerun or replace those logs. - -## Remaining acceptance gates - -Independent review and exact-lock CI were completed for PR201 as linked above. Real phone/TV authentication, reconnect, remote and Android lifecycle checks remain physical-device gates. Desktop opt-in shutdown with real ADB, other clients, updater/restart, custom servers, and Windows/macOS/Linux remains a physical/platform acceptance gate. Simulated tests and a successful debug compile do not prove those behaviors. diff --git a/docs/RELEASE-DECISION-2026-09-09.md b/docs/RELEASE-DECISION-2026-09-09.md deleted file mode 100644 index 6b0547ff..00000000 --- a/docs/RELEASE-DECISION-2026-09-09.md +++ /dev/null @@ -1,253 +0,0 @@ -<!-- -Preserved 2026-09-12 from an external coordination tree that is being retired. -Original path: mayor/artifacts/hq-b1t/shield-release-decision-20260909.md -Content is unchanged below the horizontal rule; only this header and the closing -appendix were added. Nothing in this packet was ever executed: no tag, no -release, and no device run. Treat it as the proposal it was, not as a record of -work that happened. ---> - -# Shield Optimizer — release decision packet (2026-09-09) - -Prepared by shield_optimizer/crew/mechanic (Claude Fable 5.1) at the Mayor's request. -Nothing in this packet has been committed, pushed, tagged, released, or run on a device. -Every item below is a proposal for Bryan's decision. - -> **Update 2026-09-10:** D1 executed under Bryan's commit/push policy: `main` is at d02de57 (36b561c core+desktop, d02de57 mobile). D2, D3 and D4 remain open. - -## 0. What is being decided - -| # | Decision | Recommendation | -|---|----------|----------------| -| D1 | Commit the crew/mechanic candidate to `main` | Two whole-file commits (core+desktop first, mobile second), pushed directly to `main` per repo convention, after D3 | -| D2 | Release version and channel | Desktop `v2-2.2.0-beta.1` first, promote to `v2-2.2.0` after device pass and reporter confirmations. Mobile bumps to `0.2.0` internal debug APK, no store channel | -| D3 | Physical test on Bryan's Shield TV | Run the script in section 3 before D1 push (about 45 minutes) | -| D4 | Cross-OS CI additions | Add mobile and desktop harness jobs to `v2-tests.yml`, Node 22+ (section 4) | - -## 1. Candidate inventory and provenance - -- Tree: `shield_optimizer/crew/mechanic`, working tree on top of `origin/main` 48cba23, HEAD unchanged, zero local commits. -- Size: 41 tracked files modified (4226 insertions, 1251 deletions) plus 12 untracked application files. Excluded from any commit: `v2/node_modules`, `v2/node_modules.portpair06-link` (preserved symlink), `.svelte-kit`, build output. -- Manifest with per-file SHA-256, included patches and gate outputs: `mayor/artifacts/hq-b1t/shield-so-vtm.9-integration/manifest.json` (sha256 3ab4e56b…). -- Gates already passed on this macOS host (arm64, Node 26.3.0): core cargo fmt/clippy/test 201, src-tauri 36, desktop svelte-check 0/0 + build, mobile svelte-check 0/0 + build, mobile Playwright suite 67/67, desktop pairing harness, app-files-catalog test, desktop gallery regenerated (gallery.gif 8db1dfc7…, gallery-light.gif d1ceea10…). Not run: Linux, Windows, any device. -- Every constituent was implemented by a Sol polecat in an isolated clone or worktree, source-reviewed by mechanic, and (where UI/copy) accepted by navigator; exact patches and hashes are on the beads and under `~/.local/share/gastown/evidence/shield_optimizer/`. - -### 1.1 What the candidate contains - -Desktop + core (GitHub-issue related): -- **#88 TCL Android 14 "Device Not Supported" (so-7iw)**: `adb pair` success no longer guesses `HOST:5555`; the app requires the separate Connect IP:port from the Wireless debugging screen, clears the PIN, keeps the pairing success message after a failed connect, and retains mDNS-discovered devices. Files: `v2/src-tauri/src/adb/driver.rs`, `v2/crates/core/src/commands/devices.rs`, `v2/src/routes/+page.svelte`, `README.md`, harness `v2/tests/pairing-flow.mjs`. -- **#87 Sony XBR "Unable to Set Default Launcher" (so-x02.1)**: launcher switching now reports each failure stage explicitly and verifies rollback instead of a silent failure. Files: `v2/crates/core/src/commands/launcher.rs`. Root cause on the Sony (Android 8) is NOT proven; this improves diagnosis and rollback reporting. -- **#89 macOS "relentlessly asking for permission to access removable volumes" (so-2en, so-2en.1)**: Locating the `adb` binary no longer probes PATH or the launch working directory (which can be a removable volume) until every higher-priority candidate (explicit override, app-managed platform-tools, ANDROID_HOME) has failed, and the Sideload tab scans a saved APK folder only on an explicit click (folder remembered in localStorage). Files: `v2/src-tauri/src/adb/driver.rs`, `v2/src/lib/components/SideloadTab.svelte`, `v2/src/lib/api.ts`. The macOS TCC trigger was NOT reproduced; this removes the known probe sources. Network scanning on boot is unchanged. -- **#86 SmartTube "App file backups not found" (so-3ty)**: the catalog fix (`Documents/SmartTubeBackup`, `org.smarttube.stable`) is already on `main` 48cba23 but absent from released v2-2.1.0. This candidate adds a durable test `v2/tests/app-files-catalog.mjs` (+1 script line in `v2/package.json`). Publication is what closes the report. -- **Unknown safety (so-fb3.2)**: `engine/safety.rs` no longer falls through to Safe; uncatalogued packages are `Unknown` with a reason. Desktop consumers (`AppRow.svelte`, `OptimizeTab.svelte`, `devices/[serial]/+page.svelte`, `types.ts`, `demo-mock.ts`) show Unknown, never auto-select it, and stop treating a failed inventory lookup as Enabled. New shared contract `v2/shared/safety.ts`. -- **Mobile backup scope (so-vtm.6)**: core `commands/apps.rs` gains the all-installed backup listing used by the mobile Backups screen. -- Tooling: `v2/screenshots/capture.mjs`, `screenshots/README.md`, regenerated `gallery.gif` / `gallery-light.gif`, `.gitignore` adds `.runtime/`. - -Mobile companion app (`v2/mobile`, feedback items so-vtm.1–.9 and so-fb3.1/.2/.6): -- Saved-TV identity: hardware id is the only strong identity; no merge/rename/delete on IP-only evidence; coexisting identities at one address stay separate with neutral copy; discovery rows show Connected / Saved address (unverified) / Found / Not found; single saved TV auto-dials only when it is truly alone. (`savedDevices.ts`, `discoveryRows.ts`, `Onboarding.svelte`, `Devices.svelte`). -- Unknown safety consumers + unknown-app diagnostics logging (`safety.ts`, `Apps.svelte`, `AppDetailSheet.svelte`, `Diagnostics.svelte`, `More.svelte`, `RiskGuide.svelte`, `unknownDiagnostics.ts`). -- Apps system-app visibility filter (.4), APK backup scope and coverage (.6), frame-rate control copy (.5), Dashboard recommended-app count + Optional apps review with Keep default (.7; `Dashboard.svelte`, `Optimize.svelte`, `session.svelte.ts`). -- Tests: `savedDevices`, `unknownDiagnostics`, `apps-filter`, `backups`, `dashboard-optional-review`, updated `session` (67 cases). -- Docs: `HANDOFF.md`, `USER-FEEDBACK-2026-09-05.md`, new `LIFECYCLE-EVIDENCE.md`, `FEEDBACK-UX-2026-09-07.md` (navigator-owned; navigator's final reconciliation for .7 is still pending). - -## 2. D1 — proposed commit plan - -Repo convention (crew CLAUDE.md): crew pushes directly to `main`, no feature branches, no `--amend`, no `Co-Authored-By`. If Bryan wants a review surface instead, the alternative is a short-lived branch `convoy/shield-2026-09` with one PR; the content below is identical either way. - -Why two commits rather than one per feature: `Apps.svelte`, `api.ts`, `types.ts`, `commands/mod.rs` and `commands/apps.rs` each carry several features. Per-feature commits would need hunk splitting and would not compile independently. Two whole-file commits keep every intermediate state buildable. - -**Commit 1 — core, desktop, tooling** -``` -Harden safety verdicts, pairing, launcher rollback and sideload scanning - -- Safety engine returns Unknown instead of Safe for uncatalogued packages; - desktop lists never auto-select Unknown and stop assuming Enabled on a - failed inventory lookup (shared contract in v2/shared/safety.ts) -- Pairing no longer guesses HOST:5555 after adb pair; the Connect IP:port - from Wireless debugging is required and the mDNS device list is kept - (refs #88) -- Launcher switching reports each failure stage and verifies rollback - (refs #87) -- adb binary lookup stops probing PATH and the launch directory before - higher-priority candidates; saved APK folder is scanned only on demand - (refs #89) -- Durable test for the app-files catalog covering SmartTube backup paths - (refs #86); pairing-flow harness; regenerated screenshot galleries -``` -Files: `.gitignore`, `README.md`, `v2/crates/core/**` (5 files), `v2/shared/safety.ts`, `v2/src-tauri/src/adb/driver.rs`, `v2/src/**` (7 files), `v2/tests/*` (2 files), `v2/package.json`, `v2/screenshots/*` (4 files). - -**Commit 2 — mobile companion app** -``` -Mobile: stable saved-TV identity, Unknown safety, backup scope and optional-app review - -- Discovery recognizes saved TVs only on verified identity; duplicate - addresses stay separate; truthful Connected/Saved/Found/Not found rows -- Unknown safety verdicts, unknown-app diagnostics log, system-app filter, - all-installed backup scope, frame-rate copy -- Dashboard shows installed-recommended enabled count; Optimize gains an - Optional apps review with Keep as default and explicit per-app actions -- 67 browser/pure test cases; handoff, lifecycle evidence and feedback docs -``` -Files: `v2/mobile/**` (17 modified + 10 new). - -GitHub issue handling: use `refs #NN` only. Do not auto-close. After release: -- #86: close with a comment pointing at the release (fix verified by catalog test; device check optional). -- #88, #89, #87: comment with the release and ask the reporters to confirm on their TCL / macOS / Sony; close on confirmation. #87 stays open if the Sony still refuses `set-home-activity`, since root cause is unproven. - -## 3. D2 — proposed release - -Current published desktop release: v2-2.1.0 (June 22, 2026). Candidate changes are user-facing behavior plus a safety-classification change, so a minor bump. Because pairing (#88), launcher (#87) and macOS TCC (#89) are unverified on the reporters' hardware, ship a beta first. - -- Desktop: `v2-2.2.0-beta.1` via `cd v2 && ./release.sh --minor --beta` (interactive; script sets WiX version `2.2.301` per the documented band scheme, commits `Release v2-2.2.0-beta.1`, tags, pushes on confirmation; workflow builds Linux/macOS/Windows, regenerates the gallery, bumps the Homebrew cask). Promote with `./release.sh --set 2.2.0` once confirmations arrive. -- Mobile: bump `v2/mobile/package.json` to `0.2.0` (and the Android versionCode in the Tauri Android config) in the same release commit; build `npx tauri android build --apk --debug --target aarch64` for Bryan's phone; no store submission. Mobile has no release workflow today. - -### 3.1 CHANGELOG section to add at the top of `v2/CHANGELOG.md` -``` -## v2-2.2.0-beta.1 - -Safer defaults, honest pairing, and a companion-app overhaul. - -### Safety -- Apps missing from the audited catalog are now labeled **Unknown** with a - reason instead of Safe. Unknown apps are never pre-selected for disable - or uninstall; you choose them explicitly and the confirmation says so. -- A failed inventory read no longer shows an app as Enabled. - -### Devices and pairing -- Android 11+ pairing now asks for the separate Connect IP:port shown on - the Wireless debugging screen instead of guessing port 5555 (#88). -- The PIN is cleared after each attempt and a successful pairing is not - reported as a failure when the connect step fails. -- The app no longer probes PATH or its launch directory for adb before - checking its own platform-tools and ANDROID_HOME (#89). - -### Launchers -- Setting a default launcher reports exactly which step failed and confirms - the rollback restored the previous launcher (#87). - -### Sideload -- The Sideload tab remembers your APK folder and scans it only when you - click Scan saved folder, which avoids repeated removable-volume prompts on - macOS (#89). - -### Backups -- SmartTube backup search now includes Documents/SmartTubeBackup (#86). - -### Mobile companion (beta) -- Saved TVs are recognized by verified identity, not by IP address; devices - that share an address stay separate. -- Unknown-safety labels, an unknown-app diagnostics log, a system-app - filter, full installed-app backup scope, and a guided Optional apps review - where Keep is the default. -``` - -## 4. D3 — physical test script for Bryan's Shield TV - -Preconditions: Shield TV and the Mac on the same LAN; Settings → Device Preferences → Developer options → Network debugging ON; note the current default launcher and one app you are willing to disable and re-enable (for example a game). Build the desktop candidate from crew/mechanic with `cd v2 && npm run tauri dev` (or `npm run tauri build`). For the mobile checks, sideload the debug APK on an Android phone (companion app targets the TV; it is not installed on the TV). - -Desktop (about 25 minutes): - -| Step | Action | Expected | -|------|--------|----------| -| D1 | Launch app from the DMG with a USB drive mounted, do not click anything | Devices page loads; the usual boot-time scan may run; no macOS removable-volume permission prompt appears, and none recurs after quitting | -| D2 | Click Scan Network | Shield appears with [NET] tag; a single scan message | -| D3 | On the TV enable Wireless debugging → Pair device with pairing code; in the app click Pair PIN, enter that IP:port and PIN | Message says paired; PIN field clears; the app does NOT report a device on port 5555; the note tells you to enter the Connect IP:port | -| D4 | Enter the Connect IP:port from the main Wireless debugging screen in Connect IP | Device becomes `device`, row clickable | -| D5 | Open the device → Apps. Find an app that is not in the catalog (a sideloaded APK) and a known bloat entry | Sideloaded app shows Unknown with a reason; catalog bloat shows Caution; nothing Unknown is pre-ticked in Optimize | -| D6 | Optimize tab: leave defaults, click Apply, read the confirmation | Confirmation lists only Caution items; Unknown items absent unless you ticked them; apply completes; disabled apps re-enable from the Restore path | -| D7 | Launchers: switch default launcher to another installed launcher, then switch back | Each attempt reports its stage; on any failure the previous launcher is restored and the report says so; HOME on the TV matches the app's report | -| D8 | Sideload: pick a folder with APKs, quit and relaunch the app | Folder is remembered; nothing scans until Scan saved folder is clicked; on macOS no repeated volume prompt | -| D9 | Files → App file backups for SmartTube (if installed) after Settings → Backup/Restore → Local backup | The `.zip` under Documents/SmartTubeBackup is found | - -Mobile companion (about 20 minutes, phone on the same LAN): - -| Step | Action | Expected | -|------|--------|----------| -| M1 | Fresh install, Connect new device | Discovery shows the Shield as Found (no Saved label yet) | -| M2 | Connect and open the dashboard, then disconnect and rescan | The Shield shows as Saved with its name; exactly one saved row | -| M3 | Force a new DHCP lease (or change the TV's static IP), rescan | Same single saved row moves to the new address; no duplicate | -| M4 | Dashboard | "X of Y installed recommended apps are enabled" with real numbers; no ring or "System optimized"; Review app choices opens Optimize | -| M5 | Optimize → Recommended / Optional apps | Optional rows default to Keep; a sideloaded app shows UNKNOWN with a reason; no Select all on Optional | -| M6 | Choose Disable on one optional app, switch tabs, Apply | Summary shows 1 optional; confirmation names the app and the 0.5× animation post-pass; Cancel leaves the TV untouched; Apply disables only that app | -| M7 | Apps → system filter; Backups | System apps hidden by default and toggle on; Backups lists every installed app, not only the catalog | -| M8 | Diagnostics → unknown-app log | Entries exist for the Unknown apps seen in M5 | - -Abort rule: if any step leaves the TV without a launcher or with an unexpected disabled app, use More → Emergency recovery (mobile) or Restore (desktop) and stop; record the step. - -## 5. D4 — cross-OS CI needs - -Current `v2-tests.yml`: Rust fmt/clippy/test on ubuntu-22.04, macos-latest, windows-latest; frontend svelte-check + build on ubuntu with Node 20. `v2-release.yml` builds bundles on tag and runs the Playwright gallery job. Gaps this candidate introduces: - -1. **Node version**: `v2/tests/app-files-catalog.mjs` uses `node:module` `stripTypeScriptTypes`, available from Node 22.13; CI pins Node 20. Bump the frontend job to Node 22 (or 24). -2. **Desktop harnesses**: add steps `npm run test:app-files-catalog` and `npx playwright install --with-deps chromium && npm run test:pairing-flow` in `v2` on ubuntu. -3. **Mobile job** (new, ubuntu): `cd v2/mobile && npm ci && npm run check && npm run build && npx playwright install --with-deps chromium && npm test`. Expect 67 cases, about 2 minutes. so-ncj tracks transient failures when six browser suites start concurrently; until fixed, run with `node --test --test-concurrency=1 tests/*.test.mjs` or add a single retry. -4. **Windows/Linux Rust**: no new native dependencies; the matrix already covers the core and src-tauri changes. Mobile `src-tauri/src/lib.rs` (one-line command registration) is only compiled by the Android build, which CI does not run; add `cargo check -p` for the mobile crate on ubuntu if a cheap guard is wanted. -5. **What CI cannot prove**: macOS TCC prompts (#89), TCL/Sony device behavior (#87, #88), Android runtime lifecycle of the companion app. Those stay on the D3 script and reporter confirmations. - -## 6. Open items that do not block D1–D4 - -- Navigator copy acceptance of so-vtm.7 and docs reconciliation (in progress; text-only). -- so-fb3.2 typed process identity for memory rows, so-fb3.3/.4/.5/.7 (product follow-ups, not in this candidate). -- so-ncj browser-suite flakiness (P3). - - ---- - -## Appendix — what is still unknown on #87 and #89 - -Preserved from a separate triage note dated 2026-09-08. Both fixes on `main` were written -without reproducing the reporter's hardware, so these are the open questions a future -investigation should answer rather than assuming the fix was the root cause. - -For #89 causal completion, the missing evidence is the affected app/macOS version, installation and selected ADB/folder paths, close-window versus true-quit lifetime, and the process/path responsible for TCC requests. Current source and pinned Tauri 2.11.2 show no project exit-prevention loop; source inspection alone does not reproduce the report. Do not reset TCC, grant access, kill shared ADB, or silently change relative path semantics. Any later controlled reproduction must preserve explicit relative binary/transfer/install paths and stay within authorized host/device scope. - -For #87 causal completion, compare actual target enabled state, resolved HOME/component, setter result and restoration result across the failing and successful sequences. The current generic error cannot identify which branch failed. AOSP Android 8 already supports the relevant package commands; no blanket version incompatibility or speculative fallback is justified. Existing never-disable, stock-only and explicit opt-in guards remain mandatory. - ---- - -## 7. Addendum — v2-2.3.0 device test script (2026-09-29) - -`v2-2.2.0` shipped 2026-09-16; nothing since has run on a physical device. `v2-2.3.0` -(the ATV Optimizer rename plus #88, #119–#124) adds its own risk areas on top of the D3 -script above, which still applies to whatever it didn't touch. Run these in addition, -not instead: - -| Step | Action | Expected | -|------|--------|----------| -| A1 | Windows: install the previous `v2-2.2.0` MSI, then run a **staging** 2.3.0 MSI over it (see "Staging build" below) | Installer treats it as an upgrade (no "uninstall the existing version first"); the app relaunches as ATV Optimizer with the same data dir, saved devices and snapshots intact | -| A2 | macOS: with an existing `Shield Optimizer.app` installed from a prior DMG, install a staging 2.3.0 build (see below) | New app installs as `ATV Optimizer.app` alongside the old one under the new name; launching it shows the same saved devices/snapshots (shared data dir); the old `Shield Optimizer.app` is safe to trash afterward | -| A3 | Launcher tab → Advanced → Set another app as Home… with an installed custom launcher (e.g. Projectivy) | Picker lists installed apps; choosing one asks Android to make it Home and reports honestly. On a Shield/Google TV the stock launcher usually keeps Home: expect `ok = false` with a message that stock is in the way. Stock is never disabled by this step | -| A3b | Same, picking an app that declares no Home screen (e.g. YouTube) | Refused honestly ("doesn't declare a Home screen"); nothing is disabled; Disable stock launcher stays unavailable | -| A4 | **Precondition: A3 reported stock is in the way.** Launcher tab → Advanced → Disable stock launcher, taking the offered snapshot first | Confirmation names the risk. Backend disables stock, verifies the target now holds Home, and reports success; pressing Home on the remote opens the target. The TV has a Home screen throughout | -| A4b | Repeat A4 with a target that cannot actually take Home, keeping ADB connected throughout (the rollback is itself an ADB command, so a disconnected TV cannot be restored until it reconnects) | Takeover reports failure and **re-enables the stock launcher**; pressing Home opens stock. Picking TV Settings as the target is refused outright. If a real disconnect ever leaves stock disabled, reconnect and use Emergency Recovery on the Overview tab | -| A5 | A readable device whose TV evidence is **unknown** (reports neither `tv` nor a phone/tablet class — the #120 Xiaomi case) | Row carries an amber **UNCONFIRMED TV** tag and opens directly, with no Open anyway prompt. Unknown is never treated as "not a TV" | -| A6 | A device that **positively reports** it is not a TV (a phone or tablet on the same adb) | Row shows **NOT AN ANDROID TV** and is not a link; Open anyway is offered behind a short confirm. Confirming opens the tools; after reconnecting later, the row opens directly with a muted NOT A TV tag (remembered by hardware id, never by address) | -| A6b | An **unauthorized** device (accept the prompt later) | Neither tag appears; the row explains the authorization prompt. Nothing is claimed about device type | -| A7 | Wireless debugging → Pair device with pairing code; in the app use Pair PIN | On success, Connect IP is pre-filled with the paired host plus a trailing colon and takes focus, with a one-line prompt for the separate port from the Wireless debugging screen; entering that port connects | -| A8 | Tweaks → Screensaver | Reads the device's current `secure.screensaver_components`; offers AOSP Basic Daydream; Restore puts back whatever was configured when the tab loaded, without needing to know a vendor screensaver's component name | -| A9 | Snapshots: take a snapshot without changing the launcher, then open Preview restore | Preview shows no launcher change proposed (the "Now" column reads the TV's actual current Home, not "—"); restoring does not touch the launcher | -| A10 | Header → Report a bug | Opens the GitHub bug-report issue form; the diagnostics field is pre-filled when the bundle fits in the link, otherwise the dialog says to paste it; nothing is sent automatically | - -### Staging build (for A1 and A2) - -Cutting `v2-2.3.0` (or any `-rc` tag) publishes to every installed copy through the updater -manifest and bumps the Homebrew cask, so the upgrade test must use a local build that is never -tagged or committed: - -1. On the target OS, check out the commit you intend to tag. -2. Set the version in the working tree only, in every file `release.sh` edits: `version` to - `2.3.0` in `v2/src-tauri/tauri.conf.json` and `v2/package.json`; the `[package]` version in - `v2/src-tauri/Cargo.toml` and `v2/crates/core/Cargo.toml` (the running app reports - `CARGO_PKG_VERSION`, so skipping these builds an installer that calls itself 2.2.0); and - `bundle.windows.wix.version` to `2.3.999`. The build refreshes `Cargo.lock` itself. -3. `cd v2 && npm ci && npm run tauri build`. The MSI lands under - `v2/target/release/bundle/msi/`, the DMG under `.../bundle/dmg/`. -4. Run A1 / A2 against those artifacts, then `git checkout -- .` to discard the version edits. - -If no Windows machine is available before tagging, run A1 immediately after the release -workflow finishes, on the published MSI, and treat a failure as a stop-ship: pull the release and -cut a patch. - -Abort rule is the same as D3: if a step leaves the TV without a launcher or with an -unexpected disabled app, use Restore and stop; record the step. - diff --git a/docs/WHERE-EVERYTHING-IS.md b/docs/WHERE-EVERYTHING-IS.md deleted file mode 100644 index c453a045..00000000 --- a/docs/WHERE-EVERYTHING-IS.md +++ /dev/null @@ -1,90 +0,0 @@ -# Where everything is - -Master index, written 2026-09-12 as Gas Town was wound down. Gas Town was a -multi-agent orchestration workspace at `~/Developer/gastown` coordinating four -projects. It is going away. Each project now runs as a standalone agent from its -own folder in `~/Developer`. This file lists every location so nothing is lost. - -## The four projects - -| Project | Standalone folder | GitHub | Backlog | -|---|---|---|---| -| Accrete, incremental game in Godot | `~/Developer/accrete` | `bryanroscoe/accrete` | 52 issues labeled `gastown` | -| Novalyst, book compendium and reader | `~/Developer/Novalyst` | `bryanroscoe/Novalyst` | 41 issues labeled `gastown` | -| Shield Optimizer, Android TV app | `~/Developer/shield_optimizer` | `bryanroscoe/shield_optimizer` | 22 issues labeled `gastown` | -| Saget, home server operations | `~/Developer/sagetserver` | `bryanroscoe/sagetserver` | 43 issues labeled `gastown` | -| Gas Town HQ, tooling and coordination | `~/Developer/gastown` | `bryanroscoe/gastown-hq` (private) | 28 issues labeled `gastown` | - -Every issue is titled `[bead-id] title`, so references to old bead ids such as -`ac-mlg.4` or `nv-sus` still resolve by searching the issue list. Each repository -also holds, under `docs/`: - -- `gastown-*-beads-export.json`, the complete tracker export including closed - items, with full descriptions, acceptance criteria and comment history. This is - the authoritative record where an issue body was trimmed for length. - -## What is in git and what is not - -In git and pushed: all source, the design and engineering record, acceptance -evidence, decision packets, run reports and the bead exports. The preservation -sweep on 2026-09-12 moved several bodies of work that had been living only in -agent workspaces into the repositories: - -- Accrete: 111 design files, the canonical gameplay design, tuning decisions and - every playtest judgment including Bryan's verbatim reviews, now `docs/design/`. -- Novalyst: 87 crew and polecat documents, the entire review and acceptance record. -- Saget: 101 branch-only candidate files under `candidates/`, twelve task - directories with their scripts, tests and fixtures, all re-tested from the new - paths, plus the T61 relinker which had been sitting in no git repository at all. -- Shield: the release decision packet, `docs/RELEASE-DECISION-2026-09-09.md`, - with the commit plan, draft changelog and the physical device test script. -- Gas Town HQ: 410 Mayor briefs, reviews and decision packets under - `mayor/artifacts/`. - -Not in git, on disk only, too large or not source: - -| Path | Size | What it is | -|---|---|---| -| `~/Developer/accrete/builds/accrete-recovery-r08-20260911f` | 422 MB | The current playable Accrete build, Mac app plus Windows zip, copied out of the Gas Town tree. Launch guide is `README-AMENDED-HANDOFF.md` inside it. | -| `~/Developer/gastown/accrete/.runtime` | 9.2 GB | Every Accrete build and QA run, R01 through R08F, with receipts and captures. Superseded builds; keep only if you want the full history. | -| `~/Developer/gastown/mayor/artifacts` | 4.4 GB | Mayor working tree. The documents from it are in `bryanroscoe/gastown-hq`; the rest is build output, caches and vendored upstream checkouts. | -| `~/Developer/gastown/.dolt-data` | 196 MB | The original beads databases. The JSON exports carry the same content in readable form. | -| `~/Developer/Novalyst/test_books` | 13 GB | Novalyst book sources and generated output, including the owned Neuromancer EPUB. | -| `~/Developer/recovery-archives` | 31 GB | Pre-existing archive, unrelated to the shutdown. | -| `~/Developer/gastown-quarantine` | 12 KB | A stale beads directory moved aside on 2026-09-09 because the scheduler was scanning it every 30 seconds. Safe to delete. | - -## Other checkouts that exist - -- `~/Developer/Novalyst-portraits-v2`, branch `codex/portraits-v2`, same remote as - Novalyst, with uncommitted work. Flagged to the Novalyst owner during the sweep. -- `~/Developer/tmux-adapter`, branch `fix/web-adapter-history-copy-input`, remote - `gastownhall/tmux-adapter`. Holds a built and deployed fix for terminal scroll - history, text selection and Command-Backspace. It could not be pushed: the - account has read access only on that repository. Commit `b438ef4` is local. -- `~/Developer/sagetbot`, `~/Developer/saget-org`, `~/Developer/tvintelligentsia` - and others predate Gas Town and were not part of it. Several have uncommitted - changes of their own. - -## Project state at shutdown - -**Accrete.** R08F is the current build and the first the crew cleared for play. -The measured performance fix took 200 loose pieces from 0.24x to 0.89x achieved -speed. Open: remaining copy and geometry polish, Bastion events never crew-played, -native relaunch unverified because the host screen stays locked, and deferred -save-persistence coverage. - -**Novalyst.** Backend and Flutter client with green CI. A complete run over -*Neuromancer* produced 216 entities, 843 page snapshots and 24 recaps, reviewed -and accepted. Open: pricing, legal scope, entity density and recap length. - -**Shield Optimizer.** State as of 2026-09-12 was `36b561c` + `d02de57`; `main` has moved well past that. See `v2/HANDOFF.md` for current state and `CLAUDE.md` for what is in flight. Remaining then and now: run the physical device test and cut a release tag. - -**Saget.** Ten local candidates accepted, seven production changes applied with -rollback and independent verification. Remaining decisions are listed in -`sagetserver/crew/steward/review-evidence/bryan-decisions-20260908.md`, also in -the repository. - -## Migration tool - -`mayor/bd2gh.py` in `bryanroscoe/gastown-hq` converted beads to GitHub issues. It -is idempotent: re-running it creates only what is missing. diff --git a/v2/ADB-QUIT-HARDENING.md b/v2/ADB-QUIT-HARDENING.md deleted file mode 100644 index a7692b33..00000000 --- a/v2/ADB-QUIT-HARDENING.md +++ /dev/null @@ -1,37 +0,0 @@ -# Quit hardening after PR168 - -The implementation authority is [shyney7's PR168](https://github.com/bryanroscoe/shield_optimizer/pull/168), merged as `40ba983b76e37891f439d5ce19c8364acdf53698`. Its default-off setting, shared snapshot leases, tracked local children and conservative quit path remain the foundation. There is no server ownership inference. A live Remote session still causes quit to leave the server running; this work does not force session teardown. - -## Focused regression scope - -Installation must hold the existing AppState operation counter before any installer effects, through download, extraction and driver replacement. A canceled async caller must not release a still-running blocking extraction's lease. Closed admission must reject a new install before the injected installer runs. Tests use a bounded worker and a nonexistent replacement binary, never Google's download endpoint or the user's platform-tools directory. - -Additional fixtures cover a direct client exiting while a descendant retains its output pipes, and old/replacement drivers retaining the same process-admission boundary. The pipe test tracks the direct client only; it does not claim to own or reap grandchildren. A mock quit test verifies that a snapshot of the old driver holds up drain and that the eventual stop is delegated only to the replacement mock driver. - -See [the validation evidence](evidence/quit-install-admission-20261017/README.md). No UI, copy or captured screen changes, so screenshots and video are not applicable. Existing simulated preview video is historical evidence of a different proposal, not evidence of current production behavior. - -## Preserved historical work - -These draft PRs and their branches remain intact pending independent review of this replacement. Their controllers, policies and development app must not be merged as a stack onto PR168. - -| Draft | Preserved value | Disposition after independent review | -| --- | --- | --- | -| [174](https://github.com/bryanroscoe/shield_optimizer/pull/174), head `2eb5fef` | Earlier unwired lifecycle reasoning | Superseded by PR168's production implementation | -| [175](https://github.com/bryanroscoe/shield_optimizer/pull/175), head `e5484d9` | Installer, inherited-pipe, driver replacement and process/IPC regression ideas; [audit](https://github.com/bryanroscoe/shield_optimizer/blob/e5484d9e2eb3095f0c1926d2c921f8e2b94b5763/v2/ADB-PROCESS-LIFETIMES.md) | Extract selected cases; retire competing controller/factory and broad wrappers | -| [176](https://github.com/bryanroscoe/shield_optimizer/pull/176), head `4df768c` | Installer admission-gap regression; [simulated settings video and provenance](https://github.com/bryanroscoe/shield_optimizer/tree/4df768ce1c888e6e39d066d98c9b5ac48fbf54f0/v2/evidence/exit-preview-20261016) | Preserve evidence; three-choice policy/first-quit prompt remain a separate proposal | -| [177](https://github.com/bryanroscoe/shield_optimizer/pull/177), head `10497a0` | [Cancellation/updater contract research](https://github.com/bryanroscoe/shield_optimizer/blob/10497a0cece9b884149e076a8a7621b22bdb3012/v2/EXIT-COORDINATOR-CONTRACT.md) | Historical alternative, not a second coordinator to adopt | -| [178](https://github.com/bryanroscoe/shield_optimizer/pull/178), head `af3afc9` | [Pinned framework and native storage notes](https://github.com/bryanroscoe/shield_optimizer/blob/af3afc9aa71a05dce64d9e03a95d2890483c5ef8/v2/NATIVE-EXIT-ADAPTER-GATES.md) | Preserve research; no native preference migration or new exit executor | -| [179](https://github.com/bryanroscoe/shield_optimizer/pull/179), head `1b0c555` | [Development event/mailbox experiments and limitations](https://github.com/bryanroscoe/shield_optimizer/blob/1b0c555908a5ed857232ed8ea3e9149a277b14af/v2/WIRED-NATIVE-EXIT-DEVELOPMENT.md) | Retire alternate entry point after preservation review | - -## Framework boundaries checked against pinned source - -Cargo.lock pins Tauri and tauri-runtime-wry 2.11.2, updater plugin 2.10.1 and process plugin 2.3.1. The cached registry sources were inspected for these versions: - -- `tauri-2.11.2/src/app.rs`: `ExitRequestApi::prevent_exit` ignores restart requests. `AppHandle::exit` falls back to cleanup and `std::process::exit` if runtime delivery fails. `cleanup_before_exit` requires immediate exit without further Tauri API calls. -- `tauri-plugin-updater-2.10.1/src/updater.rs`: Windows installation calls its synchronous `on_before_exit` hook and then exits directly. An ordinary asynchronous exit listener cannot be assumed to run for that path. - -These boundaries justify keeping updater integration separate. This patch does not replace updater hooks, intercept process exit, claim native-window acceptance, or infer a token from the next exit event. It does not alter PR168's restart handling. Physical macOS/Windows/Linux shared-client and updater acceptance remain unperformed here. - -## Mobile boundary - -[Mobile PR163](https://github.com/bryanroscoe/shield_optimizer/pull/163) at `60a1934c5e4c575a81bd124698b9a9f6203e5fe8` diverged from merged main (56 commits ahead, 16 behind when inventoried). Integrating main into that branch is a separate task. This follow-on adds only a shared operation-lease API and desktop installation/tests; it does not add mobile quit policy, change Android transport behavior, or port the historical broad command-wrapper rewrite. diff --git a/v2/ATV-OPTIMIZER-ANDROID-PLAN.md b/v2/ATV-OPTIMIZER-ANDROID-PLAN.md deleted file mode 100644 index 8c9df897..00000000 --- a/v2/ATV-OPTIMIZER-ANDROID-PLAN.md +++ /dev/null @@ -1,68 +0,0 @@ -# ATV Optimizer Android App — Implementation Plan - -## Summary - -Build a separate Android phone/tablet app that acts as the ADB client for Android TV / Google TV devices over Wi-Fi. The mobile product name is **ATV Optimizer**. The desktop app took the same name in v2-2.3.0; it deliberately kept its identifier (`com.shieldoptimizer.app`), data folder, updater channel, Homebrew cask token and release pipeline, and pins its WiX UpgradeCode to the old name's value so installs upgrade in place. - -This plan starts with a behavior-preserving Rust workspace extraction so the audited safety engine and driver-generic command layer can be shared by desktop and mobile. - -## Architecture - -- Keep the current desktop app at `v2/src-tauri`. -- Add a workspace rooted at `v2/Cargo.toml` with: - - `crates/core` (`shield-optimizer-core`) for the pure engine, shared ADB trait/output parsers, shared command handlers, embedded app lists, and entitlement model. - - `src-tauri` for the existing desktop Tauri app, subprocess ADB driver, updater, platform-tools installer, host-network scan, and desktop file-path commands. - - Future `mobile/` for the Android Tauri app and mobile frontend. -- Mobile ADB transport will be a Kotlin Tauri mobile plugin backed by libadb-android, called by a Rust `WirelessAdb` implementation. -- `AdbDriver` is intentionally partial on mobile: `shell`, `raw_bytes`, and synthesized `raw(["devices"])` work; `spawn`, `forward`, `push`, and `pull` are unsupported until mobile-specific equivalents are built. -- Scrcpy fast remote input remains desktop-only via `#[cfg(not(target_os = "android"))]`; mobile keeps the shared `adb shell input ...` fallback. -- Desktop UI remains untouched. A future mobile Svelte frontend should live separately and share only layout-agnostic TypeScript types/fixtures/components. - -## Free / Pro Split - -Free: pairing/connect, device profile, health/storage/RAM, risk-tiered audit, launcher view, screenshot, force-stop, trim caches, normal reboot, basic shell remote input, open Settings, and recovery/safety commands. - -Pro: curated debloat/uninstall/optimize, snapshots save/apply/delete, launcher takeover, settings/tweaks writes, app permission/op writes, advanced reboot, multi-device, and later advanced remote/sideload/files/backup/clone. - -Desktop remains always-Pro. Mobile starts Free and uses signed entitlement tokens to unlock Pro. Shared Rust gates return `LOCKED:<feature>` before side effects. - -## Milestones - -1. **M0 — Transport/permission/license spike:** prove phone-to-TV pair/connect/shell/screencap on real hardware through a Tauri Android plugin; audit libadb-android dependencies and Android network permissions. -2. **M1 — Workspace extraction:** create `shield-optimizer-core`, keep desktop behavior-identical, update workflows/scripts for workspace paths, and correct stale handoff facts. -3. **M2 — Mobile app scaffold + `WirelessAdb`:** add Android Tauri app, Kotlin `AdbService`, Rust `WirelessAdb`, and mobile handler list. -4. **M3 — Guided pairing + Free diagnostic UX:** add mDNS/manual pairing flow and free diagnostic screens. -5. **M4 — Touch-first mobile frontend:** build the separate mobile UI with demo fixtures. -6. **M5 — Pro gates + licensing:** implement license activation/refresh/offline grace and gate every Pro command in Rust. -7. **M6 — Distribution beta:** signed sideload-first APK/AAB, developer verification/package registration, and optional store-channel decisions. - -## Validation - -After M1, run from `v2/`: - -```bash -cargo fmt --all --check -cargo clippy -p shield-optimizer-core -p shield-optimizer-v2 -p atv-optimizer-mobile --all-targets -- -D warnings -cargo test -p shield-optimizer-core -p shield-optimizer-v2 -p atv-optimizer-mobile -npm run check -npm run build -``` - -For the mobile scaffold, run from `v2/mobile/`: - -```bash -npm run build -npx tauri android build --target aarch64 --apk -``` - -From M2 onward, add hardware validation for pairing, reconnect, screencap, shell command success/failure, mDNS fallback, persisted ADB keypair, and Android network permissions. - -## Assumptions - -- Desktop rebrand is out of scope. -- Mobile MVP excludes APK sideload, files, backup/clone, screen mirror, and mouse mode. - Status 2026-10-06: Files (browse, pull to the phone, delete under `/sdcard`, Find app backups) - and APK backup/restore have since shipped on mobile, unverified on a device. Phone-to-TV upload - (needs an Android document picker), APK sideload, cloning, screen mirror and mouse mode are - still not built. See `mobile/HANDOFF.md`. -- Existing untracked root files (`atv-optimizer-android-strategy.html`, root `package.json`, root `package-lock.json`, root `node_modules/`) are not part of this implementation unless explicitly added later. diff --git a/v2/ATVTOOLS-PARITY.md b/v2/ATVTOOLS-PARITY.md index 17e6bffc..1624aeb1 100644 --- a/v2/ATVTOOLS-PARITY.md +++ b/v2/ATVTOOLS-PARITY.md @@ -1,12 +1,12 @@ -# aTV Tools parity plan +# aTV Tools parity -Comparison of Shield Optimizer v2 against **aTV Tools** (`dev.vodik7.atvtools` by vodik7) — the most popular ADB toolbox for Android TV — and a prioritized plan for closing the gaps that fit our product. +Comparison of the ATV Optimizer desktop app against **aTV Tools** (`dev.vodik7.atvtools` by vodik7), the most popular ADB toolbox for Android TV, and the gaps that remain. -Research notes: aTV Tools is a **phone/tablet companion app** (Android 8.1+/iOS 14+) controlling the TV over network ADB; freemium (~$2 lifetime Pro unlocks app management, remote, recording, gamepad). We're a free, open-source **desktop** app over the same transport — so most of its features are reachable for us, and some of its phone-centric ones (gamepad, mirroring) don't make sense here. +aTV Tools is a **phone/tablet companion app** (Android 8.1+/iOS 14+) controlling the TV over network ADB; freemium (~$2 lifetime Pro unlocks app management, remote, recording, gamepad). We're a free, open-source **desktop** app over the same transport — so most of its features are reachable for us, and some of its phone-centric ones (gamepad, mirroring) don't make sense here. ## Feature comparison -| Capability | aTV Tools | Shield Optimizer v2 | +| Capability | aTV Tools | ATV Optimizer (desktop) | |---|---|---| | Curated debloat catalog + safety list | ❌ | ✅ (core strength) | | Optimize/Restore wizard with per-app defaults | ❌ | ✅ | @@ -31,54 +31,16 @@ Research notes: aTV Tools is a **phone/tablet companion app** (Android 8.1+/iOS | Screen mirroring / gamepad / media remote | ✅ (phone-centric) | — (out of scope for desktop) | | Open source / free | ❌ | ✅ | -**Playback report** (Playback tab, unreleased) shows codec configuration, reported HDR/display modes, and audio policy. These observations do not establish runtime decoder support, acceleration, or playback quality. +**Playback report** (Playback tab) shows codec configuration, reported HDR/display modes, and audio policy. These observations do not establish runtime decoder support, acceleration, or playback quality. -**Bottom line:** we beat aTV Tools on the *debloat/optimize/safety* core, they beat us on *general device utilities*. The gaps worth closing are the utilities that complement debloating; the phone-centric features aren't our product. +**Bottom line:** ATV Optimizer leads on the *debloat/optimize/safety* core and now covers the general device utilities that complement it (APK backup and cloning, file manager, screenshots, force-stop, text entry, shell runner with bookmarks, cache trim, CPU/network sampling, permissions, low-latency remote). The desktop Shell tab is an explicit expert opt-in: its lexical gate catches obvious protected-package commands but shell expressions can bypass it, so it never carries the curated actions' safety guarantee. Mobile does not expose a shell. -## Prioritized roadmap +## Remaining gaps -### P1 — App backup / cloning + file management (the asked-for set) +- **Screen recording** — `screenrecord` (3-minute cap, no DRM content), pull and save. Not built. +- **Mouse mode** on the Remote tab. The Remote has D-pad, hold-to-repeat and typing/paste over the scrcpy control channel ([`REMOTE-LATENCY-PLAN.md`](REMOTE-LATENCY-PLAN.md)), but no pointer. +- **Out of scope:** screen mirroring, gamepad and media remote — phone-form-factor features. -**1. APK backup ("Save APK to computer")** -Shape: for an installed app, resolve its APK path(s) with `pm path <pkg>` (may return multiple lines for split APKs), `adb pull` each to a user-chosen folder, name them `<pkg>-<version>.apk`. UI: a "Backup APK" action on App List rows + the memory table. -- Driver needs a `pull(serial, remote, local)` capability on `AdbDriver` (new trait method wrapping `adb -s X pull`). -- Split APKs (`pm path` returning base.apk + split_*.apk) must install together via `adb install-multiple` — detect and warn. +## Idea not yet built -**2. App cloning ("Install on another device")** -Shape: combine #1 with the existing `install_apk`: pick app on device A → pull APK(s) to a temp dir → pick target device B → `adb -s B install(-multiple)`. One wizard: "Copy app to another device…" listing other connected devices. -- Caveat to surface in UI: app *data* doesn't come along (no root); DRM/licensed apps may refuse; paid apps should be reinstalled via Play instead. - -**3. File manager (browse / push / pull)** -Shape: a new device tab "Files". Backend: `list_dir(serial, path)` via `ls -lA` parsing (or `toybox ls -llA`), `pull_file`, `push_file`, `delete_file` (confirm + path-restricted to `/sdcard` by default). UI: single-pane browser of `/sdcard` with breadcrumbs, Upload / Download / Delete buttons. -- Keep scope to `/sdcard` (user storage) initially — no system paths, avoids foot-guns. - -**4. Screenshots** -Shape: `adb -s X exec-out screencap -p > local.png`, save to a user folder, show a preview + "Save / Copy". One button on the device header or Health tab. (Driver needs an exec-out/binary-capture capability.) - -### P2 — Power-user utilities - -**5. Force-stop** on memory-table rows (`am force-stop <pkg>`) — trivial, pairs with the existing Disable button. -**6. Send text to TV** — `input text '<escaped>'` for typing Wi-Fi passwords/searches from the desktop keyboard. Small input box on the device header. (Escape carefully; relates to the package-validation work.) -**7. Shell runner with bookmarks** — implemented on the unreleased desktop Shell tab. Explicit expert acknowledgment → editor → Run → bounded stdout/stderr. Presets/bookmarks populate the editor only. The lexical gate catches obvious protected-package commands but can be bypassed by shell expressions; this is an explicit exception to curated-action safety. Mobile shell is deferred. -**8. Bulk cache clear** — `pm trim-caches 999999999999` (one call, no per-app loop). -**9. CPU + network monitor** — implemented by `resource_sample` on the unreleased desktop and mobile apps: two device-side samples with `/proc/uptime` elapsed time and per-interface network rates. Independent of the health report; mobile sampling is explicitly requested, not polled. - -### P3 — Evaluate later -**10. Screen recording** — `screenrecord` (3-min cap, no DRM), pull + save. Nice demo material. -**11. Permissions viewer/grant/revoke** — `dumpsys package <pkg>` parse + `pm grant/revoke`. Niche; gate behind Advanced. -**12. Remote control / D-pad** — `input keyevent` buttons. **Shipped** (Remote tab, scrcpy low-latency channel, clipboard paste in `2d571fd`). -**Skip:** screen mirroring, gamepad, media remote — phone-form-factor features. - -## Related design items (from beta feedback) - -**A. "Optional" apps not in the catalog.** Many preinstalled apps aren't in our curated list but are safe-if-unused (user request). Proposal: an "Everything else" section at the bottom of the App List — third-party packages (`pm list packages -3`) not in the catalog, badged `NOT CURATED`, default action **Keep**, with Disable/Uninstall available behind the standard safety gate + a one-line "Optional — remove if you don't use it" description. Keeps the curated list authoritative while making the long tail actionable. - -**B. Snapshot ↔ Optimize convergence.** The snapshot preview is now a table (beta.7). Possible next step: render the preview as an *editable* optimize-style plan (per-row dropdowns: apply/skip each disable) so apply-snapshot and optimize share one mental model — and potentially one component. - -## Suggested sequencing - -1. P1.1 + P1.2 (APK backup → cloning) — directly requested, builds on existing install path -2. P1.4 screenshots (small, high-delight) and P2.5 force-stop (trivial) -3. P1.3 file manager (largest single piece) -4. A (optional-apps section) — data-model + UI, pairs naturally with the App List -5. P2.6–9 as filler between releases +**Snapshot ↔ Optimize convergence.** The snapshot preview is a table. A possible next step is to render it as an *editable* optimize-style plan (per-row apply/skip) so apply-snapshot and Optimize share one mental model, and potentially one component. diff --git a/v2/HANDOFF.md b/v2/HANDOFF.md index 993d5dbf..39315911 100644 --- a/v2/HANDOFF.md +++ b/v2/HANDOFF.md @@ -1,86 +1,109 @@ # v2 — current state -v2 is a shipping desktop app. Last **published** version: **2.3.0** (2026-09-30). +v2 is a shipping desktop app. Latest **published** version: **2.3.0** (2026-09-30). -Full release pipeline live: installers built for macOS/Linux/Windows on every `v2-*` tag push via `.github/workflows/v2-release.yml`; macOS also distributed via Homebrew tap (`bryanroscoe/homebrew-shield-optimizer`). +Installers for macOS/Linux/Windows are built on every `v2-*` tag push via +`.github/workflows/v2-release.yml`; macOS is also distributed via the Homebrew tap +(`bryanroscoe/homebrew-shield-optimizer`). ## Unreleased on `main` -`v2-2.3.0` shipped 2026-09-30 (the ATV Optimizer rename plus #88, #119–#124). -`v2-2.3.1` is prepared in `v2/CHANGELOG.md` (Setup Wraith fixes #122/#157/#158, #97, -#98, #100, #129 and the rest listed there) and waits on the owner's go to tag. -Things the next reader needs that the code does not say: +`v2/CHANGELOG.md` has a prepared `v2-2.3.1` section (the Setup Wraith fixes #122/#157/#158, +#97, #98, #100, #129 and the rest listed there) that has not been tagged, plus an `Unreleased` +section above it (the Logs tab, Stop ADB when quitting, and fixes). The next tag waits on the +owner's go. Before tagging, run the physical script in +[`../docs/DEVICE-TEST-SCRIPT.md`](../docs/DEVICE-TEST-SCRIPT.md) and `npm run e2e`. -- **ADB quit hardening:** PR168 is the implementation authority. See [ADB-QUIT-HARDENING.md](ADB-QUIT-HARDENING.md) for the focused installer regression follow-on, framework limits and preserved historical drafts #174–179. +## Things the next reader needs that the code does not say -- **The app is now ATV Optimizer.** `identifier`, the `ShieldOptimizer` data dir and - the updater channel are unchanged on purpose; the WiX UpgradeCode is pinned in - `tauri.conf.json` to the value the old name derived. The Homebrew tap change - (`homebrew-shield-optimizer#2`) merged with the `v2-2.3.0` release; its `url` names - the renamed DMG. +- **The app is ATV Optimizer.** `identifier`, the `ShieldOptimizer` data dir and the updater + channel keep the old name on purpose, and the WiX UpgradeCode is pinned in `tauri.conf.json` + to the value the old name derived, so existing installs upgrade in place. The Homebrew tap and + cask token are still `shield-optimizer`; the cask's `url` names the renamed DMG. - **Launchers are data**: `crates/core/data/app-lists/launchers.json`, parsed once in - `loader.rs`. The Launcher list shows HOME handlers only. An earlier `LEANBACK_LAUNCHER` - union listed every TV app as a Home app (every TV launch activity carries that - category) and was removed; `launcher-rows` and a Rust test guard it. The - last-HOME-handler guard counts HOME handlers alone. -- **Stock takeover is its own step.** `set_home_any` (Launcher › Advanced) enables a - package and asks Android to make it Home, then reports whether Android accepted it. - It never disables stock. Disabling stock is the separate `disable_stock_launcher` - command behind its own confirmed button. Both stay gated by - `is_last_enabled_home_handler`, so the TV always keeps a Home screen. + `loader.rs`. The Launcher list shows HOME handlers only. Don't add a `LEANBACK_LAUNCHER` + query: every TV app's launch activity carries that category, so it lists every TV app as a + Home app. `launcher-rows` and a Rust test guard this. The last-HOME-handler guard counts HOME + handlers alone. +- **Stock takeover is its own step.** `set_home_any` (Launcher › Advanced) enables a package and + asks Android to make it Home, then reports whether Android accepted it. It never disables + stock. Disabling stock is the separate `disable_stock_launcher` command behind its own + confirmed button. Both stay gated by `is_last_enabled_home_handler`, so the TV always keeps a + Home screen. +- **Launcher setting on old Android.** Android 8 has neither `cmd package query-activities` (9+) + nor `cmd role` (10+), so every way of setting Home shares one setter ladder that falls back + past them (#87). - **Snapshot semantics.** `engine/snapshot.rs` receives the current launcher in - `ApplyPlanInputs` and sets `launcher_to_set` only when the snapshot's launcher - differs from it; `apply_snapshot` skips the launcher ladder entirely when it is - `None`. Settings already at the snapshot's value are carried as "already set", so the - preview's Now column is real. A restore disables the recorded apps and writes the - settings back; it never re-enables or reinstalls anything. -- **One label source, one recommendation source.** `src/lib/safety.ts` owns every - verdict label (Protected, Caution, Safe to remove, Unknown, Checking…, Safety - unavailable). `src/lib/recommendation.ts` owns `effectiveMethod`, `recommendation`, - `canOfferUninstall` and the review labels; the App List, Optimize defaults and - Health's Suggestion column all call it. Uninstall is recommended only when the app is - reinstallable (`play_store || defunct`), and never labelled "Remove". Don't add a - mapping inline in a component; `recommendation-labels` checks the screens agree. -- **Demo catalog**: `src/lib/demo-apps.json` is `common.json` followed by `shield.json`, - copied verbatim. Regenerate it whenever those change, or the demo drifts from the - product. -- **Logs**: `<data_dir>/logs/`, daily rotation, 7 kept. Debug level is per-crate, not - global, so adb lines are not buried under hyper. `collect_diagnostics` formats in - the engine (pure) and reads in `src-tauri/src/commands/diagnostics.rs`. -- **TV evidence** is a three-way value (`tv | not_tv | unknown`); only `not_tv` blocks a - row. Do not re-introduce "unknown means phone" — that was #120. -- **Not addressed here**: the mobile identity bugs #115–#118 (same rule family, other - app). - -The 2.2.0 device-verification notes below still apply to the fixes that shipped in it. - -The five user-reported issues are all addressed here: - -| Issue | State | -|---|---| -| [#86](https://github.com/bryanroscoe/shield_optimizer/issues/86) SmartTube backups | Fixed. Catalog searches `Documents/SmartTubeBackup`; a failed search no longer reads as "no matches". Covered by `npm run test:app-files-catalog`. | -| [#87](https://github.com/bryanroscoe/shield_optimizer/issues/87) Sony launcher default | Root-caused. On Android 8 the stock fast path registered nothing before disabling stock, because `cmd package query-activities` (9+) and `cmd role` (10+) do not exist there. Both paths now share one setter ladder. Needs the reporter to confirm. | -| [#88](https://github.com/bryanroscoe/shield_optimizer/issues/88) TCL Android 14 not found | Fixed in two parts: pairing stopped guessing `:5555`, and Scan Network now reads `adb mdns services` so a random wireless-debugging port is discoverable at all. There is no Android version gate in this codebase. | -| [#89](https://github.com/bryanroscoe/shield_optimizer/issues/89) macOS volume prompts | Root cause identified: adb subprocesses inherited the launch cwd, and the adb daemon outlives the app, so a DMG launch left a process pinning `/Volumes/...`. Every spawn is now pinned to a stable directory. Only a physical macOS run can confirm. | -| [#91](https://github.com/bryanroscoe/shield_optimizer/issues/91) Remote clipboard paste | Fixed in `2d571fd`. A focused non-editable div does receive paste with `clipboardData` (checked in Chromium and WebKit, the latter being macOS's WKWebView), so an `onpaste` handler plus a Paste button was enough — no clipboard plugin. The only one of the five confirmed working by a human. | - -The first four have migrated duplicates (`#107`, `#112`, `#94`, `#92`) — close one side of each pair. #91's duplicate is `#108`. + `ApplyPlanInputs` and sets `launcher_to_set` only when the snapshot's launcher differs from + it; `apply_snapshot` skips the launcher ladder entirely when it is `None`. Settings already at + the snapshot's value are carried as "already set", so the preview's Now column is real. A + restore disables the recorded apps and writes the settings back; it never re-enables or + reinstalls anything. +- **One label source, one recommendation source.** `src/lib/safety.ts` owns every verdict label + (Protected, Caution, Safe to remove, Unknown, Checking…, Safety unavailable). + `src/lib/recommendation.ts` owns `effectiveMethod`, `recommendation`, `canOfferUninstall` and + the review labels; the App List, Optimize defaults and Health's Suggestion column all call it. + Uninstall is recommended only when the app is reinstallable (`play_store || defunct`), and + never labelled "Remove". Don't add a mapping inline in a component; `recommendation-labels` + checks the screens agree. +- **Demo catalog**: `src/lib/demo-apps.json` is `common.json` followed by `shield.json`, copied + verbatim. Regenerate it whenever those change, or the demo drifts from the product. +- **Pairing and discovery.** A successful `adb pair` never implies a connect port; the user + enters the separate Connect port from the Wireless debugging screen. Scan reads + `adb mdns services`, so a random wireless-debugging port is discoverable. There is no Android + version gate anywhere (#88). +- **adb working directory.** Every adb spawn is pinned to a stable directory. The adb server + outlives the app, so inheriting the launch cwd left a process pinning a DMG's `/Volumes/...` + path and triggered repeated macOS removable-volume prompts (#89). +- **Remote paste** works through an `onpaste` handler on the focused remote element plus a Paste + button; a focused non-editable element receives `clipboardData` in both Chromium and WebKit, so + no clipboard plugin is needed (#91). +- **Stop ADB when quitting** (`src-tauri/src/commands/quit.rs`, `crates/core/src/adb/lifetimes.rs`) + is off by default because the adb server is shared by every tool on the machine. Even when on, + quit leaves the server running while any operation lease or adb process is outstanding after + a short drain (an open Remote session holds a lease), when the app restarts for an update, and + when adb points at a server the app cannot target. The app never infers that a server is its + own. The platform-tools installer holds an operation lease from before download through driver + replacement. Framework limits behind this design (Tauri 2.11.2, updater plugin 2.10.1): + `ExitRequestApi::prevent_exit` ignores restart requests, and the updater's Windows install + path calls its synchronous `on_before_exit` hook and exits directly, so an async exit listener + cannot be assumed to run there. Keep updater integration separate from the quit path. Real + macOS/Windows/Linux checks with other adb clients, the updater restart and custom servers have + not been run. +- **Logs tab** (`crates/core/src/commands/logcat.rs`, desktop only; mobile does not register + it) takes finite, read-only logcat snapshots, not a stream: each read replaces the last, + entries can repeat between reads, and high-volume traffic can roll out of the window. Reads + start only on an explicit click. Severity, one exact tag and an optional package constrain + the device-side read; package filtering resolves the running main PID per snapshot and fails + rather than falling back to unfiltered logs. Every call goes through the bounded driver (30 s, + 256 KiB per stream), one read at a time. Auto-refresh waits three seconds after each read and + stops on error, tab departure, device-page reset, document hiding or Stop. Tags cannot start + with a hyphen or carry shell/filter syntax, and nothing clears buffers, resizes them or writes + settings. Logs are never saved, uploaded or added to bug reports. +- **App logs**: `<data_dir>/logs/`, daily rotation, 7 kept. Debug level is per-crate, not + global, so adb lines are not buried under hyper. `collect_diagnostics` formats in the engine + (pure) and reads in `src-tauri/src/commands/diagnostics.rs`. +- **TV evidence** is a three-way value (`tv | not_tv | unknown`); only `not_tv` blocks a row. Do + not re-introduce "unknown means phone" — that was #120. ## Roadmap -ATV Optimizer Android app plan: see **[`ATV-OPTIMIZER-ANDROID-PLAN.md`](ATV-OPTIMIZER-ANDROID-PLAN.md)**. - -Feature parity gaps against aTV Tools — see **[`v2/ATVTOOLS-PARITY.md`](ATVTOOLS-PARITY.md)** for the current comparison table and prioritized plan. +- Feature gaps against aTV Tools: [`ATVTOOLS-PARITY.md`](ATVTOOLS-PARITY.md). +- ATV Optimizer for Android: [`mobile/HANDOFF.md`](mobile/HANDOFF.md) and + [`mobile/BACKLOG.md`](mobile/BACKLOG.md). ## Known deferred items -- **Mobile Android scaffold** — initial ATV Optimizer mobile app lives in [`mobile/`](mobile/). It builds an unsigned aarch64 APK and still needs real phone/TV validation for mDNS, pairing, reconnect, shell, and screencap. -- **Memory-usage spike (user report)** — likely root-caused to a tab lazy-load re-fetch loop (fixed in PR #62); confirm it's gone on the next release build. Unrelated to the Health-tab verdict stall fixed in `73d481d`. -- **Screen recording** — the one aTV Tools capability still absent; see [`ATVTOOLS-PARITY.md`](ATVTOOLS-PARITY.md). -- **Code signing** — builds are unsigned on macOS and Windows. Setup notes are at the top of `.github/workflows/v2-release.yml`. - -Done since this list was last written, kept here only because other docs still point at them as pending: remote-control latency (the scrcpy control channel shipped — `crates/core/src/adb/remote_input.rs`, server jar bundled at `v2/src-tauri/resources/scrcpy-server-v3.1`) and Remote clipboard paste (#91, shipped in `2d571fd` with the `test:remote-paste` harness). +- **Screen recording and mouse mode** — the aTV Tools capabilities still absent; see + [`ATVTOOLS-PARITY.md`](ATVTOOLS-PARITY.md). +- **Code signing and update-key custody** — builds are unsigned on macOS and Windows. The + reasoning is in the root `CLAUDE.md`; setup notes are at the top of + `.github/workflows/v2-release.yml`. +- **Frontend audit** — `npm audit` reports one low advisory, `cookie` 0.6.0 via SvelteKit + (GHSA-pxg6-pf52-xh8x). The fix needs `cookie` ≥0.7.0, outside SvelteKit's supported range, so + don't force an override; re-audit before each release. ## Invariants + release process -See **`CLAUDE.md`** (or `AGENTS.md` for agents) at the repo root — architecture invariants, safety-gate rules, release script usage, and MSI versioning notes are all there. +See **`CLAUDE.md`** (or `AGENTS.md`, a symlink to it) at the repo root — architecture +invariants, safety-gate rules, release script usage, and MSI versioning notes are all there. diff --git a/v2/README.md b/v2/README.md index 2cb72452..f9f396df 100644 --- a/v2/README.md +++ b/v2/README.md @@ -56,7 +56,7 @@ The engine is the part that's portable. It knows what the rules are (which packa ``` v2/ -├── README.md, ATV-OPTIMIZER-ANDROID-PLAN.md +├── README.md, HANDOFF.md, CHANGELOG.md ├── Cargo.toml, Cargo.lock # Rust workspace ├── package.json # desktop frontend dependencies ├── crates/ diff --git a/v2/REMOTE-LATENCY-PLAN.md b/v2/REMOTE-LATENCY-PLAN.md index 69401c25..e4aeb2a0 100644 --- a/v2/REMOTE-LATENCY-PLAN.md +++ b/v2/REMOTE-LATENCY-PLAN.md @@ -1,98 +1,65 @@ -# Remote-control latency plan +# Remote-control input channel -Status: **implemented.** All three phases below shipped. `RemoteInputSession` lives in -`crates/core/src/adb/remote_input.rs` (not `src-tauri/` — `16e5c42` moved the shared layer), the -pinned scrcpy v3.1 server is bundled at `v2/src-tauri/resources/scrcpy-server-v3.1`, and -`crates/core/src/commands/input.rs` drives the channel with the old per-press path as fallback. +The Remote tab injects input through a resident scrcpy-server control channel instead of one +`adb shell input` call per press. `RemoteInputSession` lives in +`crates/core/src/adb/remote_input.rs`, the pinned scrcpy v3.1 server is bundled at +`v2/src-tauri/resources/scrcpy-server-v3.1`, and `crates/core/src/commands/input.rs` drives the +channel with the per-press `input` path as a transparent fallback. The mobile app uses the same +channel over its own transport; see [`mobile/FAST-REMOTE-PLAN.md`](mobile/FAST-REMOTE-PLAN.md). -Kept for the Phase 0 hardware measurements and the lifecycle gotchas below — the ~700 ms figure, -`cmd input` being absent on Shield Android 11, and the server's socket-close and stdin behaviour. -Those were measured on real devices and are recorded nowhere else. The ~700 ms per press now -describes only the fallback path. +This doc keeps the hardware measurements and lifecycle behavior that justify the design. They +were measured on real devices and are recorded nowhere else. -## Why a key press takes ~700 ms today +## Why `input keyevent` is slow -Every press runs `adb -s X shell "input keyevent <code>"` -(now `crates/core/src/commands/input.rs`), paying four costs in series: +Every fallback press runs `adb -s X shell "input keyevent <code>"`, paying four costs in series: -1. Spawn a fresh `adb` client + connect to the local adb server (~30–80 ms) +1. Spawn a fresh `adb` client and connect to the local adb server (~30–80 ms) 2. Open a new shell service on the device over the network (~50–150 ms) -3. **`input` boots a fresh Java VM on the TV** (`app_process`), binds to - InputManager, injects one event, exits — ~300–600 ms on Shield firmware. - This dominates. -4. Teardown + output round-trip +3. **`input` boots a fresh Java VM on the TV** (`app_process`), binds to InputManager, injects + one event, exits — ~300–600 ms on Shield firmware. This dominates. +4. Teardown and output round-trip -A persistent `adb shell` session only removes costs 1–2; the Java VM cold-start -happens per `input` invocation no matter how the command arrives. Earlier -probes: `sendevent` is SELinux-blocked on Shield, and `input` measured ~700 ms -per keyevent. +A persistent `adb shell` session only removes costs 1–2; the Java VM cold start happens per +`input` invocation no matter how the command arrives. `sendevent` is SELinux-blocked on Shield. -## Phase 0 — benchmark on real devices (15 min, needs a Shield) +## Measurements (two NVIDIA SHIELD Android TV units, Android 11) -Confirms the cost split, and checks for a fast path on newer firmware: +Both units gave identical results: + +- **`input keyevent` ≈ 690 ms** per press (0.69 s real, of which only ~0.18 s is user CPU — the + rest is JVM cold start). The first call takes ~6.1 s (one-time warm-up). +- **`cmd input` → "No shell command implementation."** The fast binder path does not exist on + Shield Android 11, so a persistent shell plus `cmd input` is not an option there. +- **Multi-key batching** (`input keyevent 20 20 20`) is still one ~0.69 s call and can't stream + live presses. + +The probes, for re-checking other firmware: ```bash adb shell "time input keyevent 20" # baseline (cost 3 in isolation) -adb shell "cmd input keyevent 20" 2>&1 # native binder path; exists on newer - # Android and is ~10x faster if so -adb shell "time input keyevent 20 20 20" # multi-key batching support? +adb shell "cmd input keyevent 20" 2>&1 # native binder path, where it exists +adb shell "time input keyevent 20 20 20" # multi-key batching ``` -Decision rule: if `cmd input` exists and lands under ~100 ms on the whole -fleet, a persistent shell + `cmd input` is competitive and much simpler — stop -here and build that instead. Otherwise (expected on 2019-era firmware), -proceed to the scrcpy design. - -### Phase 0 results (2026-06-10, two Shield Android-TV units, Android 11) - -Measured on `192.168.42.196` and `192.168.42.71` (both NVIDIA SHIELD Android TV, -Android 11) — identical results: +**scrcpy control channel on the same hardware:** -- **`input keyevent` ≈ 690 ms** per press (0.69s real, of which only ~0.18s is - user CPU — the rest is JVM cold-start). First call ~6.1s (one-time warm-up). - Confirms the original estimate and that the Java VM boot dominates. -- **`cmd input` → "No shell command implementation."** The fast binder path - does **not** exist on Shield Android 11. The simple fallback is off the - table; scrcpy is the answer. -- **Multi-key batching** (`input keyevent 20 20 20`) is still one ~0.69s call — - no help (one JVM boot covers all three, but you can't stream live presses - through it). - -**scrcpy control channel — verified working on this hardware:** - scrcpy-server **v3.1** (90 640 bytes, SHA-256 `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0`) pushed to - `/data/local/tmp`, launched via `app_process` in control-only mode - (`video=false audio=false control=true tunnel_forward=true - send_device_meta=false send_dummy_byte=true`). -- Server identified the device ("Device: [NVIDIA] NVIDIA SHIELD Android TV - (Android 11)") and accepted a TCP control connection over `adb forward`. -- Our own client received the 1-byte handshake (`\x00`) and wrote - `INJECT_KEYCODE` messages (14 bytes each: `>BBIII` = type, action, keycode, - repeat, metaState) at **~0.01 ms/write**. -- **End-to-end injection proven with state read-back**: SLEEP (223) through - the channel flipped the device Dreaming → Asleep — observed via an - independent `dumpsys power` poll in **152 ms including the polling - overhead** — and WAKEUP (224) restored it. The encoding is correct and the - per-press cost collapses from 690 ms to network RTT. - -**Verdict: build the scrcpy control channel (Phases 1–3 below).** Lifecycle -notes for the Rust impl, all verified on hardware: - -- A control-only server **exits on its own the moment the control socket - closes** — hold the socket for the tab's lifetime; on teardown drop the - socket first, then best-effort `pkill -f com.genymobile.scrcpy` (present at - `/system/bin/pkill` on this firmware, supports `-f`), then remove the - forward. -- **Spawn the server child with stdin explicitly `/dev/null`** (open, EOF) and - stdout/stderr piped or nulled. The device-side `app_process` aborts at - startup (exit 134, zero Java output) when the adb client's stdin is fully - closed; a GUI app's inherited stdin is unreliable, so never inherit it. - -## The fix — scrcpy-server control channel - -scrcpy (Apache-2.0) pushes a ~90 KB Java server to the device, runs it once via -`app_process` with shell privileges, and keeps it resident, injecting events -through InputManager directly. We use its control-only mode (no video/audio): + `/data/local/tmp`, launched via `app_process` in control-only mode (`video=false audio=false + control=true tunnel_forward=true send_device_meta=false send_dummy_byte=true`). +- The server accepts a TCP control connection over `adb forward`; the client receives the 1-byte + handshake (`\x00`) and writes `INJECT_KEYCODE` messages (14 bytes each: `>BBIII` = type, + action, keycode, repeat, metaState) at ~0.01 ms per write. +- End-to-end injection was confirmed by state read-back: SLEEP (223) through the channel flipped + the device Dreaming → Asleep, observed by an independent `dumpsys power` poll in 152 ms + including polling overhead, and WAKEUP (224) restored it. Per-press cost is network RTT. + +## How the channel works + +scrcpy (Apache-2.0) pushes a ~90 KB Java server to the device, runs it once via `app_process` +with shell privileges, and keeps it resident, injecting events through InputManager directly. +The app uses its control-only mode (no video/audio): ```text once per device session: @@ -103,48 +70,29 @@ once per device session: TcpStream::connect(localhost:<port>) # stays open per key press: - socket.write(14-byte INJECT_KEYCODE message) # ~5–30 ms total + socket.write(14-byte INJECT_KEYCODE message) ``` -Free wins that fall out of this design: - -- **Real key-down/key-up** → press-and-hold D-pad scrolling, which - `input keyevent` fundamentally cannot do. -- **UTF-8 text injection** → removes the printable-ASCII-only limit in - `encode_input_text` (`input text` silently mangles non-ASCII). - -## Implementation phases - -**Phase 1 — session plumbing (Rust).** New `adb/remote_input.rs`: -`RemoteInputSession` per serial (push, forward, spawn server, connect socket), -stored in `AppState` behind `Mutex<HashMap<serial, Session>>`. Lazy-start on -first Remote-tab key; auto-reconnect on socket death; teardown on disconnect -and app exit (`kill_on_drop` on the wrapping shell + scid-based kill). All adb -invocations go through the existing `SubprocessAdb` — the raw TCP socket is a -documented exception to the one-wrapper rule, like `scan.rs`'s route/ip calls. - -**Phase 2 — protocol client.** Implement exactly two scrcpy control messages -(fixed binary structs): `INJECT_KEYCODE` (action down/up, keycode, repeat, -meta) and `INJECT_TEXT`. Pin one exact scrcpy-server version + SHA-256. -Bundle the jar as a Tauri resource (Apache-2.0 attribution in the README). -Mind the handshake: the server sends a device-meta preamble before the control -socket is usable. - -**Phase 3 — wire-up + fallback.** `send_key` / `send_text` try the session -first and fall back transparently to today's `input` path if the session can't -start (odd firmware, denied `app_process`) — nothing regresses. Frontend -(`RemoteTab.svelte`): pointerdown/pointerup for hold-to-repeat, drop the -250 ms typing batch, small "live / fallback" status indicator. - -**Testing.** Session lifecycle against MockAdb (push/forward/spawn command -assertions, using the error-injection support added in PR #66); protocol -encoding as byte-exact unit-test fixtures; latency verified manually on -device. - -## Estimates and open questions - -- Phase 0: ~15 min with a device. Phases 1–2: ~2–3 days. Phase 3: ~half day. -- Riskiest bit: scrcpy handshake details — known and documented, just fiddly. -- Open: bundle vs download the server jar (leaning bundle — simpler, offline- - friendly, ~90 KB); whether press-and-hold repeat is wanted in the UI - (leaning yes — it's free and it's what makes a software remote feel real). +- Sessions are per serial, start lazily on the first Remote-tab key, reconnect on socket death, + and tear down on disconnect and app exit. All adb invocations go through `SubprocessAdb`; the + raw TCP socket to the forwarded port is a documented exception to the one-wrapper rule, like + `scan.rs`'s route/ip calls. +- Only two control messages are implemented: `INJECT_KEYCODE` (down/up, keycode, repeat, meta) + and `INJECT_TEXT`. The server version and SHA-256 are pinned. +- Real key-down/key-up gives press-and-hold D-pad repeat, which `input keyevent` cannot do, and + `INJECT_TEXT` carries UTF-8 that `input text` would mangle. +- `send_key` / `send_text` fall back to the per-press `input` path when the session can't start + (odd firmware, denied `app_process`), so nothing regresses. +- Tests cover session lifecycle against MockAdb (push/forward/spawn assertions with error + injection) and byte-exact protocol encoding; latency is verified by hand on a device. + +## Lifecycle rules (verified on hardware) + +- A control-only server **exits on its own the moment the control socket closes**. Hold the + socket for the session's lifetime; on teardown drop the socket first, then best-effort + `pkill -f com.genymobile.scrcpy` (present at `/system/bin/pkill` on Shield firmware, supports + `-f`), then remove the forward. +- **Spawn the server child with stdin explicitly `/dev/null`** (open, EOF) and stdout/stderr + piped or nulled. The device-side `app_process` aborts at startup (exit 134, no Java output) + when the adb client's stdin is fully closed; a GUI app's inherited stdin is unreliable, so + never inherit it. diff --git a/v2/screenshots/README.md b/v2/screenshots/README.md index 98725520..dcca1ff3 100644 --- a/v2/screenshots/README.md +++ b/v2/screenshots/README.md @@ -33,7 +33,7 @@ not connect to a device. CI leaves the branch unchanged. ## Automatic regeneration on release -The `refresh-screenshots` job in [`../../.github/workflows/v2-release.yml`](../../.github/workflows/v2-release.yml) reruns this whole pipeline on every `v2-*` tag and commits the refreshed `gallery.gif` + `gallery-light.gif` back to the default branch — so a release never ships stale screenshots. Those captures render on Linux (font stack falls through to Roboto rather than macOS's `-apple-system`), so the release-generated GIF can look subtly different from one you regenerate locally on a Mac. Both are fine; it's the same UI. +The `refresh-screenshots` job in [`../../.github/workflows/v2-release.yml`](../../.github/workflows/v2-release.yml) reruns this whole pipeline on every `v2-*` tag and commits the refreshed `gallery.gif` + `gallery-light.gif` back to the default branch — so a release never ships stale screenshots. Those captures render on Linux. The fonts are bundled (see [`../src/fonts/README.md`](../src/fonts/README.md)), so fonts match, but rendering can still differ subtly from a GIF regenerated on a Mac. Both are fine; it's the same UI. ## Tuning diff --git a/v2/src/fonts/README.md b/v2/src/fonts/README.md index a79cf68c..43117972 100644 --- a/v2/src/fonts/README.md +++ b/v2/src/fonts/README.md @@ -4,11 +4,12 @@ Geist and Geist Mono come from the npm `geist` package and are distributed under License 1.1. They are bundled rather than fetched: this is a desktop app with no network guarantee, and a webfont that fails to load is a silently different-looking app. -Bundling them also removes a real source of drift. The screenshot gallery is regenerated on Linux -during a release but usually by hand on macOS, and the previous system font stack resolved to -Roboto on one and -apple-system on the other. The same files now render the same way everywhere. +Bundling them also removes a source of drift. The screenshot gallery is regenerated on Linux +during a release but usually by hand on macOS, and a system font stack would resolve to Roboto on +one and -apple-system on the other. Bundled files render the same way everywhere. These are the same files as `v2/mobile/src/fonts/`, kept in both places so neither app depends on the other's tree. If you update one, update both. -A consolidated in-app third-party notices surface is still required before release. +The desktop app has no in-app third-party notices screen yet (mobile shows its notices on +the More screen); adding one is open work. From ff67fd16abbeed79454ff132e5627434bad08701 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe <bryanroscoe@gmail.com> Date: Thu, 22 Oct 2026 23:20:58 -0500 Subject: [PATCH 2/3] Mobile docs: delete historical audits and receipts, rewrite the handoff Delete the architecture review, the old screen/command matrix, the correctness audit, both feedback logs, the unknown-diagnostics UX contract, the transport licensing research, the dependency remediation receipt and the release-polish checklist and receipts. Their live facts (the no-GPL transport rule and why, saved-TV identity rules, the #105 description, open polish checks) move into HANDOFF, BACKLOG and LIFECYCLE-EVIDENCE. Rewrite HANDOFF.md as a present-tense current-state handoff and BACKLOG.md as an ordered P0/P1/P2 queue. Trim commit hashes, dated checkpoints and coordination vocabulary from the remaining plans, and correct stale claims (notices coverage, the pubkey encoder fix, the desktop rename, already-registered commands, component and CI facts). --- v2/mobile/ANDROID-DEPENDENCIES.md | 11 +- v2/mobile/ARCHITECTURE-REVIEW.md | 201 ------ v2/mobile/BACKLOG.md | 396 +++------- v2/mobile/BILLING-COMMAND-MATRIX.md | 34 +- v2/mobile/BILLING-IMPLEMENTATION-PLAN.md | 10 +- v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md | 152 ---- v2/mobile/DEPENDENCY-REMEDIATION-20261016.md | 58 -- v2/mobile/EXPORT-DISCOVERABILITY.md | 35 +- v2/mobile/FAST-REMOTE-PLAN.md | 177 ++--- v2/mobile/FEATURES.md | 141 ---- v2/mobile/FEEDBACK-UX-2026-09-07.md | 264 ------- v2/mobile/FREE-SETTINGS-UNDO-DESIGN.md | 6 +- v2/mobile/HANDOFF.md | 720 ++++++------------- v2/mobile/LIFECYCLE-EVIDENCE.md | 335 +++------ v2/mobile/PAIRING-PLAN.md | 70 +- v2/mobile/README.md | 21 +- v2/mobile/RELEASE-POLISH-RECEIPTS.md | 40 -- v2/mobile/RELEASE-POLISH.md | 43 -- v2/mobile/RELEASE.md | 46 +- v2/mobile/TRANSPORT-LICENSING-RESEARCH.md | 316 -------- v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md | 154 ---- v2/mobile/USER-FEEDBACK-2026-09-05.md | 94 --- v2/tools/emulator/README.md | 54 +- 23 files changed, 607 insertions(+), 2771 deletions(-) delete mode 100644 v2/mobile/ARCHITECTURE-REVIEW.md delete mode 100644 v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md delete mode 100644 v2/mobile/DEPENDENCY-REMEDIATION-20261016.md delete mode 100644 v2/mobile/FEATURES.md delete mode 100644 v2/mobile/FEEDBACK-UX-2026-09-07.md delete mode 100644 v2/mobile/RELEASE-POLISH-RECEIPTS.md delete mode 100644 v2/mobile/RELEASE-POLISH.md delete mode 100644 v2/mobile/TRANSPORT-LICENSING-RESEARCH.md delete mode 100644 v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md delete mode 100644 v2/mobile/USER-FEEDBACK-2026-09-05.md diff --git a/v2/mobile/ANDROID-DEPENDENCIES.md b/v2/mobile/ANDROID-DEPENDENCIES.md index 2445e29e..d1ce4c73 100644 --- a/v2/mobile/ANDROID-DEPENDENCIES.md +++ b/v2/mobile/ANDROID-DEPENDENCIES.md @@ -1,8 +1,8 @@ # Android dependency notice inventory — release preparation Sources: app and ADB-plugin `build.gradle.kts`, plus the Android build files from locked -Tauri 2.11.2 and opener 2.5.4 Cargo registry packages. This inventory adds the previously omitted -AndroidX Browser dependency. It is not a resolved Gradle dependency report. +Tauri 2.11.2 and opener 2.5.4 Cargo registry packages. It is a declaration-level list, not a +resolved Gradle dependency report. | Declaration site | Maven coordinates / declared version | |---|---| @@ -31,9 +31,8 @@ configuration name in that generated project). Inventory every resolved artifact upstream NOTICE, including transitive Kotlin/Jackson dependencies; preserve any required texts. Reconcile selected versions with this declaration inventory and regenerate notices. This final resolved-graph/license-text gate remains OPEN; it cannot be cleared by the declaration table. -No Gradle dependency or package version is changed by this preparation. -## Locked published-crate evidence +## Locked published-crate check The cached `tauri-2.11.2.crate` archive SHA-256 is `437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28`, exactly matching @@ -41,5 +40,5 @@ The cached `tauri-2.11.2.crate` archive SHA-256 is `aab1b0ecb929ea70b33ad42d7df55c185a3485f9609ec998047232b45df76a4d`; line 43 declares `com.fasterxml.jackson.core:jackson-databind:2.15.3`. The local extracted file is byte-identical. It does NOT declare jackson-datatype-json-org. A declaration seen in a newer cached Tauri version -must not be attributed to this locked package. This receipt verifies declarations, not Gradle -transitive resolution or packaged contents. +must not be attributed to this locked package. Re-check when the Tauri pin changes. This verifies +declarations, not Gradle transitive resolution or packaged contents. diff --git a/v2/mobile/ARCHITECTURE-REVIEW.md b/v2/mobile/ARCHITECTURE-REVIEW.md deleted file mode 100644 index 0339c06b..00000000 --- a/v2/mobile/ARCHITECTURE-REVIEW.md +++ /dev/null @@ -1,201 +0,0 @@ -# ATV Optimizer Mobile — Architecture Review (2026-07-04) - -> Historical review. Most original P0 frontend findings were subsequently fixed. Current open -> correctness work is tracked in [`BACKLOG.md`](BACKLOG.md); do not treat unchecked prose in this -> document as current state without verifying the code. - -Full review of the mobile app across three dimensions (transport/core, frontend, build/ -distribution/security), each done by a dedicated pass. Findings are consolidated and -prioritized here. Companion to `HANDOFF.md` (state + playbook). Items marked **[device]** -need on-device verification (the test Pixel was unplugged when this was written). - -## Guiding principle (from the user) -`crates/core` is **shared with the working desktop app** — mobile must stay aligned with it. -Several mobile-specific edits to shared core are band-aids for a transport problem and should -be reverted once the transport is fixed properly (see T1/T3 below). Keep only genuine -cross-platform improvements. - ---- - -## P0 — Correctness / trust bugs (fix first) - -### F1. `reboot_device` called with the wrong arg name — silently never reboots -`screens/Dashboard.svelte:125` passes `{ serial, rebootMode: "normal" }`; the core command -takes `mode` (Tauri maps `rebootMode`→`reboot_mode`, so `mode` is missing → deserialize -error). The UI shows "Reboot command sent!" regardless, then disconnects. **Fix:** `{ serial, -mode: "normal" }` (matches desktop `api.ts`). *Trivial.* - -### F2. Failed health load renders as a *healthy* device (worst trust bug) -`Dashboard.svelte` sets `errorHealth` on failure but the template never renders it — it only -branches on `loadingHealth`. So when `health_report` throws, the dashboard shows -`healthScore = 100`, "System optimized", "0 active bloat apps". A total backend failure looks -like a perfectly clean TV. Also `healthScore`/`freedRamEst` are invented client-side -(`100 - activeBloatCount*3`, `activeBloatCount*45`), not backend values. **Fix:** render an -error/retry state on `errorHealth`; derive the score from a real signal or drop the number. - -### F3. Diagnostics shows fabricated numbers as live metrics -`Diagnostics.svelte:38-49` hardcodes fallbacks indistinguishable from real data: -`storageTotal ?? "16G"`, `storageUsed ?? "9.8G"`, `used_percent ?? 63`, `tempC ?? 48`, -`audioOutput ?? "Atmos"`, `displayHz ?? "60Hz"`. When `health_report` returns null the screen -confidently shows "9.8G / 16G", "48°C", "Atmos". **Fix:** render `—`/"unavailable"/skeleton -when a field is null; never a plausible fake. - -### F4. Optimize screen is 100% mock and discards the real plan -`Optimize.svelte` hardcodes the debloat list + "182 MB" sizes; `loadPlan()` calls -`prepare_optimize` only to swallow the `LOCKED:` error and throws the real `OptimizePlan` -away. Nothing reflects the connected device. **Fix:** show the real read-only plan preview on -Free, or clearly label it "example". - -### F5. Diagnostics reimplements safety classification inline — violates the safety invariant -`Diagnostics.svelte:91-134` has its own `getSafetyTag` + package→name map, bypassing core's -audited `safety_info` + known-names loader. This is the "one detection function / do-not- -disable list is mandatory" invariant. **Fix:** call `safety_info`. (Note: the *destructive* -paths are still safe — B6 confirms core gates them — but the displayed tags can disagree with -the engine, which is misleading.) - -### T4. Reboot reports a false failure **[device]** -`wireless_adb.rs:130` routes `reboot` over `exec:`; when the device reboots, adbd drops the -socket, the read hits the 8s timeout → `reboot_device` returns `ok:false` even though it -worked. Also `self.connected` still holds the dead device. **Fix:** treat a transport drop on -a `reboot` command as success; clear `self.connected`. - ---- - -## P1 — Architecture - -### F6. No shared session store; state prop-drilled as `any`; entitlement never threaded -`App.svelte` holds `host`/`connectPort`/`connectedDevice` and prop-drills a 4-field block into -all 7 screens (typed `any`). No caching (Dashboard + Diagnostics each re-fetch `health_report`). -Entitlement (Free/Pro) is never read on the client — `More.svelte` fakes it by string-matching -`"pro"`. Null-device transition can crash Dashboard (`connectedDevice.serial` with no guard). -**Fix:** a small runes store `src/lib/session.svelte.ts` (connectedDevice, host, entitlement + -connect/disconnect/reconnect actions); fetch entitlement once after connect; guard the null -transition. - -### F7. Backend contract drift — inline types + raw `invoke()` duplicate desktop `api.ts`/`types.ts` -Every screen redefines partial types and raw `invoke("string")` calls; results typed `any`. -The desktop `v2/src/lib/{api.ts,types.ts}` are the canonical, "keep in sync with core" -wrappers. The F1 reboot bug is exactly what a typed wrapper catches. **Fix (plan M4):** extract -a shared typed `api`/`types` module (add the `wireless_*` + `find_remote` commands); replace -every inline `invoke`/`any`. - -### T1. The global Kotlin mutex is the true root cause of the storage timeout **[device]** -`AdbService.kt` wraps every `shell()` in one global `Mutex`, so `health_report`'s 6 `dumpsys`/ -`df` calls (fired in parallel by `tokio::join!`) execute strictly serially, each holding the -lock through its full read-to-EOF. `df` queued behind big dumpsys dumps blew its budget — the -reported regression. libadb's `AdbConnection` already multiplexes independent `AdbStream`s over -one socket, so concurrent `openStream` is supported by design. **Fix:** demote the global mutex -to a lifecycle-only lock (exclusive for connect/pair/disconnect; shared lease for shell/ -screencap so they fan out). *Verify concurrent `exec:` streams demux correctly on the Shield.* -The interim band-aid (generous 10s core timeouts + `%`-anchored parse, already committed) keeps -storage working until this lands. - -### T2. `exec:` hardcodes `exit_code = 0` — command-failure detection degraded **[device]** -The Kotlin `exec:` path returns `exitCode = 0` always (no stderr either). Any core logic -trusting `success()`/`exit_code` instead of `shell_reported_failure()` reads every mobile -command as succeeding. **Fix:** derive a real exit code (exec: doesn't carry one — either use -`shell,v2:` framing, or append `; echo $?` and parse, or make core rely only on -`shell_reported_failure()` on this transport). *Verify with a known-failing `pm`/`settings`.* - -### T3. Revert the mobile band-aids in shared core once T1/T2 land -`health.rs` (per-command timeouts + `/proc/meminfo` fallback + error swallowing) and -`apps.rs::list_other_packages_impl` ("run sequentially to avoid Mutex queuing races" + -timeouts) are compensation for T1. They double the transport's own 8s cap. **Keep** -`parse.rs::parse_storage_info` `%`-anchor (genuine cross-platform robustness, tested). Revert -the rest to the desktop-clean version after the mutex is fixed. - ---- - -## P1 — Missing flows the user explicitly asked for - -### F8 / T6. Saved-TV chooser + auto-reconnect on launch — entirely absent -No persistence anywhere (no localStorage, no store, no reconnect). Onboarding always starts at -`scan`. The design has a "Reconnect" screen + "Saved TV" tile; the RSA key is already persisted -Kotlin-side so re-auth is silent. **Needed:** persist paired devices (host/port/last-name/ -device_type) and add a launch screen listing saved TVs with an auto-reconnect attempt, falling -back to Scan. This is the intended first screen. - -### F9 / T5. Real device info after connect -Symptom: Connected/Dashboard show generic "Android TV"/"unknown". **Root cause (corrected):** -`list_devices_impl` *does* enrich via `harvest_properties` (batched `getprop`), and Onboarding -*does* call `list_devices` after connect — but `harvest_properties` silently swallows a -transport error and returns defaults (`name = "Android TV"`, `model = "Unknown Device"`). Under -the old `shell:` hang the getprop degraded to defaults. **Likely resolves now that `exec:` -works — [device] verify** that the batched getprop returns real props post-connect. Also: log -the swallowed error instead of hiding it, and use one canonical `deviceLabel` derivation -(prefer `properties.friendly_name`) — Onboarding and Dashboard currently derive the name -differently. - -### F10. `find_remote` is Nvidia-Shield-only but shown on every screen -`find_remote` runs `am start -n com.nvidia.remotelocator/...` (Shield-only); errors on Google -TV, and is silently swallowed on most screens. **Fix:** gate the button on -`device_type === "shield"`. - ---- - -## P2 — Build / distribution / polish - -### B1. `gen/` regeneration footgun (HIGH latent) -Hand-edits live in the regenerable `gen/android` tree: the icon set (mipmaps + adaptive vectors -+ anydpi-v26), `AndroidManifest` (App.kt name, leanback, cleartext, FileProvider), `strings.xml`, -`build.gradle.kts` (minSdk/compileSdk/applicationId). All tracked (survive in git) but a -`tauri android init` overwrites them. The libadb transport was correctly extracted to the -plugin because it *could* be; icon/manifest/strings are app-module-scoped and cannot. **Fix:** -inventory these hand-edits at the top of HANDOFF; keep the icon *source* outside gen and -regen with `tauri icon`; confirm `strings.xml` derives from `productName`. - -### B2. Material Symbols font is 5.34 MB (full set) — only 35 glyphs used -Subset with `pyftsubset`/`glyphhanger` → ~20-40 KB. The 35 used icons (grep `class="msr">`): -`add_circle, arrow_back, arrow_forward, auto_fix_high, cast, check, expand_more, fast_forward, -fast_rewind, hdr_on, help, home, keyboard, keyboard_arrow_{down,left,right,up}, lan, lock, -memory, menu, notifications_active, power_settings_new, settings, settings_backup_restore, -settings_remote, star, surround_sound, tune, tv_gen, verified_user, volume_off, volume_up, -wifi_tethering`. (Verify `tv_gen` resolves before subsetting.) - -### B3. No Android build in CI -CI (`v2-tests.yml`) covers Rust (all 4 crates via the desktop stub) + mobile svelte-check + -Vite build, but **never compiles the Kotlin/Gradle/APK**. A Kotlin error, bad JitPack coord, or -manifest-merge conflict passes green and only fails on a manual device build. **Fix:** add a -gated (paths/`workflow_dispatch`/nightly) `tauri android build --apk --target aarch64` job, or a -cheap Kotlin-compile-only job. - -### B4. Release readiness (M6) — not shippable -`bundle.active: false`; no signing keystore/config (unsigned release APK can't install); no -versionCode bump process; no release workflow/distribution. Release buildType has -`isMinifyEnabled = false` (no R8/resource shrink) and ships a universal APK (all 4 ABIs). -**Fix:** keystore + `signingConfigs.release` from `keystore.properties` + CI secrets; a mobile -release script (monotonic versionCode); an Android release workflow; enable minify + -per-ABI splits (arm64 only for real devices) — verify JNI/reflection keep-rules for libadb. - -### B5. Frontend polish (from the frontend pass) -Webview-blank-on-resume has no handler (add `visibilitychange` reload); `BottomTabs` lacks -`env(safe-area-inset-bottom)` so labels sit under the home indicator on gesture-nav devices; -missing empty/error states (Apps empty search, Dashboard retry); native `confirm()`/`alert()` -used (off-brand, blocking); overlay a11y (span-onclick close buttons, no focus trap/Esc, toggle -needs `role="switch"`); the screenshot toast claims "saved to device" but the result is -discarded. - -### B6. Security / safety — sound (LOW, informational) -The do-not-disable gate is centralized in `engine::safety` and **inherited by every mobile -destructive path** (disable/uninstall/launcher/snapshot/optimize) because mobile reuses the same -core commands — no mobile path runs `pm disable-user`/`pm uninstall` directly, and the safety -classify runs *before* `require_pro`. Snapshot path-confinement is inherited. Entitlement is -client-side (crackable), which is acceptable for a low-cost offline unlock; the safety gate is -independent of entitlement, so a cracked-Pro build still can't brick a device. Only follow-up is -M5 signed-license validation (revenue, not safety). **The HANDOFF note that Pro commands aren't -registered/gated on mobile is STALE — they are registered and gated; there are core tests -asserting ADB is never called on Free.** - ---- - -## Recommended order - -1. **F1** reboot arg (trivial). **F2/F3/F4** stop showing fake data as real (trust). **F10** gate - find_remote. **T4** reboot false-negative. — all safe, mostly type-checkable, no device needed. -2. **F8/T6** saved-TV persistence + reconnect launch screen — the headline missing flow. -3. **F6** shared session store (device + entitlement, null-guard); **F7/M4** shared typed api/types. -4. **T1** remove the global mutex → concurrent shells; then **T3** revert core band-aids; - **T2** real exit codes. — [device] verify. -5. **F9/T5** confirm device-info enrichment works over exec:; log the swallowed getprop error. -6. **B2** font subset; **B1** gen/ inventory; **B3** CI Android job; **B4** release/signing (M6); - **B5** polish. diff --git a/v2/mobile/BACKLOG.md b/v2/mobile/BACKLOG.md index 3e4eee2b..1005c0a8 100644 --- a/v2/mobile/BACKLOG.md +++ b/v2/mobile/BACKLOG.md @@ -1,321 +1,85 @@ # ATV Optimizer mobile backlog -Current owner release-polish findings and acceptance checks: [RELEASE-POLISH.md](RELEASE-POLISH.md). This supplement preserves the historical backlog below. - -Updated 2026-09-05 America/Chicago (lifecycle evidence/current authority). Bryan's -feedback records a debug build from `main` at `44d2d66` installed on the Pixel; -this is owner-reported use, not a completed physical stability matrix. -`FEATURES.md` and `ARCHITECTURE-REVIEW.md` are historical audits; use this file, -`HANDOFF.md`, and `USER-FEEDBACK-2026-09-05.md` for current work. Track execution in -`bd`, including restart feedback #7 on `so-vtm.8`. - -**Scope note (corrected 2026-09-16):** the old host-only / no-commit restriction is dead — see -`CLAUDE.md`, work is committed and pushed continuously. What remains real is the *device* gate: -nothing in `v2/mobile` has been verified on a phone or TV since `44d2d66`. Accepted Navigator work is committed and pushed under -the September 10 policy in `AGENTS.md`; physical items below remain gated. The -older installation instructions are not current permission. - -## Desktop parity pass (2026-10-06) - -Shipped to mobile on `mobile-parity-pass` (details in `HANDOFF.md`): Optimize search and app -sheet, Diagnostics suggestions, snapshot preview Now column, Apps Recognised section with -storage details, Report this app (#100) and long-press menu (#129), Launcher Setup Wraith card, -set-any-app-as-Home and separate stock disable (#157/#158), the Tweaks tab at desktop parity, Files -app-backup finder and TV-side delete (shared core, desktop #86 fix), and links opening in the -phone's browser. **Nothing here has run on a phone or TV.** Browser suites pass (207/207). - -Remaining parity gaps, in rough order: - -1. Device pass of everything above on the Pixel against a Shield and a Google TV. -2. Phone→TV upload in Files — needs an Android document picker (folds into P1.3 SAF). -3. Install APK / sideload from the phone. -4. Bug-report bundle (`collect_diagnostics`) with an explicit share step. -5. Patch-notes viewer. -6. List `androidx.browser` (pulled in by the opener plugin) and the other Gradle dependencies in - `scripts/notices-header.md`. - -## Correctness checkpoint - -Implemented in the current checkpoint: - -- Bind every normal `WirelessAdb` operation to its requested serial while holding the connection - lock, including transfer, screenshot, reboot, and raw disconnect paths. -- Serialize connect/disconnect lifecycle transitions so a slow connect cannot resurrect after a - manual disconnect. -- Give the liveness shell probe transport-level timeouts; cancelling a blocking task no longer - leaves the connection mutex held indefinitely. -- Treat reboot as successful only after adbd acknowledges the reboot service, and keep pre-ack - transport failures as failures. -- Keep local push/pull filesystem errors from evicting a healthy TV connection. -- Await manual disconnect before returning to onboarding and suppress the resulting one-shot - saved-TV auto-reconnect. -- Fully clear per-device caches on disconnect/switch and discard health/bloat responses that arrive - for a device that is no longer current. -- Fail destructive app controls closed until the canonical safety lookup resolves, and prevent a - late lookup for one package from changing another package's controls. -- Display `cross_device_warning` in the snapshot confirmation UI. -- Redact remote text and secret-bearing fields from frontend debug logs. -- Persist a valid license before changing the live entitlement. -- Make Optimize actually apply the selected audited plan one command at a time, report partial - failures, and run the performance-settings post-pass. -- Use shell-v2 framing on direct TCP devices so stdout, stderr, and exit status are real. The - Bedroom Shield live gate returned separate `out`/`err` streams and exit code 7; older devices - that reject shell-v2 retain the shell-v1 compatibility fallback. -- Fix GitHub #86 by searching SmartTube's current `/sdcard/Documents/SmartTubeBackup` export - location while retaining its legacy app-data location, and discard stale desktop file-browser - directory responses. -- Complete fast-remote failure cleanup on Android: control/server streams are dropped on blocking - workers on every handshake branch, failed starts kill the resident server, hold-repeat ticks are - bounded instead of accumulating behind a slow fallback, and `find_remote` now respects real - shell failure/exit status. -- Normalize malformed saved-TV records (including the legacy `googletv` spelling and invalid - dates), show an explicit stale-data warning after Diagnostics refresh failure, isolate safety and - Tweaks loads by device/request, capture the mutation target serial, and reload Tweaks after any - possibly-partial write failure. -- Store every APK part in a versioned, manifest-backed backup bundle; restore complete bundles with - `pm install-multiple`, path-confine every restore read, clean remote staging on every result, and - label older base-only backups as incomplete instead of offering an unsafe restore. -- Subset the bundled Material Symbols font reproducibly from Svelte references (5.34 MB to about - 111 KB) and disable autocorrect, spellcheck, and automatic capitalization for license keys. -- Retain fast-remote sessions for a 30-second Android background grace period, cancel stale cleanup - timers on resume, and serialize remote startup with teardown so an expired timer cannot kill a - newly started session. -- Allow 30 seconds for the TV authorization prompt and replace raw transport errors with actionable - retry/"Allow debugging?" guidance. Preserve the last real friendly name across rotating ADB ports, - retain 16 recent TVs, expose previous TVs in the Dashboard switcher, and chart used rather than - free RAM in Diagnostics. -- Prefer the durable cached friendly name in the live session label when a newly connected TV only - reports a generic model. A device-less 384×812 interaction pass verified switching, connection - guidance, and the used-RAM bar; the resulting `3eccc34` follow-up still needs installation and a - focused physical UI spot-check. - -## Saved-TV identity audit fixes (2026-09-30) - -Fixed GitHub #115–#118, all found by a code-read audit. Each repro is now a test in -`tests/savedDevices.test.mjs`, `tests/discoveryRows.test.mjs` and `tests/session.test.mjs`; -**none has run on a phone or TV.** - -- One hardware-id rule (`lib/identity.ts`, matching desktop `idKey`): trim, and empty or any - casing of `unknown` is no id. Stored placeholder ids migrate to id-less rows, each keyed by its - own stable random local id rather than its bare endpoint (see #146 below), so saved-TV list keys - are unique and Forget removes exactly one TV (#116). A placeholder is no longer a wildcard for - `adb-unknown-*` adverts. -- `cachedDeviceName` matches on verified id, or an id-less row at the exact endpoint; an - identified row never names a TV that reports no id. Advertised serials match only exactly or - with adbd's six-character suffix, so `shield` no longer verifies `adb-shield-a` (#117). -- Scan rows are built per stored TV, never per address; no synthesized "Saved TV" row (#115). -- `loadHealth` cannot wedge after its device vanishes mid-load, Cancel no longer shows a - reconnect failure, and Diagnostics keys its safety lookup on the sorted package set (#118). - -## Id-less saved TVs stay distinct (2026-10-01) - -Fixed GitHub #146, raised by Codex on PR #144 and declined there as an edge case at the time. -Two id-less saved TVs (no hardware id) that had shared one `host:port` used to collapse into a -single storage row on migration/read -- the older one silently discarded -- because every id-less -row was keyed by its bare endpoint. Fixed in `savedDevices.ts`/`identity.ts`/`discoveryRows.ts`: - -- Every id-less row gets a stable random `localId` the first time it is persisted; `savedDeviceKey` - keys on that instead of the endpoint, so two id-less TVs stay two rows no matter what address - they shared. Existing stored rows are migrated on first read without losing any. -- `rememberDevice` only refreshes an existing id-less row when exactly one saved row matches the - connecting endpoint. When two or more already share it, which one just reconnected is unknowable, - so the connection is deliberately not persisted against any of them -- it writes nothing, rather - than either overwriting one of them with a guess or saving a new row on every repeat reconnect - (the latter would eventually evict a genuine saved TV once `MAX` is reached). -- A shared `savedDeviceIsLiveConnection` check (`identity.ts`) is the one place that decides - whether a saved row is unambiguously the live connection: always true for a verified hardware id, - true for an id-less match only when it is the single id-less row at that endpoint. Both - `buildDiscoveryRows` and the Devices screen's "Other TVs" filter use it, so neither marks more - than one id-less saved row "connected" at a shared live endpoint, and neither hides an ambiguous - row (and its Forget control) from the list -- the discovery row falls back to a generic name and - both saved rows report "saved-address" instead of one of them claiming the connection. - -Covered by new tests in `tests/savedDevices.test.mjs` and `tests/discoveryRows.test.mjs`; none has -run on a device. - -## A different id-less device can no longer inherit a saved TV's row (2026-10-01) - -Fixed GitHub #154, raised by Codex on PR #152 (the #146 fix above) and deferred there as an edge -case needing a product decision. #146 fixed the case where *two or more* id-less saved rows already -shared an address; it did not change the original, more common case: when exactly **one** id-less -row sits at an address, `rememberDevice` still trusted that lone match on endpoint alone, so a -*different* id-less TV that later answers at the same address (DHCP reassignment, a replaced -device) silently inherited the old row's name and `localId`. - -Without a hardware id the app still cannot prove identity, so the fix is a soft signal, never a -promotion to "verified": - -- Every id-less saved row now carries an optional `fingerprint` (`model`, `manufacturer`, - `deviceCodename`, and the TV's own user-set `friendly_name`), captured from the live device's - reported properties (`identity.ts`: `deviceFingerprintOf`). Hardware-identified rows don't carry - one -- the id is already verified, so there's nothing for a fingerprint to add. -- On a lone id-less match, `fingerprintMismatch` compares saved vs. live `model`/`manufacturer`. - A clear disagreement means `rememberDevice` sets the match aside and records the connection as a - new, distinct row instead of refreshing the old one (`savedDevices.ts`). The old row is left - untouched. A missing field on either side (an older row saved before this existed, or a device - that reported nothing) is unknown, never a mismatch -- it does not block the match, so existing - rows migrate for free with no separate migration step. -- Once two id-less rows share an endpoint this way, the existing #146 ambiguity rule - (`savedDeviceIsLiveConnection`) already refuses to call either one "connected" or let a further - reconnect silently refresh either -- no new ambiguity-handling code was needed there. -- `session.svelte.ts` surfaces the mismatch as `session.identityNote` ("A different device is now - at this address."), read and cleared once by whichever screen's connect flow notices it - (Dashboard's `onMount`/`switchDevice`, Devices' `reconnect`/`reconnectCurrent`) instead of silently - going unmentioned. - -Covered by new tests in `tests/savedDevices.test.mjs` (verified to fail before the fix); none has -run on a device. - -**Codex review follow-up on PR #155 (same day, two rounds):** three valid findings, all fixed. - -Round 1: - -- `recoverOrMarkLost()` (the silent one-shot reconnect `checkLiveness()` triggers when the cheap - liveness probe fails) redialed the same host:port directly and never ran the identity check - above, so a different id-less TV that took over mid-session during a silent recovery was - trusted without comparison. It now calls `rememberCurrentDevice()` on a successful recovery, - before flipping liveness to `"live"` -- the same check an explicit reconnect gets. -- Devices' `reconnect(d)` cleared `session.identityNote` and showed its toast locally, then - immediately navigated to Dashboard -- the toast never had a chance to be seen. - -Round 2 (a sharper version of the same finding): the round-1 fix for the second point routed the -note through Dashboard's `onMount`, but a silent recovery can land while the user is already -sitting on Dashboard (or any other screen) with no mount event to trigger it -- the note would set -but nothing displayed it until the user happened to leave and come back. Fixed by making the -display global instead of per-screen: `App.svelte` now renders `session.identityNote` directly in -a `Toast` at the root (reactive to the runes store from wherever it's set, with its own 4-second -self-clearing `$effect`), and Dashboard/Devices no longer read or clear `identityNote` themselves --- they only suppress their own misleading "Connected"/"Reconnected" success toast when a note is -pending, matching the existing `savedHostHasMultipleIdentities` ambiguity-messaging pattern. - -Covered by Playwright cases in `tests/session.test.mjs`, each verified to fail before its fix, -including one that stays on a non-Dashboard screen throughout to prove the note still surfaces -without any navigation or remount. - -## Stability reset (2026-09-04) - -A four-track audit (feature parity vs v1/desktop, connection lifecycle, screen UX, Rust backend) -and fix sweep. Landed: - -- Transport: bounded reads everywhere (30 s shell inactivity, 180 s installs, 120 s vendored - safety net, 5 s TCP connect), socket-level-only eviction with a post-error probe, identity mirror - outside the device mutex, poison-tolerant locks, no second disconnect from the status probe, - shell-v1 exit normalization, PNG-first screenshots with a separate stderr sink. Vendored - `adb_client` skips stray foreign-stream packets, bounds-checks sync payloads, rejects a zero - local id, cannot spin on a framebuffer overshoot, and no longer compiles the rustls key log. -- Core: one sentinel-batched shell per read path (`adb/batch.rs`) for health, package lists, - optimize plan, launchers, display scaling; entitlement default fail-closed to Free. -- Connection UX: `Connection to the TV was lost` errors flip the session from any screen, one - silent redial before the global `ConnectionBanner` (Retry / Switch TV), 45 s visible heartbeat, - saved-TV picker on launch (auto-dial only with one saved TV and no prior explicit Disconnect), - separate `addtv` route, per-row dialing state, mDNS names in scan results, honest pairing copy, - connect ordering that never pairs an old name with a new host, generation guards on - refresh/liveness, host cleared on reset, back-button history kept to one spare entry. -- Screens: Dashboard shows the real `N of M` bloat count (no floored score), inline screenshot - preview, outside-tap dropdown dismissal; Settings (renamed from More Options) grouped, with - Emergency recovery (`panic_recovery` was registered but unreachable), recovery/bootloader reboot, - and Disconnect confirmation everywhere; Devices no longer runs a duplicate health sweep and its - Reconnect is guarded; one safety vocabulary (`lib/safety.ts`, exactly the three core kinds) used - by Optimize, AppDetailSheet, Diagnostics, RiskGuide; Optimize renders core tiers (not catalog - risk), rounded MB with honest "RAM in play" wording, Optimize/Restore mode, cancelable apply with - tabs locked, shared PaywallSheet; Apps debounced search, reinstall-existing after uninstall, - caution confirm on Disable; Diagnostics real live refresh, force-stop/disable on top memory, - explicit no-TV state, no fabricated names or thresholds; Tweaks tri-state (on/off/unset) - toggles, all four HDMI-CEC settings, 3-way frame-rate control, reset-to-default per setting, - per-card errors, confirmed display scaling with the override size shown; Launcher no longer - fabricates a DEFAULT badge, invalidates caches after changes, and warns when the channel - provider is disabled; Snapshots pin the serial the plan was built against and show a real plan - breakdown; Remote gains volume-down and wake, confirms power, shows send-text errors in the - modal and a queued-press pill; Files guards stale listings and resets on TV switch; Backups - lists every app, can delete a backup (`delete_backup`, path-confined), and drops the fake - Drive tile; overpromising copy ("every change is reversible", "roll back instantly", "or a - snapshot restore", "Tap a file to save it") corrected. - -Verified device-less: all workspace gates green (fmt, clippy on 4 crates + aarch64 Android -clippy, 165 core tests, 18 mobile tests, 27 vendored tests, `npm run check` 0/0, `npm run -build`), plus a 384×812 Playwright pass with Tauri stubbed covering scan, the two-TV picker (no -auto-dial), single-TV auto-dial, no auto-dial after an explicit Disconnect, connect-failure -guidance, lost-connection probe/recovery, Emergency recovery, and the back-button stack. -Those were the 2026-09-04 host results; later owner-reported `44d2d66` use does not -complete the physical matrix. - -## P0 — next correctness work - -1. **Resolve restart feedback #7 with owner-driven lifecycle evidence (`so-vtm.8`).** - Host review is complete at `48cba23`: 13 focused browser cases and 9 existing regressions - pass; mobile check/build and 3 pure lifecycle tests pass. See - [`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md) for exact scope, reproducible commands, - and the physical gate. Visibility-only resume retains the screen; fresh JavaScript - starts over. This does not identify Bryan's leaving action or Android lifecycle event. - No session-persistence fix is authorized from that evidence alone. - Separately, mechanic accepted `so-vtm.3` navigation locally: saved-TV success - opens Dashboard directly after profile resolution (20 worker browser tests). - Navigator did not rerun that candidate; baseline interstitial observations do - not apply to its repaired saved-TV path. Physical/publication gates remain. - - **Physical stability follow-up (gated):** once separately authorized, first identify - the installed build and capture exact Home/Back/Recents/recreation evidence. Do not - assume the APK predates the sweep, change it before recording the report, or reuse old - pairing endpoints. The broader real-Shield matrix also remains open: (a) put the TV to sleep or turn off Wi-Fi - mid-session and confirm the app flips to "Reconnecting…" then the banner within ~45 s instead of - staying green; (b) confirm a one-TV launch names the TV and can be cancelled, and a two-TV launch - waits for a choice; (c) Disconnect, kill the app, relaunch — it must not redial; (d) run Optimize - apply and confirm tabs lock and Cancel stops after the current item; (e) Emergency recovery on a - TV with a few disabled apps; (f) back button from Settings → Devices → back → back reaches the - dashboard, and one more press leaves the app; (g) Screenshot preview renders; (h) Tweaks shows - "Unset" rather than OFF for a never-written setting; (i) Settings › Licensing shows "Debug build - test key" after activating the dev key, and Settings › About opens the third-party notices. -2. **Verify code pairing end-to-end** per `PAIRING-PLAN.md` (host binary against the Pixel's - pairing service first, then a Google TV; wrong code must fail cleanly; a paired TV must connect - afterwards without an "Allow debugging" prompt). Pairing is implemented but has never talked to - a real adbd. -3. **Finish the exact fast-remote device gates.** Living Room already passed concurrent diagnostics, - file list/pull, and SHA verification while the channel was live. Repeat concurrency on Bedroom; - on both Shields test hold, live-session background/resume before and after 30 seconds, sleep/wake, - and reboot cleanup. Take a fresh controlled Living Room latency sample because one earlier p95 - was 108 ms even though later samples were 98 and 87 ms. - -## P1 — required product features - -1. Finish the remaining fast-remote Phase 4 matrix in `FAST-REMOTE-PLAN.md`: Bedroom concurrency, - holds, sleep/wake, reboot, live-session background/foreground, reconnect, and the fresh Living - Room latency sample. -2. **Verify** the new Android 11 wireless-debugging code pairing on a real device (implemented - 2026-09-04, unverified end-to-end; follow the manual test in `PAIRING-PLAN.md`: Pixel pairing - service from the host first, then a Google TV, including the wrong-code and silent-reconnect - checks). -3. Add Android SAF import/export and user-selected push destinations (this is also what unblocks - phone→TV upload in Files), then consider Google Drive sync for complete APK bundles. -4. Add panic recovery, advanced reboot, permission/app-op controls, reinstall-existing, and a - deliberate performance/correctness pass across Launcher, Tweaks, Files, and Backups. -5. ~~Signed commercial licensing~~ — done 2026-09-04 (`crates/core/src/license.rs`, - `tools/atvopt-license`, `mobile/LICENSING.md`). Remaining: decide the store/checkout that - issues keys, and back up `~/.atvopt/license-signing-key.prod`. -6. Follow-ups surfaced by the 2026-09-04 audit: - - ~~snapshot.rs batching~~, ~~`remote_warm`~~, ~~`pull_file` cap + de-dupe~~, ~~saved TVs - keyed by `ro.serialno`~~ — done 2026-09-04. - - Product decision: let free users see the Optimize plan read-only (core `prepare_optimize` - is Pro-gated today, so the flagship tab is a lock card for free users while the Dashboard - already shows the count for free). - - Product decision: `Feature::FileManager` / `Feature::BackupClone` exist in core but gate - nothing; either wire them or delete them. - - Screenshots and pulled files land in app-private storage with no export; SAF (P1.3) is - what makes them useful. +The ordered queue of remaining mobile work. [`HANDOFF.md`](HANDOFF.md) describes how the app works +today; this file lists only what is left. Nothing in `v2/mobile` has been verified on a phone or TV +in its current form, so most of P0 is physical-device work. Device sessions need the owner's +go-ahead; don't infer permission from a phone being powered on. + +## P0 — physical device gates + +1. **Device pass of the current build** on the Pixel against a Shield and a Google TV. One session + should cover, in order: confirm the installed build, the #105 capture (item 2), then + [#93](https://github.com/bryanroscoe/shield_optimizer/issues/93) saved-TV discovery and + reconnect: a legacy `:5555` Shield without mDNS shows one Connected row; a Google TV whose port + rotates shows the other-port state with no duplicate; an address change never carries a cached + name or identity to a different TV; reconnect results are honest. Then the stability matrix: + - put the TV to sleep or turn off Wi-Fi mid-session: the app flips to "Reconnecting…" and then + the banner within about 45 s instead of staying green; + - a one-TV launch names the TV and can be cancelled; a two-TV launch waits for a choice; + - Disconnect, kill the app, relaunch: it must not redial; + - Optimize apply locks the tabs, and Cancel stops after the current item; + - Emergency recovery on a TV with a few disabled apps; + - Back from Settings → Devices → back → back reaches the Dashboard, and one more press leaves + the app; + - the screenshot preview renders; Tweaks shows "Unset" rather than OFF for a never-written + setting; + - Settings › Licensing shows "Debug build test key" after activating the dev key, and + Settings › About opens the third-party notices. + + And the UI checks that browser emulation cannot settle: system bars, cutout, rotation, gesture + and three-button navigation, and the keyboard opening and closing (no overlap, no doubled + padding); Android font scaling and pinch zoom on the Diagnostics header, display-scaling + presets and reports; touch scrolling and focus in app sheets and nested dialogs; settings + readback on real vendor firmware (Private DNS, background-process cap, screensaver names, + density-only display overrides); RAM, storage and thermal readings against the TV's own + figures; the setup-helper disable rollback; and stock-launcher protection on a Fire TV build + ([#205](https://github.com/bryanroscoe/shield_optimizer/issues/205)). +2. **The app-restart report** ([#105](https://github.com/bryanroscoe/shield_optimizer/issues/105)). + Follow the physical gate in [`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md): name the exact + leaving action and correlate it with the Android lifecycle before changing any persistence. +3. **Code pairing end to end** per [`PAIRING-PLAN.md`](PAIRING-PLAN.md): a host binary against the + Pixel's own pairing service first, then a Google TV from the app. A wrong code must fail + cleanly, and a paired TV must then connect with no "Allow debugging" prompt. +4. **Fast-remote Phase 4** per [`FAST-REMOTE-PLAN.md`](FAST-REMOTE-PLAN.md): concurrency on the + Bedroom Shield; on both Shields hold-to-repeat, live-session background/resume before and after + 30 seconds, sleep/wake and reboot cleanup; and a fresh controlled Living Room latency sample. + +## P1 — product work + +1. **Android export.** Screenshots, pulled files and APK bundles land in app-private storage with + no way out. Start with one explicitly selected screenshot (Save a copy or Share), scoped in + [`EXPORT-DISCOVERABILITY.md`](EXPORT-DISCOVERABILITY.md); then SAF import, which also unblocks + phone→TV upload in Files; then consider Google Drive for complete APK bundles. +2. Install APK / sideload from the phone. +3. The bug-report bundle (`collect_diagnostics`) with an explicit share step. +4. A patch-notes viewer. +5. Product decisions for the owner: + - Let Free users see the Optimize plan read-only. Core `prepare_optimize` is Pro-gated, so the + flagship screen is a lock card for Free users while the Dashboard already shows them the + count. + - `Feature::FileManager` / `Feature::BackupClone` exist in core but gate nothing; wire them or + delete them. + - The launcher icon reads slightly top-heavy. A 1.8 px downward optical shift is mocked in + `evidence/settings-polish-2026-10-17/icon-optical-comparison.png`; the assets are unchanged + pending a preference. +6. Commerce per [`BILLING-IMPLEMENTATION-PLAN.md`](BILLING-IMPLEMENTATION-PLAN.md) and bounded Free + undo per [`FREE-SETTINGS-UNDO-DESIGN.md`](FREE-SETTINGS-UNDO-DESIGN.md). Neither is built. ## P2 — release readiness -- ~~Android signing config, versionCode tooling, per-ABI docs~~ — done 2026-09-04 - (`mobile/RELEASE.md`). Still open: create the real keystore (or enroll Play App Signing), a CI - job that builds a signed AAB, and a first signed release APK size measurement. -- ~~Third-party notices~~ — generated (`mobile/THIRD-PARTY-NOTICES.md`, in-app under Settings › - About; regenerate with `scripts/gen-notices.sh`). Still open: a legal/privacy review pass; note - five MPL-2.0 crates (unmodified, fine) and the Unicode-3.0 family were accepted. -- Complete the accessibility/navigation review. -- Reconcile the mobile feature matrix with actual command behavior and remove overpromising copy - such as “exact rollback,” “automatic snapshots,” or “fully reversible” where the implementation - cannot guarantee it. - -## Separate scope - -Desktop rebranding remains separate because changing the Windows product identity can orphan -existing MSI installations. Do not mix it into the mobile release track. +See [`RELEASE.md`](RELEASE.md) for the full checklist. Open items: + +- Create the real keystore (or enroll Play App Signing), add a CI job that builds a signed AAB, + and measure the first signed release APK. +- Resolve the Android release runtime dependency graph and its notices + ([`ANDROID-DEPENDENCIES.md`](ANDROID-DEPENDENCIES.md)); the declaration-level list is not enough. +- A legal and privacy review. The Cargo notices accept five unmodified MPL-2.0 crates and the + Unicode-3.0 family. [`DATA-INVENTORY.md`](DATA-INVENTORY.md) is a draft, not a published policy. +- Choose the store or checkout that issues license keys, and back up + `~/.atvopt/license-signing-key.prod`. +- Complete the accessibility and navigation review. +- Many installed apps are uncatalogued. Classify existing review coverage per package, model and + version from the shared `crates/core/data/app-lists/` before asking for new device reads; unknown + stays Unknown, and every catalog change is individually sourced and safety-tested. + +Whole-app pinch zoom stays enabled by the owner's choice; no viewport restriction is planned. diff --git a/v2/mobile/BILLING-COMMAND-MATRIX.md b/v2/mobile/BILLING-COMMAND-MATRIX.md index 6f2e5d81..66dd862e 100644 --- a/v2/mobile/BILLING-COMMAND-MATRIX.md +++ b/v2/mobile/BILLING-COMMAND-MATRIX.md @@ -1,6 +1,6 @@ # Current mobile command and UI entitlement matrix -Audited base: `f7897c1`. This is current behavior, not a pricing/gate migration. All 77 registered/invoked commands are listed. Free means no Pro entitlement prerequisite; connection, permission, safety and path restrictions still apply. Signed-key activation remains validated, not an unrestricted unlock. +This is current behavior, not a pricing/gate migration. Every registered/invoked command is listed; `BILLING-COMMAND-MATRIX.json` holds the same rows, and `tests/entitlement-preview.test.mjs` fails when a command is registered or removed without updating it. Keep this table in step with the JSON. Free means no Pro entitlement prerequisite; connection, permission, safety and path restrictions still apply. Signed-key activation remains validated, not an unrestricted unlock. Callers name actual API call sites; `session.svelte.ts` mediates Onboarding/Devices connections and Dashboard/Diagnostics caches. Screens can show Pro badges without blocking navigation. An unused API wrapper is explicitly shown as having no direct caller. @@ -93,32 +93,22 @@ Callers name actual API call sites; `session.svelte.ts` mediates Onboarding/Devi - Settings, permission and Home before-value restore remain gated or incomplete. A bounded journal/undo mechanism needs separate review. This slice creates no persistent journal and does not make arbitrary writes free. - FileManager/BackupClone enum labels do not imply enforcement: mobile file/backup handlers currently have no Pro prerequisite. No gate changes are proposed here. - Remote basic input, file paths, package names, safety checks and connection-generation fencing remain independent safety boundaries. -- The new entitlement preview is isolated and unused by session/backend authorization; store purchase callbacks alone cannot grant real Pro. No real billing adapter or service exists in this slice. +- The new entitlement preview is isolated and unused by session/backend authorization; store purchase callbacks alone cannot grant real Pro. No real billing adapter or service exists. -## Slice boundary and validation +## Entitlement preview boundary `entitlementPreview.ts` is a pure, ephemeral reducer. `EntitlementPreview.svelte` is an isolated presentation component and is not imported into the running app. Only the test harness mounts it; the simulated store lives under tests and cannot emit a verified verdict. Neither modifies `session.entitlement`, AppState, license files, TV state or purchase history. Verified/revoked reducer events are simulation inputs, not an implemented security boundary or provider verdict. -No persisted entitlement, purchase verification service, account system, product, price, payment -call, offline cache policy, tip URL or before-value journal is added. Production integration -requires the separately reviewed Play verification and free-undo design. +There is no persisted entitlement, purchase verification service, account system, product, price, +payment call, offline cache policy, tip URL or before-value journal. Production integration +requires the separately reviewed Play verification ([`BILLING-IMPLEMENTATION-PLAN.md`](BILLING-IMPLEMENTATION-PLAN.md)) +and Free undo ([`FREE-SETTINGS-UNDO-DESIGN.md`](FREE-SETTINGS-UNDO-DESIGN.md)) designs. -Run `npm test`, `npm run check` and `npm run build` in mobile. Shared-core tests -`free_user_can_` exercise actual recovery implementations against mock ADB after entitlement -downgrade, not physical devices. The emergency recovery wrapper now delegates unchanged behavior -to a private helper so the Free-state path can be called in a unit test. No command gate changes. -The matrix coverage test detects added/removed registered commands; it is not a substitute for -reviewing future gate changes or auditing every indirect call chain. - -The approved product direction is documented separately in PR170: free compatibility checks, -$2.99 one-time Pro, no timed trial, optional benefit-free tips and normal Play promos for existing -supporters. This slice does not configure any of those products or change current gates. - -Local evidence: 450 shared-core tests and strict core Clippy pass; integrated mobile full suite 224/224 -(including cancellation and Free-recovery UI checks) passes. -Mobile check reports 0 errors / 0 warnings; build, Rust formatting and diff checks pass. These are -Mac host/mock/browser results, not Android transport or purchase-validation evidence. The branch -preserves merged release-preparation ancestry `7fe0e6a` through normal merge `a75605c`. +Shared-core tests named `free_user_can_` exercise the real recovery implementations against mock +ADB after an entitlement downgrade (not physical devices). The emergency recovery wrapper delegates +to a private helper so the Free-state path can be called in a unit test. The matrix coverage test +detects added/removed registered commands; it is not a substitute for reviewing future gate +changes or auditing every indirect call chain. diff --git a/v2/mobile/BILLING-IMPLEMENTATION-PLAN.md b/v2/mobile/BILLING-IMPLEMENTATION-PLAN.md index 30155f8f..ec2bbd71 100644 --- a/v2/mobile/BILLING-IMPLEMENTATION-PLAN.md +++ b/v2/mobile/BILLING-IMPLEMENTATION-PLAN.md @@ -1,8 +1,8 @@ -# Android commerce implementation plan — 2026-10-16 +# Android commerce implementation plan -Status: draft plan only. No purchase code, product, account, credential, backend, payment or tip -link is configured by this change. Base: merged mobile safety work `f7897c1`. Release-preparation -PR169 remains a separate review. Nothing here authorizes a store submission or real charge. +Status: draft plan, not built. No purchase code, product, account, credential, backend, payment or +tip link is configured. Today's mobile Pro unlock is the signed offline license key in +[`LICENSING.md`](LICENSING.md). Nothing here authorizes a store submission or a real charge. ## Approved product direction @@ -146,7 +146,7 @@ claimed complete until ownership is reverified. Do not promise lifetime offline Tips remain disabled until approved; prove no benefit/entitlement change when tipped. A tip exception is not assumed merely because a button says "support". -## Official references checked 2026-10-16 +## Official references - Purchase integration: https://developer.android.com/google/play/billing/integrate - Verification/ack/voided purchases: https://developer.android.com/google/play/billing/security diff --git a/v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md b/v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md deleted file mode 100644 index 232f0586..00000000 --- a/v2/mobile/CORRECTNESS-AUDIT-2026-09-05.md +++ /dev/null @@ -1,152 +0,0 @@ -# Repository correctness audit — 2026-09-05 - -> **Snapshot, 2026-09-05.** Superseded by [`HANDOFF.md`](HANDOFF.md) §2; counts and file states -> below are pinned to that date. - -## Fix pass — 2026-09-05 - -All nine findings below are addressed in the working tree. The original findings are retained as the reproduction record. Desktop/shared-core and mobile were in scope; v1 was not. The other agent's handoff/backlog/cloud-task files remain untouched. - -- Optimize navigation is locked centrally, including hardware Back; Cancel remains reachable, and the apply loop stops if its session changes. -- Remote input captures its target and generation, discards queued work on destruction/session change, and caps ordinary taps at eight pending/in-flight presses. -- Native connection publication is guarded by monotonic request IDs. Cancellation invalidates an unfinished handshake and removes a just-published canceled connection without dropping a newer connection. Onboarding uses per-attempt IDs for same-host retries. Profile refresh accepts only the expected authorized endpoint. -- Health and profile transport failures propagate; the frontend preserves the previous health report with an error. Recovery is limited to one automatic attempt per loss episode. Late errors from older generations cannot start recovery on the new TV. -- Downloads use a size-limited writer and a dedicated socket within the existing WirelessAdb driver, so a rejected local write does not evict the main control socket. Temporary files are removed on failure; successful downloads are published without overwriting existing files. -- Apps, Optimize, snapshot planning, and Launcher require successful installed/disabled read sections. Batches carry individual exit statuses and completion markers; optional HOME/memory reads may fail without inventing package state. -- Reboot completion uses the initiating generation and no longer schedules an unscoped delayed disconnect. - -Regression coverage now lives in `tests/session.test.mjs` and the affected Rust modules. CI runs the mobile browser suite, the entire Rust workspace (including the license CLI), and the excluded vendored transport tests. Third-party notices were regenerated after promoting tempfile to a runtime dependency. - -Validation for the fix pass: 9 browser regressions passed; core 186, mobile 26, desktop 35, and license CLI 2 Rust tests passed; vendored transport 54 passed with 1 device-dependent test ignored. Workspace formatting/Clippy passed. Desktop and mobile Svelte checks reported 0 errors/0 warnings and both production builds passed. Android aarch64 Clippy and an arm64 debug APK build also passed. These are local results; cross-OS CI and physical phone/TV stability, pairing, and background/resume tests still need to run. - -PowerShell clarification: `pwsh` could not be located through PATH, common install locations, or Spotlight on this machine. The initial audit's claim that it was not installed was stronger than the evidence established. - -## Original audit - -Reviewed HEAD `34d0575` on `feat/atv-optimizer-mobile`. This was a review, not a fix sweep. No production source was changed. `HANDOFF.md` and `BACKLOG.md` were left to the other agent. - -The mobile app still has reproducible lifecycle bugs despite passing the host gates. The strongest findings concern commands surviving navigation, connection cancellation that only affects the frontend, and backend failures being converted into successful responses. These plausibly explain reported symptoms; no physical device was used to attribute a particular user incident to them. - -## Findings - -### 1. P1 — Back abandons Optimize's controls while mutations continue - -Sources: `src/lib/router.svelte.ts:107`, `src/screens/Optimize.svelte:225`, `src/components/BottomTabs.svelte:28`. - -The tab buttons honor `session.applyInProgress`, but the hardware-back/popstate handler calls `router.back()` unconditionally. Optimize's asynchronous apply loop survives component destruction. Its destroy hook only clears a toast timer. - -**Browser reproduction:** at 384×812, supply a two-item plan through a stubbed Tauri bridge, start Apply, and hold the first disable response. Call browser `history.back()` (the event path used by the Android handler). The screen becomes Dashboard, `applyInProgress` remains true, and no Cancel button exists. Release the first response: the second `disable_package` is still invoked. - -**Required behavior:** enforce navigation policy centrally while an operation owns the session, or move the operation into an application-level controller with persistent progress/cancel controls. Cover hardware back, connection-banner navigation, and component destruction. - -### 2. P1 — Queued remote presses can be sent to a different TV - -Sources: `src/screens/Remote.svelte:36`, `src/screens/Remote.svelte:90`, `src/screens/Remote.svelte:170`. - -The queue captures a function that reads `session.serial` when it eventually executes, rather than capturing the target at the time of the press. Destroying the screen stops repeat timers but does not cancel queued work. The backend's serial validation cannot protect against this: the frontend supplies the new TV's valid serial. - -**Browser reproduction:** hold the first Volume up invocation for A, click Volume down, navigate away, change the active session to B, then resolve the first invocation. Recorded calls: - -```text -send_key { serial: "A:5555", key: "volume_up" } -send_key { serial: "B:5555", key: "volume_down" } -``` - -**Required behavior:** capture serial and connection generation at enqueue time; discard pending input on navigation, disconnect, or generation change. Bound ordinary queued presses as well as repeat ticks. - -### 3. P1 — Device profiling can report a live device after losing its socket - -Sources: `../crates/core/src/commands/devices.rs:51`, `../crates/core/src/commands/devices.rs:278`, `src/lib/session.svelte.ts:113`. - -`list_devices_impl` reads the initial device list, then profiles each device. If that shell fails, `harvest_properties` returns default properties rather than an error. On mobile the failure may have just evicted the connection, but `list_devices_impl` still returns the original `status: device` entry. The frontend treats any returned device as live and can display a successful connection with a generic name and unknown type while the transport is disconnected. This also bypasses the API-level lost-connection signal because the command resolved successfully. - -**Evidence:** source trace through the actual mobile eviction path, shared profile function, and frontend connect/recovery path. Not a physical-device reproduction. - -**Required behavior:** propagate transport loss; distinguish unavailable optional properties from a disconnected device. Publish a coherent connection identity and profile instead of accepting a stale device-list entry after profiling fails. - -### 4. P1 — Health failures bypass error/retry and lost-connection handling - -Sources: `../crates/core/src/commands/health.rs:130`, `../crates/core/src/commands/health.rs:331`, `src/lib/session.svelte.ts:284`. - -The health command converts both transport errors and its 30-second timeout into an empty successful report. `session.loadHealth` consequently overwrites the previous report, marks the cache loaded, and leaves `healthError` empty. The API connection-loss listener never sees a rejection. Dashboard's error/retry and Diagnostics' stale-data handling cannot work for these failures; subsequent non-forced loads accept the empty cache. - -**Evidence:** the existing passing test `a_failed_batch_still_returns_a_default_report` explicitly asserts this behavior for `device offline`. This is a contract problem currently reinforced by a test. - -**Required behavior:** preserve partial results when an optional metric is unavailable, but reject a failed whole transport operation. Preserve the last valid report with an explicit error/stale state and trigger session recovery. - -### 5. P2 — Recovery is repeated indefinitely, despite the one-attempt policy - -Sources: `src/lib/session.svelte.ts:216`, `src/lib/session.svelte.ts:237`, `src/App.svelte:40`. - -After a recovery fails, `liveness` is `lost`, but the next heartbeat still probes and calls `recoverOrMarkLost`. There is no lost-state guard or recovery budget. `recovering` only deduplicates simultaneous attempts and is cleared after every attempt. Visibility changes, Dashboard visits, and subsequent command errors can also trigger another attempt. - -**Executable reproduction:** compiled the existing session module with the installed TypeScript/Svelte compilers, mocked an offline status and failed connect, and called `checkLiveness()` twice. Result: two connection attempts, both ending in `lost`. - -**Required behavior:** one automatic attempt per loss episode, followed by an explicit retry unless a different background-recovery policy is deliberately chosen. Reset the budget only on a new successful session or user retry. - -### 6. P1 — Canceling connect does not cancel backend connection ownership - -Sources: `src/lib/session.svelte.ts:164`, `src/lib/session.svelte.ts:200`, `src/screens/Onboarding.svelte:52`, `src-tauri/src/wireless_adb.rs:213`. - -`cancelConnect` only increments a frontend counter. The native handshake can still complete and replace the active socket, while the frontend ignores its result. A subsequent failed connect invokes `restoreCurrentLiveness`, which adopts whichever device the backend returns without updating the stored host/port or clearing the old cache. This can separate the active device from the reconnect target. At launch it can produce a connected device with an empty stored host; with an existing session it can retain a different host. - -There is a related same-host retry race: Onboarding identifies a pending attempt only by `connectingHost`. Cancel and immediately retry the same host, and the older completion matches the new attempt's host, allowing it to advance the screen or clear the new attempt's busy state. - -**Executable reproduction:** delay a connect to B, cancel it, let the mocked backend finish B, then fail a connection to C. The session adopts B while retaining its previous host. The backend implementation confirms the canceled handshake still publishes its connection. - -**Required behavior:** native and frontend connection generations must agree about which attempt owns the socket. Canceled work must not publish ownership. Use an attempt ID, not a hostname, for onboarding completion guards; restore identity and caches atomically. - -### 7. P2 — The download size limit is applied after downloading the entire file - -Sources: `src-tauri/src/file_commands.rs:294`, `src-tauri/src/file_commands.rs:299`, `src-tauri/src/wireless_adb.rs:386`. - -`pull_file` checks the 2 GiB limit only after `raw_transfer` finishes. The transport streams without a byte limit into a normal file while holding the shared connection mutex. An oversized file can consume all available phone storage and block other ADB work for the duration of the transfer. A transfer failure returns before the oversize cleanup and leaves a partial file behind; retry then creates another suffixed file. - -**Evidence:** source trace; no large transfer was performed. - -**Required behavior:** preflight size when available, enforce the limit during streaming, use a temporary destination, remove partial output on every failure, and publish the final name only after success. Preserve a healthy connection when the destination rejects further writes. - -### 8. P2 — Failed disabled-package reads are interpreted as “nothing disabled” - -Sources: `../crates/core/src/adb/batch.rs:20`, `../crates/core/src/commands/apps.rs:89`, `../crates/core/src/commands/optimize.rs:74`, `../crates/core/src/commands/snapshot.rs:50`. - -The batching format retains stdout sections but drops per-command status and stderr. Consumers reject an empty installed-package list, but accept an empty disabled-package section without checking whether that command succeeded. A failed `pm list packages -d` therefore makes Apps report disabled apps as enabled and Restore produce an empty plan. A truncated batch can have the same effect. Checking only the final shell exit code is insufficient for Optimize because its last command is `dumpsys meminfo`. - -**Evidence:** source trace of the batch format and its consumers. Existing tests cover missing installed data, but do not establish the validity of an empty disabled list. - -**Required behavior:** include per-section exit status/completion metadata and distinguish an empty successful result from a failed or missing section. Destructive planning should require complete relevant input. - -### 9. P2 — Delayed reboot cleanup can disconnect a newer session - -Sources: `src/screens/Dashboard.svelte:143`, `src/screens/More.svelte:153`, `src/App.svelte:34`. - -Both reboot handlers schedule an unscoped `onDisconnect()` 1.5 seconds after success. They do not capture a session generation, cancel the timer on destruction, or verify that the active TV is still the reboot target. If the user changes screens and establishes another connection while the original reboot request or timer is pending, the old callback disconnects the new TV and resets navigation. It also persists the explicit-disconnect preference for that unrelated session. - -**Evidence:** source trace; not timed against a physical TV. - -**Required behavior:** bind reboot completion and cleanup to the initiating session generation. Never run a global disconnect from a stale screen callback. - -## Validation performed - -All results below are local macOS results, not a three-OS CI claim: - -- `cargo fmt --all --check`: passed. -- `cargo clippy --workspace --all-targets -- -D warnings`: passed. -- Rust tests: shared core 179 passed; desktop 35 passed; mobile 21 passed; license CLI 2 passed. Doc tests passed. -- Vendored `adb_client`: 54 passed, 1 physical-device test ignored. -- Mobile `npm run check`: 0 errors, 0 warnings; `npm run build`: passed. -- Desktop `npm run check`: 0 errors, 0 warnings; `npm run build`: passed. -- Headless Chromium at 384×812, real Svelte components and a stubbed Tauri bridge: reproduced remote input crossing devices and an actual two-item Optimize apply continuing after browser Back. -- Executed compiled session/router logic with deferred mock API responses: reproduced repeated recovery, canceled-connect state divergence, and back-handler bypass. - -The targeted checks above were run without committing a test harness or changing production behavior. Their results should become permanent regression cases when the defects are fixed. - -## Coverage and limits - -Reviewed mobile navigation/session/API flow, screen action handlers, wireless lifecycle/serialization, transfer storage, the shared profile/health/package/Optimize/snapshot paths, safety-gate call sites, and CI configuration. No new engine I/O or obvious missing never-disable gate was found in the reviewed mutation paths. That is a scoped review observation, not a proof of every possible path. - -The current CI workflow runs host Rust checks and frontend checks/builds. It does not execute the frontend behavioral cases above, build/test Android-specific lifecycle and Kotlin code, or run the excluded vendored crate's unit tests. It also omits the license CLI from its explicit test/clippy package lists. These are concrete coverage gaps behind a green build. - -No phone/TV interaction, Android APK build, Android-target clippy, physical pairing, mDNS validation, background/resume matrix, or Wi-Fi-loss test was performed in this audit. PowerShell/Pester validation could not run because `pwsh` is not installed. Desktop and v1 therefore should not be described as exhaustively verified by this pass. - -The next stability pass should fix findings 1–6 together around explicit session ownership, add the reproduced scenarios as automated tests, then address transfer and batch-result correctness. After that, install the resulting APK and run the documented physical matrix; host mocks cannot establish real wireless or Android lifecycle reliability. diff --git a/v2/mobile/DEPENDENCY-REMEDIATION-20261016.md b/v2/mobile/DEPENDENCY-REMEDIATION-20261016.md deleted file mode 100644 index f67af42e..00000000 --- a/v2/mobile/DEPENDENCY-REMEDIATION-20261016.md +++ /dev/null @@ -1,58 +0,0 @@ -# Dependency remediation receipt — 2026-10-16 - -This changes dependencies/tooling only. No application feature, payment, account, permission, -font binary or license is changed. No force upgrade or dependency override is used. - -## Mobile - -- Verified no application import or build input uses `geist`; `src/app.css` directly loads the - checked-in WOFF2 files. Only package metadata, font provenance and a notices warning referred - to the npm package. Remove that unused dependency and its Next/Sharp peer chain (50 packages). -- Retain checked-in Geist 1.7.2 assets and complete OFL attribution/license. Update the provenance - wording and the optional installed-package notices check; regenerate both notices outputs. -- Target only compatible affected transitives: devalue 5.8.1 -> 5.9.4, nanoid 3.3.15 -> 3.3.20, - PostCSS 8.5.16 -> 8.5.30 and source-map-js 1.2.1 -> 1.2.2. Svelte/Vite major versions unchanged. -- Fresh audit and clean `npm ci --ignore-scripts`: **0 vulnerabilities**. Audit is time-dependent; - repeat at release. Install scripts were not enabled for this validation. - -## Desktop tooling - -The separate desktop lockfile had affected Vite, SvelteKit and shared build transitives. Its -compatible update keeps existing majors: Vite 6.4.2 -> 6.4.4, Kit 2.61.1 -> 2.70.3, devalue -5.8.1 -> 5.9.4, nanoid 3.3.12 -> 3.3.20, PostCSS 8.5.15 -> 8.5.30, source-map-js 1.2.1 -> 1.2.2. -Npm also synchronizes the stale root lockfile version metadata 2.1.0 -> the already existing -package.json version 2.3.0; this is NOT a version bump, release or tag. - -Fresh desktop audit: **0 critical/high/moderate; 3 low reports** for cookie 0.6.0 and its -SvelteKit/adapter-static dependency chain. These are one underlying advisory, not three distinct -vulnerabilities: https://github.com/advisories/GHSA-pxg6-pf52-xh8x. -Kit 2.70.3 still declares cookie ^0.6.0; fixed cookie >=0.7.0 is outside that range. Do not force -an override without a separate compatibility review. These are dev/build dependencies and this -app ships a static SPA (`ssr=false`), not a Node/SvelteKit cookie-serving backend. This limits the -observed exposure; it is not proof of safety for every build input or future server deployment. -The production-filtered desktop audit is zero. The affected Kit import is server-side. -Monitor the upstream range and re-audit before release. - -## Validation and typography - -Mobile/desktop clean installs, checks and ordinary builds pass. After preserving merged mobile -head `ff7d2e1` through normal merge `d120b8c`, the integrated mobile suite passes 224/224. -All 13 inherited release-verifier tests pass. All 26 esbuild platform lock entries are retained. -Desktop real-component launcher and Optimize safety tests pass against simulated devices. - -The before/after mobile screenshot is byte-identical at 384x812; Geist, Geist Mono and Material -Symbols all report loaded. All checked-in font bytes are identical: - -- Geist: a369fcf5628ea2aa4e1b9e2ec6a5b3624e365bda588e1f0f2f12b564f728fbb8 -- Geist Mono: fba8f577f38a2bbcbe818efa6348dd58f36303a10b8737c42fefad275be563ab -- Material Symbols: 920a58b8f7793d89dadacd5d14102d103a0059c8076c79bd32347751b3b4bd44 - - -Desktop demo comparisons covered 15 screens in both themes. 23 initial captures were identical; -three light frames initially captured a blank/wrong view. Targeted re-capture with explicit -rendered-heading assertions matched those three baselines byte-for-byte, yielding 26/30 exact. -The other four differ only in spinner phase or dynamic App List read timestamps; typography and -layout were inspected. No modified screenshot harness or regenerated gallery is committed. -A fixture-only compiled-demo probe hit the existing top-level-await/target constraint; normal -shipping builds pass and the supported dev-demo path was used for the comparison. This is not -physical-device, Android signed-artifact or general navigation-regression evidence. diff --git a/v2/mobile/EXPORT-DISCOVERABILITY.md b/v2/mobile/EXPORT-DISCOVERABILITY.md index 8e86e5f6..fcc257d8 100644 --- a/v2/mobile/EXPORT-DISCOVERABILITY.md +++ b/v2/mobile/EXPORT-DISCOVERABILITY.md @@ -1,11 +1,8 @@ # Screenshot and report export diagnosis -Read-only source investigation, 2026-10-17. Source audit base: `56aa7c56450464b2060c27838373029b6b90c14f`. -Reconciled with approved modal integration `83f69b77e2b19b643e21ae456d0f597e90ef7d83`; its -release-checklist completion is retained and it changes none of the export destinations below. -Approved PR186 is inherited from the current base; product changes in PR187/188 are not included. Open PRs were inspected; none implements Android export. -SAF export remains explicitly open in `BACKLOG.md`, `HANDOFF.md` and `RELEASE.md`. This document -proposes scope; it does not implement storage, permission, native or UI changes. +Where every file and report the app produces actually goes, and the proposed scope for Android +export. SAF export is open work (`BACKLOG.md` P1). Nothing here is implemented beyond what the +tables describe. ## Current destinations and actions @@ -41,30 +38,24 @@ private files. The generated AndroidManifest already declares `androidx.core.con `${applicationId}.fileprovider`, with `exported=false` and `grantUriPermissions=true`. `gen/android/app/src/main/res/xml/file_paths.xml` maps `external-path` and `cache-path` to `.`. A provider exists; screenshot Open/Share UI and the native export/share bridge remain unwired. -No app-level `ACTION_SEND` or SAF create-document flow was found in the inspected sources. No broad storage permission change is needed -for existing private saves, and none was made. +No app-level `ACTION_SEND` or SAF create-document flow was found in the inspected sources. No broad storage permission is needed for the +existing private saves. Existing tests cover screenshot PNG/error validation and safe filename conversion in core; file-copy feedback, stale-session suppression, collision preservation and partial-file cleanup; unknown-diagnostic snapshot fidelity/redaction; and app-report browser/clipboard fallback. No mobile browser test stub for native `take_screenshot` was found. Evidence-capture calls to Playwright `page.screenshot` are not product screenshot tests. No SAF save/cancel or native Share -coverage exists because those product flows are not implemented. No tests were rerun for this -documentation-only investigation; previous unrelated green runs are not export acceptance. +coverage exists because those product flows are not implemented. Green runs of unrelated tests are not export acceptance. -## Small existing-flow scope +## Copy rules for the existing flows -A safe first UI-only slice can clarify the screenshot preview's saved filename/private location, -state that it is absent from public Gallery/Downloads and that Close only dismisses the preview, -and keep Files' “Copied this session” destination language consistent. It must not add a misleading -Open/Share button or send users to Files as if that screen listed phone screenshots. Text reports -should continue saying **Copy**, with clipboard success/error states, rather than claiming file save. -Opening an oversized app report can also copy its text before browser launch; preserve that feedback. - -Add mocked screenshot success/error/close tests and clipboard-failure checks, with narrow/large-text -screenshots and a short interaction recording. Keep existing warning text, report redaction and -copy/export fidelity. This slice needs no native bridge, dependency, manifest, Tauri capability or -storage-permission changes. No such implementation is included here. +The screenshot preview names the saved file and its private location, says it is absent from the +public Gallery/Downloads, and says Close only dismisses the preview; Files' "Copied this session" +uses the same destination language. Don't add an Open/Share button before the native bridge +exists, and don't send users to Files as if it listed phone screenshots. Text reports say **Copy**, +with clipboard success/error states, and never claim a file was saved. Opening an oversized app +report copies its text before the browser launches; keep that feedback. ## Broader export scope to agree before implementation diff --git a/v2/mobile/FAST-REMOTE-PLAN.md b/v2/mobile/FAST-REMOTE-PLAN.md index f32fdce2..ce7d25e3 100644 --- a/v2/mobile/FAST-REMOTE-PLAN.md +++ b/v2/mobile/FAST-REMOTE-PLAN.md @@ -1,19 +1,19 @@ # Mobile fast-remote plan -Status (reviewed 2026-09-01): **Phases 1–3 complete; Phase 4 device gates in progress.** Mobile now uses a persistent +Status: **Phases 1–3 are built; the Phase 4 device gates are still open.** Mobile uses a persistent scrcpy control-only channel with transparent `adb shell input` fallback. The phone cannot reuse the desktop's localhost `adb forward` path because it talks directly to `adbd`, so the server and control socket each own a dedicated authenticated ADB connection while normal commands continue on `WirelessAdb`'s existing connection. -## Spike result +## Why the dependency needed a patch The direct ADB protocol can open the socket that scrcpy exposes. The client sends an ADB `OPEN` whose destination is `localabstract:scrcpy_<scid>`, then exchanges `WRTE`, `OKAY`, and `CLSE` messages on that logical stream. An adb-server forward ultimately performs the same device-side open, so no localhost forward is needed on mobile. -`adb_client` 3.2.2 already contains the protocol machinery, but its public API stops one layer too +Upstream `adb_client` 3.2.2 contains the protocol machinery, but its public API stops one layer too high: - `ADBMessageDevice::open_session` constructs the required `OPEN`, but it is `pub(crate)`. @@ -24,22 +24,21 @@ high: - The crate's `Forward` and `Reverse` commands only work through `ADBServerDevice`; they do not apply to a direct `ADBTcpDevice` connection. -The current upstream `main` branch has the same API boundary as 3.2.2. This is not a dead end: the -missing surface is small, and the crate is MIT-licensed. It does mean the fast path must not be -implemented by simply removing the Android `cfg` gates from the desktop code. +The missing surface is small and the crate is MIT-licensed, so the vendored copy adds it. The fast +path cannot be had by simply removing the Android `cfg` gates from the desktop code. -## Chosen design +## Design -Add a small, upstreamable raw-service stream to `adb_client`, initially carried as a pinned, -documented patch so Android builds are reproducible. Keep the patch generic rather than adding -scrcpy-specific behavior to the dependency. +The vendored `adb_client` carries a small, upstreamable raw-service stream as a pinned, +documented patch, so Android builds are reproducible. The patch is generic, with no +scrcpy-specific behavior in the dependency. -The API should create a **dedicated authenticated TCP connection** for one service and return an +The API creates a **dedicated authenticated TCP connection** for one service and returns an owned stream. It must not clone the transport used by `WirelessAdb`: cloned `TcpTransport` values share one socket, and `adb_client` has no dispatcher that routes incoming messages by logical stream ID. Two concurrent readers on that socket can consume each other's frames. -The stream implementation must: +The stream implementation: 1. connect and authenticate with the same persisted RSA key as `WirelessAdb`; 2. send `OPEN(local_id, 0, "localabstract:scrcpy_<scid>")` and validate `OKAY` plus both IDs; @@ -50,7 +49,7 @@ The stream implementation must: panic-prone; 7. enforce timeouts so a dead TV cannot pin a blocking worker forever. -The mobile host will wrap the blocking stream with `spawn_blocking`; the core engine remains pure. +The mobile host wraps the blocking stream with `spawn_blocking`; the core engine remains pure. All ADB behavior stays behind the existing `WirelessAdb`/`AdbDriver` seam rather than creating a second mobile ADB wrapper. @@ -69,9 +68,9 @@ Reuse the pinned desktop scrcpy server v3.1 and the existing protocol encoders i - device path: `/data/local/tmp/shieldopt-scrcpy-server.jar` - control messages: 14-byte big-endian key down/up and length-prefixed UTF-8 text -Avoid maintaining a second jar copy. Compile the existing resource into the mobile host with -`include_bytes!`, verify its hash in a test, and materialize it in app-private storage only when a -local path is needed for `adb_client::push`. +There is no second jar copy: the mobile host compiles the existing resource in with `include_bytes!` +(`src-tauri/src/scrcpy_resource.rs`), verifies its hash in a test, and materializes it in +app-private storage only when a local path is needed for `adb_client::push`. Cold start sequence: @@ -83,10 +82,10 @@ Cold start sequence: 5. read and validate the `0x00` dummy byte; 6. retain the service stream until disconnect, connection loss, or an explicit session reset. -The detached short-shell launch was rejected during the first device gate: the older Shield exits -the background child with its shell. The implemented path opens a generic long-lived `shell:` -service through `ADBTcpService`, retains that connection for the session, and opens the abstract -control socket on another dedicated connection. This keeps the normal command connection free and +A detached short-shell launch does not work: the older Shield exits the background child with its +shell. So the server runs under a generic long-lived `shell:` +service opened through `ADBTcpService` and kept for the session, and the abstract control socket +uses another dedicated connection. This keeps the normal command connection free and gives teardown an owned server handle without using `ADBDeviceExt::exec`'s detached reader thread. Teardown order: @@ -103,92 +102,74 @@ killed by an older cleanup task. ## Integration phases -### Phase 1 — dependency API and protocol tests - -**Complete (2026-07-13).** The workspace now pins `vendor/adb_client` through -`[patch.crates-io]`. `adb_client::tcp::ADBTcpService` owns a dedicated authenticated connection and -implements the raw stream with a conservative 4 KiB ADB payload cap. Its in-memory transport tests -cover exact NUL-terminated `OPEN`, `WRTE`/`OKAY`, partial reads, interleaved traffic, multi-frame -writes, physical EOF, malformed and zero IDs, timeouts, local close, and peer `CLSE`. The patch -passes its native tests and clippy with warnings denied, and cross-compiles for -`aarch64-linux-android` with the mobile `framebuffer` feature. - -### Phase 2 — mobile session transport - -**Complete (2026-07-13).** - -- Extend the existing driver seam with only the generic byte-stream capability the Android session - needs; desktop may retain its current forward/socket implementation. -- Make `RemoteInputSession` and the `AppState` session registry available on Android with a - platform-specific startup transport, while keeping the shared key/text encoders unchanged. -- Generalize server-resource resolution for Android using the embedded jar materialization above. -- Drop the session during `wireless_disconnect`, device switching, and liveness failure. - -### Phase 3 — shared commands and fallback - -**Complete (2026-07-13).** - -- Remove the hard-coded `scrcpy channel unavailable on Android` branch in shared `send_key` and - `send_text` once the Android session implementation exists. -- Preserve `forceShell`, transparent same-request fallback, session reset after a stream error, the - key allowlist, and the existing `SendTextResult.transport` contract. -- Keep the frontend unchanged unless device testing exposes a status or lifecycle gap. It already - enables hold-to-repeat only after the backend reports `transport: "channel"`. - -### Phase 4 — device and regression gates - -**In progress.** Validated from the Pixel 10 Pro on the Bedroom Shield (`192.168.42.196`): - -- the first press cold-started the channel and landed (1,097 ms including push/server startup); -- 30 consecutive warm D-pad presses all reported `transport: "channel"`, with 75.2 ms p95 - (59.3–76.4 ms observed); -- UTF-8 text (`héllo ✓`) reported `transport: "channel"` successfully; -- explicit disconnect and force-stop both removed the resident app-process and - `@scrcpy_<scid>` socket, and reconnect succeeded; -- the exact embedded jar was materialized on the Pixel and verified against the pinned SHA-256; -- the Android build and Android-target clippy pass with warnings denied. - -The Living Room Shield (`192.168.42.71`) also delivered every warm press over the channel. Three -30-press samples measured 108, 98, and 87 ms p95 respectively as the channel/device warmed. UTF-8 -channel text and ASCII compatible-shell text succeeded, Unicode compatible-shell text reported its -limitation honestly, and diagnostics plus file listing/pull remained usable while the channel was -live. The pulled sentinel matched by SHA-256, force-stop left no resident scrcpy process/socket, and -the 30-second background expiry plus resume cancellation were observed in lifecycle logs. - -Still required: exercise hold-to-repeat, TV reboot, sleep/wake, and full background-reconnect flows -with a **live fast-remote session** on both Shields. The observed lifecycle logs proved timer expiry -and stale-timer cancellation, but do not by themselves prove warm-session reuse before 30 seconds or -cleanup after 30 seconds. Repeat concurrent diagnostics/file work on Bedroom; Living Room already -passed that portion. Take one fresh controlled Living Room warm-latency sample because the first of -its three runs exceeded the strict 100 ms stop condition. The Bedroom Shield was unauthorized during -the latest host-side attempt, so its remaining gates were not repeated. - -### Next physical session order - -1. Build and install current branch HEAD on the Pixel over wireless ADB; the last installed APK - predates the cached-label fix in `3eccc34`. Spot-check the previous-TV menu, resulting header - label, used-RAM bar, and missed-prompt error before returning to remote testing. -2. Authorize Bedroom if necessary, open one live channel, and run its outstanding concurrent - diagnostics and file list/pull check. -3. On each Shield, test hold, sleep/wake, reboot cleanup, resume under 30 seconds, and expiry over - 30 seconds while the channel is actually live. Inspect process/socket cleanup after each terminal - path. -4. Run one deliberate 30-press Living Room latency sample and record p95. Coordinate any repeated - TV input with Bryan; do not generate blind button traffic during normal viewing. - -On each known Shield (`192.168.42.196` and `192.168.42.71` when available): - -- first press cold-starts and lands, then warm D-pad presses feel immediate; +### Phase 1 — dependency API and protocol tests (built) + +The workspace pins `vendor/adb_client` through `[patch.crates-io]`. `adb_client::tcp::ADBTcpService` +owns a dedicated authenticated connection and implements the raw stream with a conservative 4 KiB +ADB payload cap. In-memory transport tests cover exact NUL-terminated `OPEN`, `WRTE`/`OKAY`, +partial reads, interleaved traffic, multi-frame writes, physical EOF, malformed and zero IDs, +timeouts, local close and peer `CLSE`. The patch cross-compiles for `aarch64-linux-android` with the +mobile `framebuffer` feature. + +### Phase 2 — mobile session transport (built) + +- The driver seam carries only the generic byte-stream capability the Android session needs; + desktop keeps its forward/socket implementation. +- `RemoteInputSession` and the `AppState` session registry are available on Android with a + platform-specific startup transport; the shared key/text encoders are unchanged. +- Server-resource resolution on Android uses the embedded jar materialization above. +- The session is dropped on `wireless_disconnect`, device switching and liveness failure. + +### Phase 3 — shared commands and fallback (built) + +- Shared `send_key` and `send_text` use the Android session; there is no hard-coded "unavailable on + Android" branch. +- `forceShell`, transparent same-request fallback, session reset after a stream error, the key + allowlist and the `SendTextResult.transport` contract are preserved. +- The frontend enables hold-to-repeat only after the backend reports `transport: "channel"`. + +### Phase 4 — device and regression gates (open) + +Measured from the Pixel 10 Pro, on builds that predate later reconnect and lifecycle work: + +- **Bedroom Shield:** the first press cold-started the channel and landed (1,097 ms including push + and server startup); 30 consecutive warm D-pad presses all reported `transport: "channel"`, at + 75.2 ms p95 (59.3–76.4 ms observed); UTF-8 text (`héllo ✓`) went over the channel; explicit + disconnect and force-stop both removed the resident server process and `@scrcpy_<scid>` socket, + and reconnect succeeded; the embedded jar materialized on the phone matched the pinned SHA-256. +- **Living Room Shield:** every warm press went over the channel. Three 30-press samples measured + 108, 98 and 87 ms p95 as the channel and device warmed. UTF-8 channel text and ASCII + compatible-shell text worked, Unicode compatible-shell text reported its limitation honestly, and + diagnostics plus file listing/pull stayed usable while the channel was live (the pulled sentinel + matched by SHA-256). Force-stop left no resident process or socket, and lifecycle logs showed the + 30-second background expiry and resume cancellation. + +Still required: + +1. Install the current build on the phone and identify it before testing. +2. On the Bedroom Shield (authorize it if needed), open one live channel and repeat concurrent + diagnostics and file list/pull. The Living Room Shield already passed that part. +3. On each Shield, with the channel actually live: hold-to-repeat, sleep/wake, reboot cleanup, + resume under 30 seconds and expiry over 30 seconds. Inspect process/socket cleanup after each + terminal path. The lifecycle logs above proved timer expiry and stale-timer cancellation, not + warm-session reuse before 30 seconds or cleanup after it. +4. One deliberate 30-press Living Room latency sample, recording p95, because the first of its three + runs exceeded the 100 ms stop condition. Coordinate repeated TV input with the owner; don't send + blind button traffic during normal viewing. + +Acceptance on each Shield: + +- the first press cold-starts and lands, then warm D-pad presses feel immediate; - measured warm press latency is below 100 ms at p95 (target: network RTT); - press-and-hold repeats without queuing slow shell calls; - UTF-8 text works; compatible mode remains ASCII-only and honest about errors; -- sleep/wake, disconnect/reconnect, TV reboot, and app force-stop leave no orphaned +- sleep/wake, disconnect/reconnect, TV reboot and app force-stop leave no orphaned `shieldopt-scrcpy-server` process; phone resume before 30 seconds reuses the owned session, while a longer background interval cleans it up before the next cold start; - an unsupported or failed scrcpy launch falls back to shell on the same press; - normal diagnostics and file operations still work while the control channel is open. -Then deploy to the Pixel over wireless ADB only and run the repository gates from `HANDOFF.md`. +Deploy to the phone over wireless ADB only, and run the repository gates from `HANDOFF.md`. ## Stop conditions diff --git a/v2/mobile/FEATURES.md b/v2/mobile/FEATURES.md deleted file mode 100644 index b5599975..00000000 --- a/v2/mobile/FEATURES.md +++ /dev/null @@ -1,141 +0,0 @@ -# ATV Optimizer mobile — features inventory - -> **Snapshot, 2026-07.** Nearly every status row below is now wrong — four screens it calls -> missing exist. The architecture section describing a libadb transport is describing code that -> was deleted in `3e8bcfc`. Use [`HANDOFF.md`](HANDOFF.md) §4 for the current screen/command map -> and `src-tauri/src/lib.rs` for the authoritative command list. - -> Historical snapshot from the initial architecture review. Many row statuses below are stale. -> Use [`BACKLOG.md`](BACKLOG.md) and [`HANDOFF.md`](HANDOFF.md) for the current implementation and -> ordered queue; update this matrix only as a dedicated reconciliation pass. - -This is the buildout checklist for the mobile re-architecture: every desktop feature mapped to -the mobile design's 20 screens, with a Built/Partial/Todo status and the Free/Pro split. It's a -snapshot, not a contract — desktop's `v2/src/lib/api.ts` keeps evolving, so re-check this doc -against `api.ts` and `v2/mobile/src-tauri/src/lib.rs`'s `invoke_handler!` list whenever either -changes. Screen numbers/names are from the design file `ATV Optimizer Mobile.dc.html` (the -`<!-- N.N ... -->` section markers). "Backend" status reflects whether the Tauri command is -*registered* on mobile (`v2/mobile/src-tauri/src/lib.rs`); "Frontend" reflects whether a mobile -screen actually calls it (`v2/mobile/src/screens/*.svelte`). - -Sources read: `v2/src/lib/api.ts`, `v2/src/lib/types.ts`, `v2/crates/core/src/license.rs`, -`v2/crates/core/src/commands/{apps,snapshot,launcher,optimize,reboot,tuning}.rs`, -`v2/mobile/src-tauri/src/lib.rs`, `v2/mobile/src/screens/*.svelte`, -`v2/mobile/ARCHITECTURE-REVIEW.md`, `v2/ATVTOOLS-PARITY.md`, `docs/FEATURES.md`. - -## Screen → feature map - -Status legend: **Built** = a mobile screen exists and calls the real command. **Partial** = a -screen exists but is mocked/fake data, missing the real invoke, or the backend command isn't -registered on mobile yet. **Todo** = no mobile screen exists for this at all. - -| # | Design screen | Desktop command(s) / feature | Free or Pro | Mobile status | Notes | -|---|---|---|---|---|---| -| 1.0 | Reconnect (returning) | Saved-TV list + auto-reconnect (no desktop equivalent — desktop always re-scans) | Free | **Todo** | No persistence anywhere on mobile (no localStorage/store); `Onboarding.svelte` always starts at scan. Tracked as ARCHITECTURE-REVIEW F8/T6 — the intended first screen, entirely unbuilt. | -| 1.1 | Scan / Find TV | `wireless_discover` (mobile-only transport; desktop equivalent is `scanNetwork`/`adbStatus`) | Free | **Built** | `Onboarding.svelte` step `"scan"`, calls `wireless_discover`. Manual IP entry (host/pairPort/connectPort inputs) present too. | -| 1.2 | Pair code | `wireless_pair` (mobile-only; desktop has no pairing UI, uses `pairDevice`/`connectDevice` for wireless debug) | Free | **Built** | `Onboarding.svelte` step `"pair"`. | -| 1.3 | Connected success | `wireless_connect`, then `list_devices`/`device_profile` for enrichment | Free | **Built**, enrichment flaky | Step `"connected"`. Device name/model enrichment depends on `harvest_properties` over `exec:`, which ARCHITECTURE-REVIEW F9/T5 flags as historically falling back to "Android TV"/"Unknown Device" — needs on-device re-verification now that `exec:` works. | -| 2.1 | Dashboard | `health_report`, `trim_caches`, `take_screenshot`, `reboot_device`, `find_remote`, `wireless_disconnect` | Free (all of these are free-tier commands on desktop) | **Partial** | `Dashboard.svelte` calls all of the above, but: `reboot_device` is called with the wrong arg name (`rebootMode` instead of `mode`) so it silently never reboots (F1); a failed `health_report` renders as a fabricated 100-score "healthy" device instead of an error state (F2); `healthScore`/`freedRamEst` are invented client-side, not backend values. | -| 2.2 | Health / Diagnostics | `health_report` (storage/RAM/temp/display/audio breakdown), `safety_info` for per-app tags | Free | **Partial** | `Diagnostics.svelte` calls `health_report`, but hardcodes plausible-looking fallbacks (`storageTotal ?? "16G"`, `tempC ?? 48`, `audioOutput ?? "Atmos"`, …) when fields are null (F3) — indistinguishable from real data. Also reimplements safety classification inline (`getSafetyTag`) instead of calling `safety_info`, bypassing the audited core classifier (F5). | -| 3.1 | Optimize review | `prepare_optimize` (`Feature::OptimizeWizard`) | **Pro** | **Partial** | `Optimize.svelte` hardcodes a mock debloat list + "182 MB" sizes; it does call `prepare_optimize` but swallows the real `OptimizePlan`/`LOCKED:` response and discards it (F4). No real plan is ever shown. | -| 3.2 | Optimize running | Per-row apply of the plan from 3.1 (`disable_package` etc., each gated by `Feature::CuratedDebloat`) | **Pro** | **Todo** | No progress/running UI exists — 3.1 doesn't produce a real plan to run. | -| 4.1 | App list | `list_other_packages` (free), `disable_package`/`uninstall_package` (**Pro**, `Feature::CuratedDebloat`), `enable_package`/`force_stop`/`reinstall_existing`/`open_play_store` (free), `app_memory_map`/`app_usage_map`/`package_states`/`safety_info` (free) | Mixed | **Partial** | `Apps.svelte` only calls `list_other_packages` + `enable_package`/`disable_package` toggling. Missing: the curated catalog (`app_list_for_device`), force-stop, memory/usage columns, safety tags, and any Pro gate messaging on disable/uninstall (currently the command just returns a `LOCKED:`-style error with no UI explanation). | -| 4.2 | Launcher | `list_launchers`, `current_launcher`, `channel_provider_disabled` (free); `set_default_launcher`/`disable_launcher` (**Pro**, `Feature::LauncherTakeover`) | Mixed | **Todo (backend ready)** | All 4 commands are registered in mobile's `lib.rs`, but no `Launcher.svelte` screen exists yet. Pure frontend gap. | -| 5.1 | Tweaks | `get_tweaks` (free); `write_setting`/`set_display_scaling`/`set_private_dns` (**Pro**, `Feature::TweaksWrite`); `get_display_scaling`/`get_private_dns` (free) | Mixed | **Todo (backend ready)** | All 6 commands registered on mobile; no `Tweaks.svelte` screen. CEC/frame-rate/animation/scaling/DNS controls from the design are entirely unbuilt on mobile. | -| 5.2 | Snapshots | `list_snapshots`/`save_snapshot`/`preview_apply`/`apply_snapshot`/`delete_snapshot`/`snapshot_dir_path` — all **Pro**, `Feature::Snapshot` | **Pro** | **Todo (backend ready)** | All 6 commands registered on mobile; no `Snapshots.svelte` screen. "Clone this setup to a second TV" (cross-device apply) also needs `MultiDevice` handling, which mobile has no concept of yet (single connected device at a time). | -| 6.1 | Remote | `send_key`, `send_text`, `find_remote` | Free (desktop has no Pro gate on remote input; `Feature::AdvancedRemote` exists in the enum but has no call site anywhere yet) | **Built**, one bug | `Remote.svelte` calls all three. `find_remote` runs `am start -n com.nvidia.remotelocator/...`, which is Shield-only — it errors (silently swallowed) on Google TV devices and should be gated on `device_type === "shield"` (F10), not shown unconditionally as it is on nearly every screen. | -| 6.2 | More / Settings | `wireless_disconnect`, `find_remote`; license/entitlement state (no desktop equivalent — desktop is always Pro) | Free (screen itself); gates Pro features | **Partial** | `More.svelte` fakes Pro status by string-matching the literal text `"pro"` in a license-key input (`licenseStatus = "Pro Unlocked"` if `licenseKey.trim().toLowerCase() === "pro"`) — a placeholder comment even says "Since license logic isn't fully compiled into Rust core yet". Entitlement is never actually read from the backend (`AppState::with_entitlement`). | -| 6.3 | Pro paywall | Same entitlement concept as above — no desktop equivalent (desktop ships as Pro unconditionally) | Pro (marketing surface) | **Todo** | No dedicated paywall screen; the "Pro" upsell is inline copy inside `More.svelte`. The design's dedicated paywall (lifetime license, Ko-fi tip, feature bullets) is unbuilt. | -| 7.1 | Devices hub | `list_devices`, `report_all`, `rename_device` — multi-device management, `Feature::MultiDevice` (enum exists, no call site wired yet on desktop or mobile) | Pro (per design) | **Todo** | Mobile has no multi-device concept at all — one connected device, no saved-device list, no `report_all` roll-up. Overlaps with 1.0's saved-TV persistence gap. | -| 7.2 | File transfer | Mobile: `list_remote_dir`, `pull_file` (into app storage), `find_files` + `delete_path` (shared `crates/core/src/commands/files.rs`, `/sdcard`-confined exactly as desktop); desktop-only: `push_file`, `copy_file_to_device`. `Feature::FileManager` (enum exists, unwired) | Pro (per design) | **Partial** | `Files.svelte` browses, copies to app storage, deletes under `/sdcard` with a confirmation, and runs desktop's app-files catalog (`v2/src/lib/app-files-catalog.json`, read in place) with failed searches kept distinct from "no matches" (#86). Upload from the phone and export out of app storage still need an Android SAF picker. | -| 7.3 | Backups + Google Drive | Overlaps with 5.2 Snapshots (`save_snapshot`/`list_snapshots`) plus new ground: Google Drive upload/restore, cross-device "this phone" backup destination | Pro | **Todo** | Snapshot backend exists (see 5.2) but is unused on mobile; Google Drive integration doesn't exist anywhere in the codebase (desktop or mobile) — net-new scope, not a port. | -| 8.1 | App detail sheet | `safety_info`, `app_memory_map`/`app_usage_map`, then the same disable/uninstall/keep actions as 4.1 | Mixed (view free, destructive actions Pro) | **Todo** | No bottom-sheet/detail view component exists on mobile; `Apps.svelte` only supports a flat list with inline actions. | -| 8.2 | Risk & actions guide | `safety_info` (drives Safe/Blocked/Protected tiers); static "disable vs uninstall" explainer copy | Free | **Todo** | No standalone guide screen; `Diagnostics.svelte`'s inline `getSafetyTag` reimplementation (see 2.2/F5) is the closest thing, and it doesn't call `safety_info` at all. | - -## Desktop `api.ts` commands not yet surfaced anywhere in mobile - -Not registered in `v2/mobile/src-tauri/src/lib.rs` at all (backend gap, not just frontend): - -- **ADB/network lifecycle** (mobile doesn't need these — it uses the embedded wireless-ADB - Kotlin transport instead of managing a local `adb` binary): `adbStatus`, `checkForUpdate`, - `installAdb`, `restartAdb`, `scanNetwork`, `connectDevice`, `disconnectDevice`, `pairDevice` - (mobile has `wireless_discover`/`wireless_pair`/`wireless_connect`/`wireless_disconnect` - instead). -- **Sideload / app cloning**: `installApk`, `backupApk`, `cloneApp` — Pro-shaped (`Feature::Sideload` - exists in the license enum but has no call site on desktop either). These live in - `v2/src-tauri` desktop commands, not shared core. -- **File manager**: `listDir`, `pullFile`, `pushFile`, `deletePath`, `findFiles`, - `copyFileToDevice`, `listApksInFolder` — same story (`Feature::FileManager`, unwired, - desktop-only code). Needs SAF on Android (see 7.2 above). - -Registered on mobile's backend but **not yet called by any mobile screen** (pure frontend gap, -no port work needed): - -- `report_all` (7.1 Devices hub) -- `list_launchers`, `current_launcher`, `channel_provider_disabled`, `set_default_launcher`, - `disable_launcher` (4.2 Launcher) -- `get_tweaks`, `write_setting`, `get_display_scaling`, `set_display_scaling`, - `get_private_dns`, `set_private_dns` (5.1 Tweaks) -- `list_snapshots`, `save_snapshot`, `preview_apply`, `apply_snapshot`, `delete_snapshot`, - `snapshot_dir_path` (5.2 Snapshots) -- `app_memory_map`, `app_usage_map`, `package_states`, `safety_info` (4.1 App list / 8.1 App - detail / 8.2 Risk guide — data is available, no screen reads it) -- `set_app_permission`, `app_permission_state`, `set_app_op`, `get_app_op` — permissions - grant/revoke; not represented in the 20-screen design at all, and `docs/ATVTOOLS-PARITY.md` - lists desktop permissions UI as `❌` too, so this is behind on both platforms. -- `panic_recovery` — the do-not-brick recovery path is registered but has no mobile entry - point (design has no dedicated recovery screen; desktop has one under Advanced). -- `device_profile` — used implicitly by enrichment flows but no screen calls it directly for a - refresh/re-detect action. - -## Free/Pro split (from `v2/crates/core/src/license.rs::Feature`) - -Gated today (has a `require_pro` call site): `CuratedDebloat` (disable/uninstall package), -`OptimizeWizard` (prepare_optimize), `Snapshot` (all 6 snapshot commands), `LauncherTakeover` -(set/disable launcher), `TweaksWrite` (write_setting, set_display_scaling, set_private_dns), -`AppPermissionWrite` (set_app_permission, set_app_op), `AdvancedReboot` (reboot_device). - -Defined in the enum but **no call site anywhere yet** (reserved for future gating, likely when -these land on mobile): `MultiDevice`, `AdvancedRemote`, `Sideload`, `FileManager`, -`BackupClone`. Treat their eventual Free/Pro placement as an open design decision, not settled -fact — the design screens (7.1–7.3) imply Pro, but nothing in code enforces that today. - -Mobile-specific gating caveat (ARCHITECTURE-REVIEW B6): the do-not-disable safety classifier -runs *before* `require_pro` in every destructive path, and mobile reuses the same core -commands as desktop — so entitlement is client-crackable but the safety gate is independent of -it; a cracked-Pro mobile build still can't brick a device. Real gap is M5 (signed license -validation), which is a revenue concern, not a safety one. Also currently broken: `More.svelte` -fakes entitlement by string-matching, so mobile's actual Free/Pro state is never read from the -backend at all (see 6.2 above) — every gated command above still enforces server-side (Rust) -regardless of what the UI displays. - -## Mobile-only concerns (no desktop equivalent) - -- **Wireless pairing flow** (1.1/1.2/1.3) — Android TV's wireless-debugging pair/connect dance - via `wireless_discover`/`wireless_pair`/`wireless_connect`, backed by the Kotlin - `tauri-plugin-atv-adb` plugin and its libadb transport. Desktop assumes a pre-configured - `adb` binary and doesn't need this. -- **Saved-TV persistence + auto-reconnect** (1.0) — needs local storage of paired devices - (host/port/last-known-name/device_type) and a launch-time reconnect attempt with fallback to - Scan. Entirely unbuilt (ARCHITECTURE-REVIEW F8/T6). The RSA keypair is already persisted - Kotlin-side, so re-auth after the first pair is silent — only the app-side bookkeeping is - missing. -- **Safe-area / gesture-nav insets** — `BottomTabs` has no `env(safe-area-inset-bottom)` - padding, so tab labels sit under the home indicator on gesture-nav Android devices (B5). - No desktop equivalent (desktop has a title bar, not a bottom tab bar). -- **Offline/bundled fonts** — Material Symbols is shipped as the full 5.34 MB variable font for - ~35 used glyphs (B2); needs subsetting for APK size. Desktop ships as a native binary with no - equivalent web-font payload concern. -- **Webview lifecycle** — no `visibilitychange`/resume handler, so the Android webview can go - blank when the app is backgrounded and resumed (B5). Not a concern for a native Tauri desktop - window. -- **Transport concurrency** (T1/T2) — the Kotlin `AdbService` wraps every shell call in one - global mutex, serializing what should be concurrent `dumpsys`/`df` reads and causing - `health_report` timeouts under load; `exec:` also hardcodes `exit_code = 0` so command-failure - detection is degraded on this transport specifically. Desktop's `SubprocessAdb` has neither - issue. Several mobile-only band-aids in shared core (`health.rs` per-command timeouts, - `apps.rs::list_other_packages_impl` sequential execution) exist only to compensate for this - and should be reverted once the mutex is fixed (T3). -- **Android release packaging** — no signing keystore, no versionCode bump process, no release - workflow, `isMinifyEnabled = false`, universal (all-ABI) APK (B4). Desktop has a full, - working `v2-release.yml` pipeline; mobile has none of this yet. diff --git a/v2/mobile/FEEDBACK-UX-2026-09-07.md b/v2/mobile/FEEDBACK-UX-2026-09-07.md deleted file mode 100644 index 629b8ab6..00000000 --- a/v2/mobile/FEEDBACK-UX-2026-09-07.md +++ /dev/null @@ -1,264 +0,0 @@ -# Saved TVs, app risk, details, and product copy - -> **Brief written 2026-09-07; implemented in `d02de57`,** except process identity (GitHub #97). -> Statements below that this is "not implemented behavior" no longer hold. The coordination -> sections near the end refer to a retired system and are historical only. - -Navigator source review for `so-fb3.1`–`.4`, with `.6` reporting handoff, convoy `hq-cv-z4oev`. -Workspace baseline: `48cba2384ca3938e6655b77ccf85716dd86cca5f`, -`crew/navigator`. This is an implementation brief, not implemented behavior. -No app control, discovery scan, ADB, GUI, build, or test ran for this review. -Mechanic owns integration and implementation reservations; Navigator owns this -document and UX acceptance. Existing work and archived candidates are preserved. -The [source manifest](evidence/feedback-ux-48cba23-source.json) records hashes of -the 18 baseline files supporting these findings and the local-diagnostics handoff. -Mechanic reviewed and accepted this source/copy contract as an implementation -brief. That acceptance does not establish implemented, tested or shipped UI. - -## Source findings and priorities - -| Priority | Source evidence at this baseline | Required outcome | -| --- | --- | --- | -| 1: saved identity | `src/lib/savedDevices.ts` uses hardware ID when both sides have one, otherwise host equality. `rememberDevice` removes conflicting old-host records; it does not retain an addressless saved record. Onboarding discovery groups services by host separately from saved TVs. | Discovery must distinguish remembered identity, discovery evidence, and a live connection. Do not merge or inherit a name solely because an address matches. | -| 1: unsupported Safe | `../crates/core/src/engine/safety.rs::classify` defaults to `Safety::Safe`; `unknown_package_is_safe` expects that. `src/lib/safety.ts` turns it into an affirmative assurance. | Introduce canonical Unknown semantics; never-disable remains dominant on every mutation path. A successful lookup returning Unknown is different from a failed lookup. | -| 1: process identity | `../crates/core/src/adb/parse.rs::parse_dumpsys_meminfo` captures `[a-zA-Z0-9_.]+` without requiring a complete process-name token. It strips colon suffixes and can truncate hyphenated names. Health turns every resulting key into `MemoryEntry.package`. | Preserve process identity before attributing memory to installed packages. Unresolved process/system entries are inspect-only. Do not invent a package from a prefix. | -| 2: shared details | Apps opens `AppDetailSheet`; Diagnostics renders separate rows/actions. Apps enrichment awaits directly into package-keyed maps without a captured device/generation check. Detail safety request identity is package-only. | Use the same package details and action state from both entrypoints, bound to device, package, and connection generation. | -| 2: measurements | Both health and app-memory commands use the Total PSS by process section. The detail sheet hides missing/zero RAM, has no per-app storage field, and treats an absent last-used value as Never opened. | Label PSS, measurement age/scope, unavailable data, and storage separately. Missing usage history does not establish that an app was never opened. | -| 2: background control | `../crates/core/src/commands/tuning.rs` reads `settings get global background_process_limit`; mobile and desktop write that key. Mobile offers only Standard/≤4/≤2/None and claims reboot reset. | Verify the actual control/readback capability before offering more choices or claiming success. See official-source discrepancy below. | - -The submitted report appears mobile from its screen names and prior feedback, -but this review cannot identify the newly reported installed build. The earlier -owner-reported debug build at `44d2d66` is historical. Shared core affects desktop; -desktop `src/lib/types.ts` also has only three Safety variants, and -`src/lib/components/TweaksTab.svelte` uses the same background settings key. - -## Saved-device presentation contract (`so-fb3.1`) - -| Evidence | Row text and action | Identity rule | -| --- | --- | --- | -| Current confirmed session | Connected; Open dashboard | Only the live canonical session can grant this state. | -| Discovered endpoint matches a remembered address, identity unverified | Saved address · Identity unverified; Connect | Current scan results have no authenticated stable ID. Do not inherit trusted identity or merge saved TVs from this match. | -| Saved identity matched to a discovery result with sufficient identity evidence | Saved; Connect | One row, remembered name plus current endpoint. Connection/authentication/profile must still succeed. | -| Saved endpoint absent from the current scan | Saved · Not found in this scan; Retry connection | Scan absence is not proof the TV is offline. Keep the saved TV available. | -| Connection attempt failed | Couldn't connect; Retry | Show the actual failure and preserve the remembered TV. Do not assert power state. | -| Newly discovered endpoint without an established match | Found on network; Connect | No trusted saved name, silent identity merge, or Connected badge from IP/name similarity. | - -An IP match with missing hardware identity is a possible match only. The resolved -profile must decide durable deduplication; a conflicting hardware ID must never -inherit the old TV's label. A service name is not automatically a hardware ID. -Keep canceled or superseded results from modifying saved rows or routing the UI. -Preserve explicit Disconnect and add-TV intent. Reuse accepted `so-vtm.3`: -saved-TV success opens Dashboard only after profile resolution; first-time/add-TV -keeps its confirmation. This brief adds recognition, not session persistence. - -**Mechanic-local identity addendum (2026-09-08).** `so-fb3.1.2` is now integrated -in `crew/mechanic` as `savedDevices.ts` SHA-256 -`163472a87f765f2db981ef3a9fc2e7454604154e35633818506cec6f7aa8c8b1`. -Equal hardware IDs are the only cross-endpoint identity; ID-less rows match only -the exact host and port. Conflicting IDs, or an identified connection at an -ID-less saved endpoint, remain separate saved rows. This supersedes the baseline -source behavior described above, but it is not released or device-verified. -Mechanic reports 11/11 pure saved-device tests, mobile check 0/0, and a passing -build. The intentional coexistence exposes a separate screen contract tracked by -`so-fb3.1.3`: Onboarding and Devices must key, select, filter, show progress for, -and forget the exact saved identity rather than using host alone or an MRU -host:port surrogate. Show `host:port` and neutral shared-address guidance; do not -claim that two saved rows prove two currently reachable TVs. - -## Risk and action language (`so-fb3.2` and `.3`) - -| Canonical outcome | User label | Explanation / action boundary | -| --- | --- | --- | -| Never-disable | Protected | This app blocks disabling or uninstalling this package. Show the package-specific reason. No override confirmation. | -| Caution | Caution | Disabling can affect a TV feature. Show the known consequence before confirmation. | -| Affirmatively reviewed optional entry, with applicable catalog evidence | Optional | Review what this app does before changing it. Keep is the default for optional choices. A catalog match alone must not override Caution or Protected. | -| Uncatalogued or insufficient evidence | Unknown | We haven't reviewed the effects of disabling this app. It may affect other TV features. No automatic selection or safe recommendation. | -| Pending/failed verdict | Checking risk… / Risk unavailable | Keep Disable and Uninstall unavailable until classification succeeds. Offer a real retry, not a claim that reopening the whole app is required. | -| Unresolved process/system aggregate | Process details | No verified app match. App actions are unavailable. Do not classify the display string as an installable package. | - -“Optional” is proposed display vocabulary, not permission to introduce a second -classifier or silently reinterpret the current `safe` result. Mechanic must -define the pure core result, registry evidence and device applicability first. -Catalog `RiskTier`, default selection, and operation safety are different fields. -The existing JSON rationale and restore description should be reused; a missing -review source/date remains missing. No invented audit history or blanket device -compatibility claim. - -Unknown must not become a Protected bypass. Once package identity is verified, -an explicit user-selected action may be reviewable under Mechanic's canonical -policy, with Unknown warning and current entitlement checks. No blanket ban on -all unknown user apps is implied. Backend gates remain mandatory for direct -disable/uninstall, Optimize apply, snapshot apply, launcher flows and recovery -inverses. Enable/recovery must remain distinct from disabling. Do not add a local -UI allowlist or hard-coded registry packages. - -Action confirmations name the TV, app/package and operation. Suggested wording: - -- Disable: “Stops this app from running for the current TV user. You can enable - it again in Apps.” Show the risk reason and qualify recovery when losing a TV - feature could prevent access to this app. -- Enable: “Allows this app to run again.” -- Force stop: “Stops this app now and may interrupt playback. It may run again - later.” No guaranteed memory saving; unresolved process rows cannot invoke it. -- Uninstall: “Removes this app for the current TV user. App data may be lost. - Reinstall availability depends on whether the app remains on the TV or can be - installed again.” Preserve existing stronger operation-specific warnings. -- Optimize: list concrete Disable/Uninstall/settings changes in the existing - review flow. Do not add a one-tap “Free RAM” action to app details. - -## Shared detail and metric contract (`so-fb3.3`) - -Apps and a verified package row in Top memory consumers open the same details. -Show the available display name, exact package, Enabled/Disabled/Uninstalled state, -catalog purpose and effect/recovery rationale, then risk and explicit actions. -Unknown description: “No description is available for this app.” Keep process -name/PID distinct from an app name when package attribution is unavailable. - -Use “RAM at last refresh (PSS)” with a timestamp/age and Refresh. Explain only -when expanded: “Includes this process's share of memory used by other processes.” -PSS is a sampled RAM attribution, not installed size, a foreground/background -split, or guaranteed reclaimable RAM. Android documents the proportional sharing -and version-dependent output in its [dumpsys reference](https://developer.android.com/tools/dumpsys). -The current parser divides reported K values by 1024; the implementation must -make the displayed units consistent (MiB or an explicit conversion). - -Show “Not measured” for no usable sample, “Not reported in the latest sample” -when the app is absent, and “Last refresh failed” alongside stale data. Do not -turn these into 0 or “not running.” The existing top-20/top-8 lists are truncated -views, not whole-TV totals. Attribute each process sample once; avoid duplicating -shared-process RAM across multiple app rows. PID/name/UID resolution and app -aggregation belong in the shared command/data layer, not competing screen parsers. - -Use a separate “Installed storage” field only with a real storage measurement. -If only APK bytes are measured, label “App files” and disclose that data/cache are -excluded. Until a supported source exists, show “Not measured.” No source in the -reviewed detail data proves active-memory usage; omit that metric. Replace “Never -opened” with “Last opened: not reported” when usage history is missing. - -After actions, refresh/invalidate shared installed state, details, memory and -Optimize/health as relevant. Keep previous values visibly stale while refreshing; -failure must not become success. Dismiss or invalidate open confirmations on TV -switch, disconnect, package change or session-generation change, including -reconnect to the same serial. A response for TV A cannot populate TV B. An -uninstalled package has no active Disable/Force stop actions. - -Installed icons are optional. Reuse a generic retained glyph until a bounded, -device/package/version-keyed retrieval/cache is approved; no third-party icon -lookup, blocking icon downloads or unrelated font rebuild. - -## Background-limit source discrepancy (`so-fb3.4`) - -AOSP's [preference controller](https://android.googlesource.com/platform/packages/apps/Settings/+/master/src/com/android/settings/development/BackgroundProcessLimitPreferenceController.java) -(observed blob `7a7d6fa58c0736ce63df0fc71398f088377236ba`) uses ActivityManager -`setProcessLimit`/`getProcessLimit`, resetting to -1 when developer options are -disabled. It does not implement this control through the app's global settings -key. Thus a successful key write/readback does not establish effective behavior. -This is a source-based discrepancy, not a device-tested claim that every OEM -ignores the key. - -AOSP [SettingsLib choices](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/packages/SettingsLib/res/values/arrays.xml) -map Standard to -1, None to 0, and maxima 1–4 to those integers. Proposed compact -labels: Standard, None, ≤1, ≤2, ≤3, ≤4; accessible labels spell out “At most N -background processes.” ≤ means at most, not less than. Prefer a wrapping layout -or select control over six cramped buttons. - -[ActivityManagerConstants](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/services/core/java/com/android/server/am/ActivityManagerConstants.java) -initializes the override at -1 and updates it in memory. Its comments scope the -limit to cached background processes, excluding foreground/visible/service -processes. This supports an AOSP runtime-reset expectation, not the current UI's -universal “Android clears this on every reboot” assertion for a persisted settings -key or an untested OEM. It is not a cap on every background service or a promised -RAM gain. Sources were read remotely; no local device command was executed. - -Mechanic's next backend decision: supported invocation/readback and capability -detection through the existing driver seam, with unsupported/error/custom values -represented explicitly. Do not use hard-coded Binder transaction numbers or -treat absent data as verified Standard. Until that exists, recommended UI is a -disabled control reading “This app can't verify the background process limit on -this TV.” No “updated” toast based only on the old key. Verified-success copy: -“Background process limit updated.” Supported explanatory copy: “Limits cached -background processes. Apps may reload more often.” Qualify any reboot note to -verified support; no background setting replay after reconnect/restart. - -## Ready copy replacements (apply through reserved implementation owner) - -| Surface / current problem | Replacement | -| --- | --- | -| Dashboard “audited safety list” and stale More recovery location | “Review each app before disabling it. Enable disabled apps in Apps, or use Emergency recovery in Settings.” | -| RiskGuide “same audited classifier… three tiers” | “Check the risk and what may stop working before changing an app.” Populate tiers from canonical results, including Unknown. | -| AppDetail “safety engine flagged… Caution” | “Review what may stop working before disabling this app. You can enable it again in Apps.” Keep the specific warning. | -| Diagnostics “Force stop frees the process now” | “Force stop may interrupt the app. It may run again later.” | -| Optimize “TV is already clean” empty state | “No recommended changes were found for the installed apps.” Preserve optional-review discovery when present in the candidate. | -| Optimize “shared safety engine” introduction | “Review the selected apps and changes before applying them.” | -| Paywall “Safely disable or uninstall known TV bloat” | “Review and disable or uninstall listed TV apps.” | -| Paywall “Write tweaks — CEC, frame rate, DNS” | “Adjust CEC, frame-rate matching and DNS.” Keep actual feature availability/gating accurate. | -| Missing last-used history | “Last opened: not reported.” | -| Paywall “One payment · every future update” | “Enter a license key to unlock Pro features.” Preserve the existing activation CTA; do not invent checkout, price, renewal or update terms. | -| Frame-rate decorative numeric glyph | Implement the existing `so-vtm.5` task with a retained nonnumeric glyph. Mechanic confirms nitro has zero app edits, so there is no accepted icon candidate to integrate yet. Explain matching policy and TV/app support; never present it as measured FPS. | - -These replacements address reviewed claims, not merely punctuation. Existing -specific error causes, uninstall data-loss warnings and unavailable features must -survive. Desktop needs the same risk vocabulary and metric semantics at core -integration; its scope/reservations and gallery regeneration remain Mechanic-owned. -This pass is not a complete audit of every screen or a claim that copy is shipped. - -## Candidate and verification handoff - -`so-vtm.3` is an accepted separate navigation patch; reuse its archived source and -profile barrier. The observed `so-vtm.9` design still lists `.5` as pending and -`.7` as partial/unaccepted. Neither a hook nor an archive is integrated behavior. -Mechanic has confirmed the integration path is -`/Users/bryanroscoe/Developer/gastown/shield_optimizer/crew/mechanic`, HEAD -`48cba2384ca3938e6655b77ccf85716dd86cca5f` plus the exact uncommitted `.3` patch -(`5cb8b816f7e0e7f79b3ec8167288ad9c74c7f9ed56e7c55f463fddd8c1953a51`). Mechanic -reports all three file hashes match its accepted manifest. Source metadata is -mobile 0.1.0 (`com.atvoptimizer.mobile`) and desktop 2.1.0 -(`com.shieldoptimizer.app`). A separate read-only inspection of -`/Applications/Shield Optimizer.app/Contents/Info.plist` confirms the installed -macOS bundle reports version/build 2.1.0 and identifier `com.shieldoptimizer.app`. -This did not launch or control the app and does not pin its source commit or -identify the app/version behind the reported mobile symptoms. No installed -Android metadata was read and no device calls were made. -Do not reuse baseline test counts to claim candidate correctness. - -A read-only check of `../mechanic` found the same HEAD with three dirty navigation -candidate files (Onboarding, session tests, feedback), rather than a complete -combined integration. Its Onboarding calls `onConnected()` on saved success; -its safety fallback/test and numeric Tweaks glyph still match the findings above. -This confirms the problem also exists in that partial candidate. It does not -establish the state of another integration workspace or a delivered build. - -After the host resource hold is lifted, reserve focused checks for: mixed -saved/new discovery, stable-ID address change, reused IP with conflicting/missing -ID, cancel/profile races; unresolved `system`, hyphenated and colon process names, -shared UID, duplicate samples and absent PSS; all risk outcomes including a -Protected package present in a permissive catalog; same-package TV switch and -same-serial reconnect; consistent list/detail state after mutation; missing versus -zero/stale metrics; each supported background choice and unsupported/readback -failure. These are proposed checks, not tests executed in this review. Existing -required checks still apply to later code changes, with platform limits recorded. - -Next work: Mechanic resolves `.1` identity and `.2` canonical safety/data semantics -on the actual candidate, then reserves bounded Sol implementation for shared -details and the ready copy table. Navigator reviews that milestone against this -contract. No extra worker dispatch or overlapping application edits were made. - -Current reservations: `.1` owns Onboarding/savedDevices/session discovery tests; -`.2` owns Diagnostics, Optimize, AppDetailSheet, RiskGuide, shared safety/types, -core and desktop. Navigator is approved for disjoint documentation only in this -pass. Mechanic reports the `hq-7sj.7` dispatch hold remains pending scoped -activation from Mayor; no re-sling, worker recycling or old poller workaround. -No separate implementation permission is needed once Mechanic has the approved -runtime path and has assigned the nonoverlapping file scope. -The background control needs Mechanic's backend decision before a Tweaks code -reservation. Host activity still excludes GUI, app control, scans, ADB and heavy -checks; source and bounded lightweight work may proceed. - -`so-fb3.5` monetization remains separate under Mechanic; changing paywall wording -does not establish a purchase/issuance/restore flow or future-update terms. -`.6` is now P1: the immediate `.6.1` deliverable is a minimal local diagnostic log -with preview/export/clear, independent of a reporting backend. Its -[UX and acceptance contract](UNKNOWN-DIAGNOSTICS-UX.md) follows Mechanic's typed -collector design and `.2` canonical Unknown/process identity. `.7` assigns durable, -event-driven registry maintenance to Mechanic. Neither collection nor review -automatically promotes an entry to Safe. diff --git a/v2/mobile/FREE-SETTINGS-UNDO-DESIGN.md b/v2/mobile/FREE-SETTINGS-UNDO-DESIGN.md index 2a9ae8cd..c694216b 100644 --- a/v2/mobile/FREE-SETTINGS-UNDO-DESIGN.md +++ b/v2/mobile/FREE-SETTINGS-UNDO-DESIGN.md @@ -1,8 +1,8 @@ # Bounded Free settings undo — proposal, not implemented -Base: merged mobile release-preparation `7fe0e6a`. PR170/171 remain unchanged. This branch adds -only a current-behavior characterization test and this design; it creates no journal, command, -payment wiring or security rule. Decisions below need review before implementation. +Status: design only. The repository has a current-behavior characterization test for the gap +below, and nothing else: no journal, command, payment wiring or security rule. The decisions at the +end need review before implementation. ## Demonstrated gap diff --git a/v2/mobile/HANDOFF.md b/v2/mobile/HANDOFF.md index 1a925942..b6363d67 100644 --- a/v2/mobile/HANDOFF.md +++ b/v2/mobile/HANDOFF.md @@ -1,494 +1,244 @@ # ATV Optimizer (mobile) — HANDOFF -Read this top-to-bottom before doing any mobile work. It is the authoritative, current handoff -(lifecycle/current-authority corrections 2026-09-05 and local-integration review addendum -2026-09-08 America/Chicago; older checkpoints are historical). -Companion deep-dives (all in this dir): `ARCHITECTURE-REVIEW.md` (historical findings audit), -`FEATURES.md` (historical screen ↔ command map), **`BACKLOG.md` (current ordered queue)**, -`TRANSPORT-LICENSING-RESEARCH.md` (why the transport is what it is). Cross-session memory also lives -in `~/.claude/projects/-Users-bryanroscoe-Developer-shield-optimizer/memory/`. - -Current lifecycle evidence baseline: **`48cba23`**. The original mobile effort used -`feat/atv-optimizer-mobile`; do not infer current branch or release state from that historical -name. Bryan's feedback records an installed debug build from `main` at `44d2d66` (see -`USER-FEEDBACK-2026-09-05.md`). - -**Status as of 2026-09-12.** The coordination system that produced the `so-*` task ids referenced -throughout this file has been retired; those ids now map to GitHub issues labeled `gastown`, and -the full archive is `docs/gastown-shield_optimizer-beads-export.json`. Older "current authority" -restrictions in this document — host-only review, no commit/push/deploy — **no longer apply.** -The current policy is in the repo `CLAUDE.md`: commit and push finished work continuously. - -What has *not* changed is the physical gate. Nothing in `v2/mobile` has been verified on a phone -or TV since `44d2d66`, and the saved-TV/lifecycle behavior described below rests on host browser -evidence only. See **[`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md)** for the reproducible -matrix and `evidence/lifecycle/reproduce.mjs` to regenerate it. Restart feedback #7 remains -physically unresolved. The navigation change that opens the Dashboard directly after a saved-TV -connect is merged; its browser tests pass and it has never run on a device. - -**Saved-TV identity audit fixes (2026-09-30, GitHub #115–#118).** Hardware ids now go through one -rule, `normalizeHardwareId` in `lib/identity.ts` (trim; empty or `unknown` in any casing is no id, -the same as desktop's `idKey`), and stored placeholder ids migrate to id-less rows. Scan rows are -one per stored TV rather than one per address, with no synthesized "Saved TV" row, and a saved row -only shows "connected" when the live TV's id matches it (an id-less row matches by exact -endpoint); otherwise it shows the address as previously used. The cached -name follows the same identity rule as everything else, and advertised serials only match -exactly or with adbd's 6-character suffix. `loadHealth` can no longer wedge, Cancel no longer -reports a reconnect failure, and Diagnostics no longer blanks its safety badges when two apps -swap rank. Every fix is covered by a test reproducing the issue; none has run on a device. - -**Id-less saved rows stay distinct (2026-10-01, GitHub #146).** An id-less row (no hardware id) -used to be keyed everywhere by its bare `host:port`, so two genuinely different TVs that had -shared an endpoint at different times collapsed into one storage slot — the older one silently -lost. Every id-less row now gets its own random `localId` the first time it is persisted -(`savedDevices.ts`), and `savedDeviceKey`/`identity.ts` key id-less rows on that instead of the -endpoint; existing stored rows are migrated in place on first read, losing nothing. -`rememberDevice` only refreshes an existing id-less row when exactly one saved row matches the -connecting endpoint — when two or more already share it, which one just reconnected is not -knowable, so the connection is deliberately not persisted against any of them (writing nothing, -not a new row every time, is what stops repeat reconnects from eventually evicting a genuine saved -TV once `MAX` is reached). `discoveryRows.ts` and the Devices screen's "Other TVs" filter both go -through one shared `savedDeviceIsLiveConnection` check (`identity.ts`) so neither ever marks more -than one id-less saved row "connected" at a shared live endpoint, or hides an ambiguous row from -the list; the discovery row shows a generic name and both saved rows report "saved-address" -instead. Covered by new tests in `savedDevices.test.mjs` and `discoveryRows.test.mjs`; unverified -on a device. - -**A different id-less device can no longer inherit the one saved row at its address (2026-10-01, -GitHub #154).** The above fixed the two-or-more-id-less-rows case; it left the original, more -common one: a lone id-less saved row at an address was still trusted on endpoint alone, so a -*different* id-less TV that later answered there silently took over that row's name. Id-less rows -now carry an optional soft `fingerprint` (model/manufacturer/codename/user-set name) captured from -the live device's properties; `rememberDevice` only refreshes a lone match when the saved and live -fingerprints don't clearly disagree (different model or manufacturer). A disagreement is never -proof either way, so it never upgrades a match to "verified" -- it only ever rules one *out*, -recording the connection as a new row and leaving the old one untouched. A missing field on either -side is unknown, not a disagreement, so older rows migrate for free. `session.identityNote` ("A -different device is now at this address.") surfaces the mismatch once in whichever screen's -connect flow notices it. Covered by new tests in `savedDevices.test.mjs`, verified to fail before -the fix; unverified on a device. - -**Desktop parity pass (2026-10-06, branch `mobile-parity-pass`).** Mobile caught up with the -desktop features that shipped in v2-2.3.0/2.3.1. **None of it has run on a phone or TV yet**; -the evidence is the browser suites plus 384×812 screenshots in -`evidence/parity-2026-10-06/`. What changed: - -- *Navigation:* Tweaks is a bottom tab; Optimize is a screen pushed from the Dashboard's single - Optimize call to action, and Back returns to whichever screen opened it. The Dashboard gains - Tweaks and Launcher tiles; More is titled More and lists Optimize. -- *Optimize:* search by name or package id, tap a row for the app sheet, desktop's - `recommendation()` defaults and "Review: … if unused" pill. -- *Diagnostics:* desktop's Suggestion column. A process is only treated as an app once the TV - confirms the package is installed (`safety_info`); anything else goes to the catalog-free - `process_safety_info`, and an unreadable installed list reads "Not checked". -- *Snapshots:* marked Beta; "Preview restore" lists every row with a Now column from the plan's - `current_values`/`current_launcher`, and says why a missing launcher is skipped. -- *Apps:* a Recognised section lists catalogued apps (so `com.google.android.katniss` is findable - without Pro), one search over name and package, Hide not installed, the detail sheet's storage - figures with re-measure, Report this app (#100) and a long-press action menu (#129). -- *Launcher:* Setup Wraith card (off / on / risk, Turn it off, Re-enable), Advanced "set another - app as Home", a separate confirmed Disable stock launcher step, a re-read after every action - (#157/#158), Play Store install polling, and Source site links that open in the phone's browser. -- *Tweaks:* Nvidia System Hooks, Remote Assistant Button (mic permission), Screensaver (Basic - Daydream / Off / Restore previous), the 720p display preset, regrouped sections. -- *Files:* Find app backups from desktop's catalog, copy a hit to the phone, and TV-side delete - under `/sdcard`. The find/delete logic moved to core `commands::files`, shared with desktop, and - fixed desktop's #86 message for missing backup folders. -- *Plumbing:* `tauri-plugin-opener` (capability scoped to http/https; `api.openUrl` also rejects - other schemes), `types.ts` aligned with core, icon font re-subset, notices regenerated. - -Still not on mobile: **phone→TV upload** (needs an Android document picker / SAF), **install APK / -sideload**, the **bug-report bundle** (`collect_diagnostics`), and a **patch-notes viewer**. The -opener's Android library (`androidx.browser`) is not yet listed in the hand-maintained part of -the notices, which covers no Gradle dependencies at all. - ---- - -## Reconnect safety follow-up (2026-10-15) - -Launcher invalidates reads, picker/confirmation intent, polling and action continuations on -session generation or liveness changes, including reconnection to the same endpoint. The -session generation is reactive so screens observe a reconnect even if its liveness transitions -complete within one render. Tweaks keeps screensaver originals by normalized verified hardware -ID, falling back to connection generation when identity is unavailable. Multi-setting writes -stop before the next write if that connection changes. Already dispatched writes cannot be -recalled. These changes have simulated browser coverage only, not phone/TV validation. -See `evidence/reconnect-20261015/README.md` for reproducible tests and videos. +Read this top-to-bottom before doing any mobile work. It is the authoritative description of the +current state. Companion docs in this directory: + +- **[`BACKLOG.md`](BACKLOG.md)** — the ordered queue of remaining work. +- [`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md) — the app-restart report (#105): host matrix and + the physical evidence still needed. +- [`PAIRING-PLAN.md`](PAIRING-PLAN.md) — the Android 11+ code-pairing protocol and its manual test. +- [`FAST-REMOTE-PLAN.md`](FAST-REMOTE-PLAN.md) — the scrcpy control channel and its open device gates. +- [`RELEASE.md`](RELEASE.md), [`LICENSING.md`](LICENSING.md), + [`ANDROID-DEPENDENCIES.md`](ANDROID-DEPENDENCIES.md), [`DATA-INVENTORY.md`](DATA-INVENTORY.md) — + signing, offline license keys, Android notices and the privacy-disclosure inventory. +- [`BILLING-IMPLEMENTATION-PLAN.md`](BILLING-IMPLEMENTATION-PLAN.md), + [`BILLING-COMMAND-MATRIX.md`](BILLING-COMMAND-MATRIX.md), + [`FREE-SETTINGS-UNDO-DESIGN.md`](FREE-SETTINGS-UNDO-DESIGN.md) — planned Play billing, the current + Free/Pro gate of every command, and the proposed Free undo. +- [`EXPORT-DISCOVERABILITY.md`](EXPORT-DISCOVERABILITY.md) — where every saved file and report + actually goes, and the proposed Android export scope. + +Cross-session agent memory also lives in +`~/.claude/projects/-Users-bryanroscoe-Developer-shield-optimizer/memory/`. + +## Status + +The app is unreleased (`package.json` version `0.1.0`) and builds an unsigned debug APK. + +**Nothing in the current `v2/mobile` code has been verified on a phone or TV.** Earlier device +sessions (a Pixel 10 Pro against two Nvidia Shields) proved the transport, the fast-remote channel +and legacy network-debugging connect, but they predate most of today's code. Everything else +described here rests on host evidence: Rust unit tests, and a browser suite that drives the real +Svelte screens against a mocked Tauri layer. That is worth a lot, but it is not a device. Never +describe a mobile behavior as device-verified unless a physical session actually exercised it. + +The open physical gates are GitHub [#93](https://github.com/bryanroscoe/shield_optimizer/issues/93) +(saved-TV discovery and reconnect), [#105](https://github.com/bryanroscoe/shield_optimizer/issues/105) +(the app-restart report), code pairing against a real adbd, and fast-remote Phase 4. See +`BACKLOG.md` P0. ## 1. What this is + A phone/tablet app (**Tauri 2 + Rust + Svelte 5**, in `v2/mobile/`) that drives an **Android TV -over wireless ADB** — no PC. Debloat / optimize / launcher / tweaks / snapshots / remote, etc. -Monetized **freemium**; the owner (Bryan) **wants to sell it** (this drove the transport rewrite — -see §3). Product name **ATV Optimizer**. It shares the audited engine (`v2/crates/core`) with the -mature **desktop** app (`v2/src-tauri` + `v2/src`); the rule is *reuse the desktop, keep core -aligned, never fork it*. - -## 2. Implementation checkpoints (validation applies to the cited checkpoint) -- **Full frontend re-architecture** (`b7c3d75`): shared typed `api.ts`/`types.ts` (mirror desktop - + core), a central `invoke` wrapper with a debug-log ring buffer (`lib/log.ts`), a runes - **session store** (`lib/session.svelte.ts` — device/host/entitlement/liveness + health/bloat - cache), a real **router with a screen stack + Android hardware-back** (`lib/router.svelte.ts`, - fixed the Home-tab→blank bug), and the desktop fast-load patterns (lazy per-screen load, - invalidate-on-mutation, optimistic UI, progressive enrichment). -- **Never-fake-data + trust fixes**: Dashboard shows a real error+retry on a failed health load - (no fabricated "healthy" device); Diagnostics/Optimize render `—`/skeleton/error, never fake - numbers; safety tags come only from core `safety_info` (no inline classifier). -- **Transport swapped to pure-Rust `adb_client`** (`3e8bcfc`) — see §3. This is the biggest change. -- **All screens built** (14 total): Onboarding (+Reconnect/saved-TV), Dashboard, Diagnostics, - Optimize, Apps (+AppDetailSheet), Remote, More, Launcher, Tweaks, Snapshots, Devices, RiskGuide, - Files, Backups. Components: BottomTabs, Toast, ConfirmDialog, FindRemoteButton (one consistent - "ring the remote?" affordance, Shield-gated), BrandMark, PaywallSheet, AppDetailSheet. -- **Licensing**: `activate_license` persists then flips backend entitlement; - `LOCKED:<feature>` errors route to PaywallSheet. Signed offline validation is - implemented (see the 2026-09-04 checkpoint below); `ATVOPT-PRO-2025` is a - **debug-build-only** test key. Checkout/key issuance remains release work. -- **Icon + branding**: launcher icon and in-app BrandMark are the designer's exact glyph — a - TV/monitor on a stand with three **vertical faders** (exact SVG in `src-tauri/icons/icon.svg`). -- **Historical device evidence**: earlier Pixel/Shield sessions connected through the new - transport and exercised specific flows. Bryan later reported usability issues on the - debug build from `main` at `44d2d66`. Neither establishes complete current physical - validation; see `USER-FEEDBACK-2026-09-05.md` and the remaining device gates. -- **Fast remote Phases 1–3**: a pinned generic raw ADB service stream, embedded scrcpy v3.1 server, - Android session registry/lifecycle, and transparent same-request shell fallback are implemented. - The Bedroom Shield gate measured 30 warm channel presses at 75.2 ms p95 and app force-stop left - no resident server/socket. Full results and remaining gates are in `FAST-REMOTE-PLAN.md`. -- **Correctness checkpoint (2026-07-13)**: serial-bound ADB operations, serialized connection - lifecycle, finite liveness-probe reads, acknowledged reboot semantics, non-evicting local file - failures, disconnect/auto-reconnect and stale-cache race fixes, fail-closed app safety UI, - snapshot cross-device warnings, secret/remote-text log redaction, transactional live entitlement, - and a working Optimize apply/progress path. Exact scope and remaining findings are in - `BACKLOG.md`. -- **Follow-up correctness checkpoint**: direct ADB uses tested shell-v2 stderr/exit framing; - SmartTube backup discovery covers its current Documents export path (GitHub #86); desktop file - browsing discards stale responses; and Android fast-remote failure cleanup plus hold-repeat - backpressure are hardened. -- **State-isolation checkpoint**: malformed saved TVs are normalized safely; Diagnostics labels - stale metrics after a failed refresh and isolates safety lookups; Tweaks requests cannot overwrite - another device, mutations capture their target serial, and partial failures force a real reload. -- **APK backup correctness**: new backups contain every `pm path` APK in a versioned bundle and can - be restored together; restore paths are confined to app storage, remote staging is always cleaned, - and legacy base-only files remain visible but are explicitly not restorable. -- **APK size polish**: `scripts/subset-material-symbols.py` derives the bundled icon font from Svelte - references and validates retained ligatures (5.34 MB to about 111 KB). The license-key field also - disables mobile autocorrect, spellcheck, and capitalization. -- **Background remote lifecycle checkpoint (2026-08-01)**: Android keeps the fast-remote channel for - a 30-second background grace, cancels stale cleanup on resume, and serializes session startup with - teardown. Host/frontend gates, the aarch64 APK build, and Android-target clippy are green; the - physical two-Shield grace/concurrency matrix is still required. -- **Connection and device-history polish (2026-08-01)**: first-time authorization now waits up to - 30 seconds and turns transport failures into guidance to approve the TV's "Allow debugging?" - prompt. Saved TVs retain their last real friendly name when ADB ports rotate, keep 16 recent - hosts, and can be selected directly from the Dashboard device menu. Diagnostics now charts used - RAM so the bar fills as memory usage increases. -- **Focused UI validation and cached-label follow-up** (`b293b66`, `3eccc34`): a 384×812 - Playwright/WebView pass exercised the real Svelte screens with Tauri invokes stubbed. It verified - the previous-TV menu, a successful TV switch, the authorization-prompt guidance, and a - `3072 / 4096 MB used` RAM bar at 75%. That pass caught one real gap: after switching, the header - could fall back to the generic model name. `3eccc34` makes the session label prefer a reported - friendly name, then the durable cached name. `npm run check` remained 0/0 and `npm run build` - passed after the fix. - -- **Stability reset (2026-09-04)** — a four-track code audit (feature parity vs v1/desktop, - connection lifecycle, screen-by-screen UX, Rust backend) followed by a fix sweep. Root causes of - the reported "looks connected but isn't / laggy / reconnects too often" symptoms were in the - transport, not the UI: - - **Infinite reads.** The vendored `adb_client` default read timeout was `u64::MAX`, and every - normal shell call used it while holding the single connection mutex. A silently dropped - socket (AP roam, TV asleep) froze every later command and the liveness probe behind it. Now: - 30 s inactivity per shell response (180 s for installs/clears), a 120 s safety net inside the - vendored crate, a 5 s TCP connect timeout, and only socket-level errors evict the connection - (a missing remote file used to disconnect the TV). - - **Identity behind the device mutex.** `wireless_status`/`list_devices` blocked a tokio worker - while any command ran. The identity is now mirrored outside the mutex. - - **Poisoned mutex = brick.** A short sync packet could panic under the lock; connect then failed - forever with "lock poisoned". Locks recover, and the two slices are bounds-checked. - - **Stray packets killed the connection.** A late CLSE/WRTE from a finished stream failed the - next command; readers now skip a bounded number of foreign-stream messages. - - **Probe race.** The status probe issued a second disconnect after eviction, which could drop a - reconnect to the same host that had just succeeded. Removed. - - **Serialized fan-out.** health/apps/optimize/launcher/display reads are one sentinel-batched - shell each (`crates/core/src/adb/batch.rs`), 19 → 6 round-trips on the hot paths. - - **Frontend liveness.** Any command error carrying the backend's "Connection to the TV was - lost" prefix flips the session (`lib/connectionEvents.ts`), which silently redials the same TV - once and only then shows the global `ConnectionBanner` (Retry / Switch TV). A 45 s heartbeat - probes while the app is visible. - - **No surprise auto-connect.** Launch shows a saved-TV picker; it auto-dials only when exactly - one TV is saved and the last session wasn't ended by an explicit Disconnect (persisted in - `atv.autoConnect.v1`), always naming the target with a Cancel button. "Add a TV" from Devices is - a separate `addtv` route that never dials. The Android back button keeps exactly one spare - history entry, so it no longer eats dead presses at the root. - - **Honest state.** Dashboard shows the real "N of M recommended-bloat apps active" instead of a - floored invented score; screenshots preview inline; Settings (was "More Options") gained - Emergency recovery (`panic_recovery`, previously registered but unreachable), reboot to - recovery/bootloader, and confirmations on Disconnect. - - Entitlement default is now Free (fail-closed); desktop opts into Pro explicitly. - Screen-level fixes from the same sweep are listed in `BACKLOG.md` under "Stability reset". - -- **Backlog pass (2026-09-04, same day)** — device-less P1/P2 items: - - **Signed licensing.** `ATVOPT-<payload>-<signature>` Ed25519 keys (Crockford base32, - case-insensitive), verified offline against embedded public keys; the dev key - `ATVOPT-PRO-2025` works in **debug builds only**. `tools/atvopt-license` (`keygen` / `issue` / - `verify`); private keys live in `~/.atvopt/` outside the repo. Settings shows licensee + term - via `license_info`. See `LICENSING.md`. - - **Release plumbing.** Gradle release signing from `keystore.properties` or `ATVOPT_KEYSTORE_*` - env, `scripts/bump-version.sh`, `cargo about` notices (no GPL family in 293 crates) shown under - Settings › About. See `RELEASE.md`. - - **Follow-ups.** `remote_warm` starts the fast-remote channel when the Remote tab opens; - snapshot reads batched; `pull_file` capped at 2 GiB with de-duplicated names; device profile - harvests `ro.serialno`. In the Mechanic-local `so-fb3.1.2` integration, equal hardware IDs are - the only cross-endpoint saved-TV identity and ID-less rows match only exact host+port. Conflicting - or newly identified histories remain separate, so two rows can share an address and single-TV - auto-dial is then suppressed. The integrated-local `so-fb3.1.3` follow-up now keys progress and - errors by saved identity, filters the current TV by verified hardware ID or exact ID-less endpoint, - shows host:port plus neutral shared-address guidance, and forgets the selected identity. Mechanic's - combined saved-device/diagnostics tests passed 25/25, mobile check reported 0 errors/0 warnings, - and the build passed; browser, device, publication, and release remain unverified. - Closing out `so-fb3.1`, the scan-row reconciliation (`lib/discoveryRows.ts`) now has its own - pure test suite (11 cases: repeated advertisements, mixed saved/new hosts, live-endpoint - identification, address and port changes, pairing-only hosts, colliding saved identities, - malformed records, `adb-…` instance names, legacy vs TLS ports). A saved endpoint whose address - answered the scan on a different port reports `saved-other-port` ("This address answered on - another port") instead of claiming it was not found — a rotated wireless-debugging port is not - evidence the TV is offline. Desktop was checked and judged unaffected at the time: it - enumerates devices through the adb binary's own serials and keeps no saved-network-device - list. That premise was right and the conclusion was wrong — "no saved list" meant "no dedupe - mechanism", not "immune". Once desktop discovery began dialling mDNS-advertised endpoints, - adb held two transports for one device and the desktop list faithfully rendered both. Desktop - now collapses rows on verified hardware id, using the same identity rule as this app. Mobile - gates: - tests 78/78 at the time (the suite is now 99), check 0 errors/0 warnings, build passed. Physical device verification of the - reconnect flow is still the open gate on the parent bead. - - **Code pairing (SPAKE2) — implemented, unverified on a device.** Clean-room Rust in the - vendored crate (`vendor/adb_client/src/message_devices/tcp/pairing/`), wired through - `WirelessAdb::pair` and the Onboarding pair step. Key finding: BoringSSL's SPAKE2 is a bespoke - variant (32-byte messages, SHA-512 transcript, its own M/N seeds, cofactor tricks) that is - wire-incompatible with the RustCrypto `spake2` crate, so the variant is implemented directly on - `curve25519-dalek` and checked against an independent Python model plus a full loopback. - Every protocol constant is cited to an AOSP file in `PAIRING-PLAN.md`, which also lists the - exact manual test (the Pixel's own pairing service first, then a Google TV). Also fixed a - pre-existing encoder bug: `android_pubkey_encode` dropped a zero top byte (~1 key in 256 - rejected by adbd for AUTH and pairing). - -- **Navigator-reviewed integrations (2026-09-08) — all shipped in `d02de57`, 2026-09-10:** - - **Canonical Unknown safety (`so-fb3.2.1`–`.2.4`).** Mechanic combined the accepted shared - contract, mobile rev3 consumers, desktop round-three consumers, and Navigator documentation in - `crew/mechanic`. Canonical safety now preserves Protected and Caution precedence and otherwise - reports Unknown with a reason; malformed, failed, or stale lookup remains unavailable rather than - becoming Unknown. Mobile and desktop removal paths require current installed-package identity and - canonical evidence, do not auto-select Unknown, and keep unresolved memory/process rows inspect-only. - Mechanic's integrated gates passed: core 199 tests plus clean clippy, desktop Tauri 36 tests, mobile - tests 25/25 with check 0/0 and build, desktop check 0/0 and build, and independent mobile 13/13 and - desktop 25/25 harnesses. `so-fb3.2.1` through `.2.4` are closed; the parent remains open for - actual process attribution. Browser sessions, desktop gallery regeneration, cross-OS CI, device, - publication, and release remain unverified. Exact hashes are recorded in - `mayor/artifacts/hq-b1t/shield-so-fb3.2-integration-record.txt`. - - **Unknown app diagnostics (`so-fb3.6.1`).** The mobile Apps screen records completed, - current-session uncatalogued-package results in a bounded local-only collector. Settings can - review a refreshable JSON snapshot, copy it explicitly, and clear it after confirmation; the - copy states that nothing is sent automatically and identifies the fields included before - sharing. The collector allowlists fields, rejects address/path/control-like tokens, - deduplicates observations, caps records/bytes/count, and makes clear win over queued saves. - Mechanic's focused collector tests passed 9/9; isolated TypeScript, Svelte compile, mobile - check (0 errors/0 warnings), and build passed. Runtime unresolved-process collection remains - deferred until the parent `so-fb3.2` process-attribution scope supplies typed identity; - `registry_version` is currently unavailable. - No browser, device, automatic upload, physical, or release claim. - - **Frame-rate presentation (`so-vtm.5`).** The misleading numeric `30fps_select` decoration is - replaced by the already-retained nonnumeric `sync_alt` glyph and marked `aria-hidden`. - Subordinate copy reports the current Never / Seamless only / Always / device-default policy - and says matching depends on TV, app, and content support; it does not present measured FPS. - Mechanic's mobile check passed with 0 errors/0 warnings and build passed. No 384 px render, - device verification, installed-build confirmation, physical matching claim, or release claim. - -## 3. THE transport story (most important context) -Originally the transport was **libadb-android (GPLv3)**, a Kotlin lib called over a JNI plugin. -Two fatal problems: (a) **GPLv3 blocks selling** a closed-source product; (b) **unreliable** — its -`exec:` streams hung on an older Nvidia Shield's adbd waiting for a CLSE that never came. -**We replaced it with `adb_client`** — a **third-party MIT-licensed pure-Rust crate** (Corentin -Liaud / `cocool97`, crates.io v3.2.2). We did NOT write it. It reimplements the ADB client -protocol (connect, RSA auth, stream multiplexing, shell/exec/push/pull) in pure Rust. Proven: -a spike ran every previously-hanging command cleanly on the real Shield; it cross-compiles to -aarch64-Android; the clean APK has **zero GPL native libs** (only our `libatv_optimizer_mobile_lib.so`). -- `WirelessAdb` (`src-tauri/src/wireless_adb.rs`) now owns an `adb_client::tcp::ADBTcpDevice` - (blocking calls via `spawn_blocking`), implementing the core `AdbDriver` trait. The pinned direct - TCP path now uses shell-v2 framing for real stdout/stderr/exit status, with shell-v1 fallback for - older devices; the Bedroom Shield live test returned separate output streams and exit code 7. - A persistent **RSA key is generated/persisted in `app_data_dir`** - (`ensure_adb_key`, PKCS#8 PEM) — first connect prompts "Allow debugging" on the TV, then silent. -- The **Kotlin plugin (`tauri-plugin-atv-adb`) is now mDNS-discovery ONLY** (NsdManager, a pure - Android framework, no GPL). libadb/Conscrypt/BouncyCastle Gradle deps + the jitpack repo are gone. -- **Legacy network-debugging (`:5555`, no pairing code) works now** — that's the proven path. -- **Code pairing:** the vendored crate now implements the Android-11 SPAKE2 flow and - `wireless_pair` is wired to it. Host model/loopback checks exist; real adbd pairing remains - unverified. Use `PAIRING-PLAN.md` for the physical gate and Network debugging as the - fallback. Generated third-party notices are available in Settings › About. - -## 4. Repo layout (mobile) -- `v2/mobile/src/` — Svelte 5 frontend. `lib/{api,types,session.svelte,router.svelte,log,savedDevices}.ts`, - `screens/*.svelte` (14), `components/*.svelte` (8), `app.css` (design tokens + offline @font-face). -- `v2/mobile/src-tauri/src/` — mobile Tauri app: `lib.rs` (builder + `generate_handler!`), - `wireless_adb.rs` (adb_client transport = `AdbDriver`), `wireless_commands.rs` (wireless_*), - `file_commands.rs` (list_remote_dir/pull_file/find_files/delete_path/backup_apk/list_backups/ - restore_apk_backup/delete_backup; find/delete wrap the shared `crates/core` `commands::files`). -- `v2/mobile/tauri-plugin-atv-adb/` — mDNS discovery plugin (Kotlin NsdManager + thin Rust). -- `v2/vendor/adb_client/` — vendored MIT transport; `SHIELD-OPTIMIZER-PATCH.md` lists every local - change (raw service stream, finite timeouts, stray-stream tolerance, `tcp/pairing/`). +over wireless ADB** with no PC: debloat, optimize, launcher, tweaks, snapshots, files, APK +backups, remote and diagnostics. Product name **ATV Optimizer**. It is sold **freemium as a +closed-source product**, which constrains the transport (§2). It shares the audited engine +(`v2/crates/core`) with the desktop app (`v2/src-tauri` + `v2/src`); the rule is *reuse the +desktop, keep core aligned, never fork it*. + +Mobile has desktop parity for Optimize (search, app sheet, desktop's recommendation defaults), +Diagnostics (Suggestion column; a process counts as an app only once the TV confirms the package is +installed), Snapshots (marked Beta; Preview restore with a Now column), Apps (Recognised section, +storage details, Report this app, long-press menu), Launcher (Setup Wraith card, set any app as +Home, a separate confirmed Disable stock launcher step, re-read after every action), Tweaks, and +Files (find app backups via shared core `commands::files`, copy to phone, TV-side delete under +`/sdcard`). Links open in the phone's browser through `tauri-plugin-opener`, scoped to http/https. + +Not on mobile yet: phone→TV upload (needs an Android document picker), install APK / sideload, the +bug-report bundle (`collect_diagnostics`), and a patch-notes viewer. + +## 2. Transport + +- **`WirelessAdb`** (`src-tauri/src/wireless_adb.rs`) implements the shared `AdbDriver` trait on top + of the vendored, MIT-licensed, pure-Rust **`adb_client`** (`v2/vendor/adb_client`, upstream by + Corentin Liaud / `cocool97`; every local change is listed in its `SHIELD-OPTIMIZER-PATCH.md`). + It talks straight to the TV's adbd: no adb binary, no adb server. Blocking calls run on + `spawn_blocking`. +- Capabilities: `shell` (shell-v2 framing for real stdout/stderr/exit status, shell-v1 fallback + for older devices), `raw_bytes`, synthesized `devices`/`disconnect`/`reboot`, push/pull through + `raw_transfer`, and dedicated byte-stream services (used by the fast remote). `forward`, + `connect`, `pair` and `spawn` through `raw` are unsupported. +- A persistent RSA key is generated in `app_data_dir` (`ensure_adb_key`, PKCS#8 PEM). The first + connect prompts "Allow debugging" on the TV; later connects are silent. +- Every read is bounded: 30 s shell inactivity (180 s for installs/clears), a 120 s safety net + inside the vendored crate, a 5 s TCP connect. Only socket-level errors evict the connection; a + missing remote file does not. Readers skip a bounded number of stray packets from finished + streams, locks recover from poisoning, and the device identity is mirrored outside the device + mutex so status calls never wait behind a running command. +- Hot read paths (health, apps, optimize, launcher, display) are one sentinel-batched shell each + (`crates/core/src/adb/batch.rs`). +- **The Kotlin plugin (`tauri-plugin-atv-adb/`) is mDNS discovery only** (Android `NsdManager`, no + third-party native libraries). +- **Legacy network debugging (`:5555`, no code) is the proven path.** Android 11+ code pairing is + implemented in the vendored crate (`vendor/adb_client/src/message_devices/tcp/pairing/`) and wired + through `WirelessAdb::pair`, but has never run against a real adbd. Onboarding keeps pointing + users at Network debugging as the fallback until `PAIRING-PLAN.md`'s manual test passes. + BoringSSL's SPAKE2 is a bespoke variant that is wire-incompatible with the RustCrypto `spake2` + crate, so it is implemented directly on `curve25519-dalek`. +- **No GPL code, anywhere in the transport.** The app is sold closed-source, so GPLv3 libraries + such as `libadb-android` are out. Google's prebuilt platform-tools `adb` is not an option + either: it is a host binary (no Android arm64 build), and the Android SDK License Agreement + (§3.4) bars redistributing it. Pairing was written clean-room from Apache-2.0 sources (AOSP + `packages/modules/adb`, BoringSSL `spake25519.cc`) and permissive crates. `cargo about` + generates the notices (`scripts/gen-notices.sh`); rerun it when adding a crate and keep the GPL + family out. These licensing conclusions are engineering judgment, not legal advice; have counsel + confirm them before public sale. +- **Fast remote**: a scrcpy control-only channel over its own authenticated connection, with + transparent same-request fallback to `adb shell input`. Android keeps the channel through a + 30-second background grace. Details and open gates: `FAST-REMOTE-PLAN.md`. + +## 3. Behavior that must not regress + +- **Saved-TV identity.** A verified hardware id is the only identity that crosses endpoints; + never merge, rename or forget on IP evidence alone. `normalizeHardwareId` in `lib/identity.ts` + (trim; empty or `unknown` in any casing is no id, matching desktop's `idKey`) is the one rule. + Scan rows are one per stored TV, never per address. An id-less row gets its own random `localId` + and matches only its exact endpoint. When two or more id-less rows share the connecting endpoint, + `rememberDevice` writes nothing, because which one reconnected is unknowable. Id-less rows carry a + soft `fingerprint` (model/manufacturer/codename/friendly name); a clear model or manufacturer + disagreement records the connection as a new row and raises `session.identityNote` ("A different + device is now at this address."), shown globally from `App.svelte`. A fingerprint can only rule a + match out, never verify one, and a missing field is unknown rather than a mismatch. + `savedDeviceIsLiveConnection` is the one check for "this saved row is the live connection". The + silent one-shot recovery runs the same identity check as an explicit reconnect. +- **Connection lifecycle.** Any error carrying the backend's "Connection to the TV was lost" + prefix flips the session (`lib/connectionEvents.ts`): one silent redial of the same TV, then the + global `ConnectionBanner` (Retry / Switch TV). A 45 s heartbeat probes while the app is visible. + Launch shows a saved-TV picker and auto-dials only when exactly one TV is saved and the last + session wasn't ended by an explicit Disconnect (`atv.autoConnect.v1`), always naming the target + with Cancel. "Add a TV" is a separate `addtv` route that never dials. The Android back button + keeps exactly one spare history entry. +- **Reconnect safety.** The session generation is reactive. Launcher, app sheets and file actions + discard reads, picker intent, polling and continuations when the generation or liveness changes, + including a reconnect to the same endpoint. Tweaks keys screensaver originals by verified + hardware id (falling back to connection generation), and multi-setting writes stop before the + next write when the connection changes. A write already dispatched cannot be recalled. +- **Never fake data.** Failed loads show an error with retry, never a fabricated healthy device or + placeholder metrics. Safety tags come only from core `safety_info`; Unknown (uncatalogued) is + distinct from unavailable (the lookup failed), and Unknown is never auto-selected. +- **Entitlement fails closed.** The default is Free; desktop opts into Pro explicitly. Shared Rust + gates return `LOCKED:<feature>` before side effects, and `require_pro` runs after the safety + check. +- Screenshots, pulled files, APK bundles and snapshots all land in app-private storage with no + export (`EXPORT-DISCOVERABILITY.md`). Copy text never claims to have saved a file. + +## 4. Licensing and commerce + +- Implemented: signed offline license keys (`ATVOPT-<payload>-<signature>`, Ed25519, verified + against public keys compiled in; `crates/core/src/license.rs`, `tools/atvopt-license`). The dev + key `ATVOPT-PRO-2025` works in **debug builds only**. Private signing keys live in `~/.atvopt/` + outside the repo; back up `~/.atvopt/license-signing-key.prod`. See `LICENSING.md`. +- Planned, not built: one-time Play Pro with free recovery (`BILLING-IMPLEMENTATION-PLAN.md`) and + bounded Free undo (`FREE-SETTINGS-UNDO-DESIGN.md`). `EntitlementPreview` is an isolated + test-only reducer and does not authorize anything. `BILLING-COMMAND-MATRIX.json` is checked by + `tests/entitlement-preview.test.mjs` against the registered commands. + +## 5. Repo layout + +- `v2/mobile/src/` — Svelte 5 frontend: `screens/` (14), `components/`, and `lib/` (typed + `api.ts`/`types.ts` mirroring desktop and core, the runes `session.svelte.ts` and + `router.svelte.ts`, `savedDevices.ts`/`identity.ts`/`discoveryRows.ts`, `log.ts` debug ring + buffer, `safety.ts`), `app.css` (design tokens + offline `@font-face`). +- `v2/mobile/src-tauri/src/` — `lib.rs` (builder + `generate_handler!`, license persistence), + `wireless_adb.rs` (transport), `wireless_commands.rs` (`wireless_*`), `file_commands.rs` + (remote listing, pull, find/delete via core, APK backup/restore), `remote_lifecycle.rs` (Android + background grace), `scrcpy_resource.rs`. +- `v2/mobile/src-tauri/gen/android/` — the generated Android project, tracked **with hand edits** + (icons, `AndroidManifest.xml`, `strings.xml`, `build.gradle.kts` signing config). Don't rerun + `tauri android init`; it would overwrite them. +- `v2/mobile/tauri-plugin-atv-adb/` — the mDNS discovery plugin (Kotlin `NsdManager` + thin Rust). +- `v2/vendor/adb_client/` — vendored MIT transport, outside the Cargo workspace. - `v2/tools/atvopt-license/` — license key generator/issuer/verifier CLI. -- `v2/crates/core/src/adb/batch.rs` — sentinel-batched shell helper; `crates/core/src/license.rs` - — signed license verification. -- Mobile docs: `HANDOFF.md` (this), `BACKLOG.md`, `FAST-REMOTE-PLAN.md`, `PAIRING-PLAN.md`, - `LICENSING.md`, `RELEASE.md`, `THIRD-PARTY-NOTICES.md` (generated). -- `v2/crates/core/` — SHARED engine+commands (pure `engine/`, `commands/*`, `adb/{driver,parse}`, - `license.rs`). Desktop and mobile both register from here. **Keep `engine/` pure; keep aligned - with desktop.** +- `v2/crates/core/` — the shared engine and commands. **Keep `engine/` pure; keep it aligned with + desktop.** - Design source of truth: `~/Downloads/Shield Optimizer mobile design (1)/ATV Optimizer Mobile.dc.html` - (20 screen sections; lime `#C9F24E`, Geist/Geist Mono). The `claude_design` MCP is NOT available - in this environment — work from the downloaded folder. - -## 5. What REMAINS - -Use **[`BACKLOG.md`](BACKLOG.md)** as the ordered source of truth. Earlier host/APK -checkpoints are recorded in `CORRECTNESS-AUDIT-2026-09-05.md`; Bryan's later -`44d2d66` install is owner-reported, not a completed physical stability matrix. -The current next steps are: - -The 2026-09-08 integrations above exist only in Mechanic's local integration workspace. -Do not describe any as shipped. The diagnostics report has no automatic network path; explicit -clipboard JSON is the current export. Keep unmatched-process collection pending until canonical -typed identity exists instead of inferring process/package identity in the frontend. - -1. **Keep restart feedback #7 open on `so-vtm.8`.** Navigator's 13 focused browser - lifecycle cases, 9 existing regressions, mobile check/build, and 3 pure lifecycle - tests passed at `48cba23`. Visibility-only resume retained the screen; JavaScript - reconstruction reset it and required a fresh connection/Connected confirmation. - Use `LIFECYCLE-EVIDENCE.md` for the precise limits and minimal owner report. - Physical investigation is the open gate: this needs a real phone and TV, and nothing - here has run on either since `44d2d66`. -2. **Verify code pairing on a real device** (BACKLOG P1 #2). Follow `PAIRING-PLAN.md` exactly: - the Pixel's own `_adb-tls-pairing` service from a host binary first, then a Google TV, including - the wrong-code and silent-reconnect checks. Until this passes, treat pairing as unverified and - keep the Onboarding copy pointing users at Network debugging as the fallback. -3. Finish the fast-remote Phase 4 device gates (`FAST-REMOTE-PLAN.md`). -4. SAF import/export for screenshots, pulled files and APK bundles (they land in app-private - storage today), then Drive. -5. Product decisions for Bryan, listed under BACKLOG P1 #6: free read-only Optimize plan; the - unused `Feature::FileManager` / `Feature::BackupClone` gates. -6. Release: create the real keystore (or enroll Play App Signing), a signed-AAB CI job, and a - checkout/store that issues license keys with `tools/atvopt-license` (`RELEASE.md`, - `LICENSING.md`). Back up `~/.atvopt/license-signing-key.prod`. - -Desktop rebranding remains a separate migration because of the MSI UpgradeCode risk. - -### Next-agent start checklist (updated 2026-09-05) - -- Record the actual branch, HEAD, and dirty paths; preserve existing work and respect - mechanic's file reservations. Read `BACKLOG.md` (P0 first), `LIFECYCLE-EVIDENCE.md`, - `PAIRING-PLAN.md` if touching pairing, and the Phase 4 section of `FAST-REMOTE-PLAN.md`. -- Gates (all must pass; run from `v2/`): `cargo fmt --check`; `cargo clippy -p shield-optimizer-core - -p shield-optimizer-v2 -p atv-optimizer-mobile -p tauri-plugin-atv-adb -p atvopt-license - --all-targets -- -D warnings`; `cargo test -p shield-optimizer-core -p atv-optimizer-mobile - -p atvopt-license` (179 / 21 / 2 at handoff); `cd vendor/adb_client && cargo test --lib` (54; - the vendored crate is outside the workspace). From `v2/mobile`: `npm run check` (0/0) and - `npm run build`. Android-target clippy works with the NDK clang exported as - `CC_aarch64_linux_android` / `CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER`. -- Reproducible device-less UI loop from `v2/mobile/`: `npm test` runs the existing browser - regressions; `node --test evidence/lifecycle/reproduce.mjs` runs the focused lifecycle matrix. - Both use mocked Tauri on Vite-compiled source. Run `npm run check` and `npm run build` - separately. These do not verify native Android callbacks or an installed APK. -- If you add a Material Symbols icon name, rerun `scripts/subset-material-symbols.py` (needs - `fonttools` + `brotli` in a venv) or the icon renders as literal text. If you add a crate, - rerun `scripts/gen-notices.sh`. -- The dev license key `ATVOPT-PRO-2025` unlocks Pro in **debug builds only**. For a release build - issue a real key: `cargo run -p atvopt-license -- issue --key ~/.atvopt/license-signing-key.prod - --licensee "you"`. -- Hot files that must not be edited by parallel agents at the same time: `wireless_adb.rs`, - `session.svelte.ts`, `App.svelte`, `router.svelte.ts`, `api.ts`, `types.ts`, mobile `lib.rs`. - The 2026-09-04 pattern that worked: one lead owns those, parallel agents own disjoint screens. -- Preserve the unrelated untracked root files (`atv-optimizer-android-strategy.html`, root - `node_modules/`, `package.json`, and `package-lock.json`); they are user-owned and not part of the - mobile commits. -- A physical session must identify the installed build and discover current wireless - endpoints. Do not reuse the ports recorded below — they are historical observations, and a - rotated wireless-debugging port is the normal case. -- Do not mark Phase 4 or pairing complete from host tests or logs alone. - -## 6. OPERATIONS PLAYBOOK (historical device commands; authorization required) - -The device commands below are reference material. Recorded addresses, tool versions and -pairing state are historical observations, not current device discovery — re-discover before -using any of them. - -Env: `ANDROID_HOME=~/Android/sdk`, NDK `28.2.13676358`, tauri-cli 2.11.x, the 4 android Rust -targets installed. Test device: **Pixel 10 Pro**. - -- **Deploy to the phone over WIRELESS ADB, never USB** (Bryan's rule). Phone at `192.168.42.211`; - its adb-connect port is random per session. To connect: it sometimes auto-connects via mDNS - (`adb-58040...._adb-tls-connect._tcp`); otherwise `adb pair 192.168.42.211:<pairport> <6-digit>` - (Bryan reads the code+port off the phone's Wireless-debugging screen), then `adb connect - 192.168.42.211:<connectport>`. The phone auto-locks/sleeps off USB power and drops the - connection — this is a recurring friction; when it's offline just wait/ask Bryan to nudge it. - The Pixel was successfully re-paired on 2026-08-16 and was reachable then, but both pairing and - connect ports rotate; always rediscover rather than reusing a recorded endpoint. -- **Build APK** (~2-4 min, run backgrounded): `cd v2/mobile && PATH="$ANDROID_HOME/platform-tools:$PATH" - NDK_HOME="$ANDROID_HOME/ndk/28.2.13676358" npx tauri android build --apk --debug --target aarch64`. - Do a `gradlew clean` first if stale native libs linger. Kotlin only compiles in this build. -- **Install/launch**: `adb -s <serial> install -r -d <apk>`; `adb -s <serial> shell am start -n - com.atvoptimizer.mobile/.MainActivity`. First launch after install may need a second `am start`. -- **Screenshots**: `adb -s <serial> exec-out screencap -p > shot.png` then Read it. Can't get past - the secure lock — ask Bryan to unlock. WebView is opaque to `uiautomator`, so blind `input tap` - by pixel coords is unreliable (device 1080×2410; the Read tool shows images at 896×2000 → ×1.21); - prefer having Bryan drive flows that need the TV's "Allow" prompt. -- **Debug logging (built)**: Rust `tracing` → logcat tag `RustStdoutStderr` + a `debug.log` file in - the app data dir (pull via `run-as com.atvoptimizer.mobile`), surfaced via `read_debug_log` in the - More screen. macOS has NO `timeout`; quote logcat tag filters in zsh (`'Tag:V' '*:S'`). -- **Fast device-less UI loop**: `npm run build`, serve `build/` on a port, Playwright headless - (chromium installed) at 384×812, stub `window.__TAURI_INTERNALS__.invoke`. Validates layout/flows - without a device (safe-area reads 0 in browser, so still spot-check on device). -- **GATES (all must pass)**: from `v2/`: `cargo fmt --check`, `cargo clippy -p shield-optimizer-core - -p shield-optimizer-v2 -p atv-optimizer-mobile -p tauri-plugin-atv-adb --all-targets -- -D warnings`, - `cargo test -p shield-optimizer-core`; from `v2/mobile/`: `npm run check` (0/0) + `npm run build`. - -## 7. Invariants / conventions (from CLAUDE.md + the review) -Engine (`crates/core/src/engine/`) stays pure (no I/O). One `AdbDriver` seam (desktop `SubprocessAdb`, -mobile `WirelessAdb`). One detection/safety function — `engine::safety` gates every disable path; -`require_pro(Feature)` runs AFTER the safety check. App lists in JSON, not code. Tauri commands -return `Result<T,String>`. Svelte 5 runes only. **Never fake data.** Commits: new commit each time, -no `Co-Authored-By`. Command arg names: camelCase in TS → Tauri maps to snake_case; `pkg`→`package`. - -## 8. Current coordination - -The role split described here (Navigator / Mechanic / Sol) belonged to a coordination system -that has been retired, along with its no-commit/no-push restriction. Current policy is in the repo -`CLAUDE.md`: work is committed and pushed continuously. Kept only because the surrounding sections -reference these names. - -## 9. Commit history (this effort, newest first) + (20 screen sections; lime `#C9F24E`, Geist/Geist Mono). The `claude_design` MCP is not available + here; work from the downloaded folder. +- Launcher icon and in-app BrandMark are the designer's exact glyph (`src-tauri/icons/icon.svg`). + +## 6. Validation + +From `v2/`: + ``` -(2026-10-06 desktop parity pass on mobile-parity-pass: d2acf0b, bbd87d1, 77f8d3d, 4e054c1, - 65acf44, bbb7d21, then the integration and docs commits; see the checkpoint at the top) -fae340c Mobile: document the licensing, release, and pairing backlog pass -5e76b24 Mobile: Android 11 wireless-debugging code pairing (SPAKE2) ← unverified on device -aa4e589 Mobile: release signing config, version tooling, third-party notices -5d52e51 Mobile: warm the remote channel, batch snapshot reads, key saved TVs by serial -78bb09d Licensing: Ed25519-signed offline license keys -0c24761 Mobile: document the stability reset and next physical checks -f3031c6 Mobile: screen correctness and honesty sweep -3e7cb3b Mobile: explicit reconnect, live connection state, honest dashboard -e51fc3f Mobile: make the ADB transport fail fast and honestly -334fda3 Vendor adb_client: bound every read and tolerate stray stream packets -bb33ecc Mobile: refresh agent handoff and device plan ← last APK on the Pixel -3eccc34 Mobile: retain cached TV labels after switching -b293b66 Mobile: clarify TV connections and device switching -5070840 Mobile: log remote lifecycle transitions -00aa8e6 Mobile: hand off background lifecycle checkpoint -53c975e Mobile: expire remote sessions after background grace -48b3574 Mobile: add fast scrcpy remote channel -988facc Mobile: add raw ADB service stream for fast remote -a9d2c8b HANDOFF: comprehensive current-state handoff for the next agent -3e8bcfc Transport: replace GPL libadb-android with pure-Rust adb_client (MIT) -b7c3d75 Re-architecture: reliability, shared foundation, real Pro, icon -16e5c42 (earlier) extract shared core workspace +cargo fmt --check +cargo clippy -p shield-optimizer-core -p shield-optimizer-v2 -p atv-optimizer-mobile \ + -p tauri-plugin-atv-adb -p atvopt-license --all-targets -- -D warnings +cargo test -p shield-optimizer-core -p atv-optimizer-mobile -p atvopt-license +cd vendor/adb_client && cargo test --lib # the vendored crate is outside the workspace ``` -Immediate next action: review `LIFECYCLE-EVIDENCE.md`, keep `so-vtm.8` open for the -owner-driven physical reproduction gate, and advance independently authorized host -work. Do not re-pair or install on the Pixel under this assignment. + +From `v2/mobile/`: `npm run check` (0 errors / 0 warnings), `npm run build`, `npm test` (the +browser suite: real screens, mocked Tauri). `node --test evidence/lifecycle/reproduce.mjs` runs the +lifecycle matrix. CI (`.github/workflows/v2-tests.yml`) runs the Rust workspace, the vendored +tests and the mobile check/build/test; **it never compiles the Kotlin or builds an APK**, so a +Gradle or manifest error only shows up in a local Android build. + +Android-target clippy works with the NDK clang exported as `CC_aarch64_linux_android` / +`CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER`. If you add a Material Symbols icon name, rerun +`scripts/subset-material-symbols.py` (needs `fonttools` + `brotli` in a venv) or the icon renders as +literal text. If you add a crate, rerun `scripts/gen-notices.sh`. + +Hot files that parallel agents must not edit at the same time: `wireless_adb.rs`, +`session.svelte.ts`, `App.svelte`, `router.svelte.ts`, `api.ts`, `types.ts`, mobile `lib.rs`. One +lead owns those while parallel agents take disjoint screens. + +## 7. Device playbook + +Device sessions need the owner's go-ahead. A session must first identify the installed build and +rediscover current wireless endpoints: wireless-debugging ports rotate, so never reuse a recorded +one. Don't mark pairing or fast-remote Phase 4 complete from host tests or logs alone. + +Env: `ANDROID_HOME=~/Android/sdk`, NDK `28.2.13676358`, tauri-cli 2.11.x, the four Android Rust +targets installed. Test phone: **Pixel 10 Pro**. A phone or TV emulator can stand in for parts of +this; see `v2/tools/emulator/README.md`. + +- **Deploy to the phone over wireless ADB, never USB** (the owner's rule). It sometimes + auto-connects through mDNS (`adb-<serial>._adb-tls-connect._tcp`); otherwise + `adb pair <phone-ip>:<pairport> <6-digit code>` (the owner reads the code and port off the phone's + Wireless debugging screen), then `adb connect <phone-ip>:<connectport>`. The phone sleeps off USB + power and drops the connection; when it goes offline, wait or ask the owner to wake it. +- **Build the APK** (2-4 min, run it backgrounded): `cd v2/mobile && PATH="$ANDROID_HOME/platform-tools:$PATH" + NDK_HOME="$ANDROID_HOME/ndk/28.2.13676358" npx tauri android build --apk --debug --target aarch64`. + Run `gradlew clean` first if stale native libs linger. Kotlin only compiles in this build. +- **Install/launch**: `adb -s <serial> install -r -d <apk>`; `adb -s <serial> shell am start -n + com.atvoptimizer.mobile/.MainActivity`. The first launch after install may need a second `am start`. +- **Screenshots**: `adb -s <serial> exec-out screencap -p > shot.png`. Nothing gets past the secure + lock screen, so ask the owner to unlock. The WebView is opaque to `uiautomator`, so blind + `input tap` by pixel coordinates is unreliable; let the owner drive flows that need the TV's + "Allow" prompt. +- **Debug logging**: Rust `tracing` goes to logcat tag `RustStdoutStderr` and to `debug.log` in the + app data dir (pull with `run-as com.atvoptimizer.mobile`); the More screen shows it through + `read_debug_log`. macOS has no `timeout`; quote logcat tag filters in zsh (`'Tag:V' '*:S'`). + +## 8. Invariants and conventions + +From the repo `CLAUDE.md`: the engine (`crates/core/src/engine/`) stays pure. One `AdbDriver` seam +(desktop `SubprocessAdb`, mobile `WirelessAdb`). `engine::safety` gates every disable path. App lists +live in JSON, not code. Tauri commands return `Result<T, String>`. Svelte 5 runes only. Command +argument names are camelCase in TS and Tauri maps them to snake_case (`pkg` → `package`). + +## 9. What remains + +`BACKLOG.md` is the ordered list. In short: + +1. The physical device pass: #93, #105 (`LIFECYCLE-EVIDENCE.md`), and the stability and UI checks + in `BACKLOG.md` P0. +2. Code pairing against a real adbd (`PAIRING-PLAN.md`). +3. Fast-remote Phase 4 device gates (`FAST-REMOTE-PLAN.md`). +4. Android SAF export/import, which also unblocks phone→TV upload; then sideload, the bug-report + bundle and a patch-notes viewer. +5. Product decisions for the owner: a free read-only Optimize plan, and the unused + `Feature::FileManager` / `Feature::BackupClone` gates. +6. Release: signing key and Play App Signing, a signed build, resolved Android dependency notices, + privacy and store disclosures, and the commerce channel (`RELEASE.md`). diff --git a/v2/mobile/LIFECYCLE-EVIDENCE.md b/v2/mobile/LIFECYCLE-EVIDENCE.md index 51b344f6..85052e9e 100644 --- a/v2/mobile/LIFECYCLE-EVIDENCE.md +++ b/v2/mobile/LIFECYCLE-EVIDENCE.md @@ -1,63 +1,15 @@ # Mobile restart report: lifecycle evidence -> **Snapshot at `48cba23`.** Two corrections: the `so-vtm.3` navigation change this calls an -> uncommitted candidate shipped in `d02de57`, and the "host-only, nothing committed or pushed" -> restrictions below are retired — see `CLAUDE.md`. The *device* gate is still real: feedback #7 -> remains unresolved on hardware. The reproducible matrix and `evidence/lifecycle/` are current. +Tracks [#105](https://github.com/bryanroscoe/shield_optimizer/issues/105). On a Pixel 10 Pro debug +build, briefly leaving the app and reopening it restarted the whole experience. Which Android +event caused that is not known: background/resume, Activity or WebView recreation, process death +and dismissal from Recents all look alike from the outside. The report stays open until a physical +session identifies the leaving action and correlates it with the runtime lifecycle. -Source-review addendum (Navigator workspace date 2026-09-07): -[new feedback UX contract](FEEDBACK-UX-2026-09-07.md) covers saved discovery, -Unknown risk, process/app identity, details and background-control copy at the -same `48cba23` baseline. No lifecycle test, GUI, build or device action was rerun -for that review while Bryan was using the host. It adds no persistence evidence -and does not close the owner-driven physical gate below. Its implementation and -integrated-candidate acceptance remain separate from this baseline report. - -Navigator, `so-vtm.8`, convoy `hq-cv-z4oev`. Recorded 2026-09-05 America/Chicago -(2026-09-06 UTC), against unchanged application source at -`48cba2384ca3938e6655b77ccf85716dd86cca5f` in `crew/navigator`. - -**Finding:** host visibility-only resume preserves the current screen. A fresh -JavaScript context resets the screen, live device, and health cache. With one -saved TV and auto-connect enabled, reload connects again and shows **Connected**, -then requires **Open dashboard**; it does not restore the prior detail screen. -This reproduces a restart-like experience in a browser, but does not establish -which Android event caused Bryan's report or prove a physical fix. - -## Pending navigation candidate is separate - -Mechanic has locally accepted `so-vtm.3`, a three-file uncommitted candidate on -`48cba23`: Onboarding, its session browser tests, and feedback #5. Mechanic's bead -records **20 passing worker browser tests** plus check/build and hash verification; -Navigator did not rerun that candidate. Its saved-TV success path calls -`onConnected()` after the device profile resolves and opens Dashboard directly. -First-time/add-TV connections retain the Connected/Open dashboard interstitial. -This changes the baseline reload destination; it does not add prior-screen or -live-session persistence. Physical verification and publication remain gated. - -The immutable application patch is at -`/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.3-d_91juzj/application.patch` -(SHA-256 `5cb8b816f7e0e7f79b3ec8167288ad9c74c7f9ed56e7c55f463fddd8c1953a51`). -This review read that patch and manifest; it did not edit the preserved Rust tree -or `USER-FEEDBACK-2026-09-05.md`. All 13 results below remain **baseline-only**. -The harness's Connected/Open dashboard assertions intentionally describe baseline -behavior and must be revised under a separate evidence scope before it can serve -as a candidate acceptance suite. No candidate rerun is requested. - -## Report and authority - -[Feedback #7](USER-FEEDBACK-2026-09-05.md#7-briefly-leaving-and-reopening-the-app-restarts-the-experience) -records Bryan's observation after installing a debug build from `main` at -`44d2d66` on a Pixel 10 Pro. The leaving/reopening action and lifecycle are not -identified. This owner-reported install supersedes older handoff assertions that -nothing after `bb33ecc` had reached a phone; it is not a completed stability matrix. - -The current assignment permits host-only review, evidence, and documentation. -No phone or TV was attached, queried, installed, paired, launched, or changed for -this work. No application code was changed, no persistence was added, and nothing -was committed, pushed, or deployed. Mechanic owns integration and file reservations; -Sol implements any bounded follow-up. The physical investigation remains open on -`so-vtm.8`; historical device instructions do not authorize running them now. +**What the host evidence shows:** a visibility-only resume preserves the current screen. A fresh +JavaScript context resets the screen, the live device and the health cache. That reproduces a +restart-like experience in a browser, but it does not establish which Android event caused the +report, and it is not a physical fix. No persistence change is justified by the host matrix alone. ## Reproduce on the host @@ -68,192 +20,123 @@ npm ci --ignore-scripts # Only if the matching host browser is missing: npx playwright install chromium node --test evidence/lifecycle/reproduce.mjs -npm test -npm run check -npm run build ``` -The harness starts a loopback Vite development server and Chromium at 384×812. -All Tauri invokes go to an explicit host fixture; unexpected commands fail. -Browser HTTP requests outside the loopback origin are blocked, and mock hostnames -use `.invalid`. No Android or ADB binary runs. Fixture devices are test data only. -The production build is checked separately; these browser cases exercise Vite's -compiled source, not an installed APK or the production bundle. - -Visibility cases override `document.visibilityState`, dispatch `visibilitychange`, -and advance Playwright's virtual clock. They test frontend event handling and the -45-second heartbeat, not actual OS backgrounding, real elapsed grace time, timer -throttling, Android suspension, or scrcpy reuse. Reload reconstructs JavaScript; -the host fixture separately models an empty or retained native connection. The -fresh-context case copies only browser storage, with an empty fixture backend. - -Default output: `/tmp/so-vtm.8-lifecycle/observations.json`. Set -`LIFECYCLE_OUTPUT` to choose a separate directory. To review an integration -candidate without editing it, set `LIFECYCLE_MOBILE_ROOT` to its absolute mobile -path; its Vite/Svelte dependencies must be available and assertions must match the -intended candidate behavior. In particular, do not use the baseline saved-TV -interstitial assertions to reject the accepted `so-vtm.3` navigation repair. -The output records the actual source HEAD, dirty paths, source hashes, runtime versions, state, and invoke -sequence. Do not carry baseline results forward to a changed candidate. +The harness starts a loopback Vite development server and Chromium at 384×812. All Tauri invokes +go to an explicit host fixture; unexpected commands fail. Browser HTTP requests outside the loopback +origin are blocked, and mock hostnames use `.invalid`. No Android or ADB binary runs. The cases +exercise Vite's compiled source, not an installed APK or the production bundle. -Captured baseline: [observations](evidence/lifecycle/baseline-48cba23.json) and -[test output](evidence/lifecycle/baseline-48cba23.tap). Host was macOS arm64, -Node 26.3.0, Chromium 148.0.7778.96. An initial harness run was stopped after a -fixture-name assertion mismatch; the fixture was corrected and the full run below -passed. No application fix was needed for that harness error. +Visibility cases override `document.visibilityState`, dispatch `visibilitychange` and advance +Playwright's virtual clock. They test frontend event handling and the 45-second heartbeat, not real +OS backgrounding, elapsed grace time, timer throttling, Android suspension or scrcpy reuse. Reload +reconstructs JavaScript; the fixture separately models an empty or retained native connection. -Durable owner-controlled archive: [so-vtm.8-3mgugq2g](/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.8-3mgugq2g). It preserves -[observations.json](/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.8-3mgugq2g/observations.json), -[the final browser log](/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.8-3mgugq2g/browser.tap), -[command results](/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.8-3mgugq2g/command-results.txt), the exact harness, and a -[SHA-256 manifest](/Users/bryanroscoe/.local/share/gastown/evidence/shield_optimizer/so-vtm.8-3mgugq2g/manifest.json). The manifest hashes each archived -artifact; it does not establish physical validation. Application documentation -patches and the workspace-only role patch are separate. Existing `.gitignore`, -`.beads`, and other runtime state are excluded from application publication scope. +Output defaults to `/tmp/so-vtm.8-lifecycle/observations.json`; set `LIFECYCLE_OUTPUT` to choose +another directory, or `LIFECYCLE_MOBILE_ROOT` to run against another checkout's `v2/mobile`. The +output records the source HEAD, dirty paths, source hashes, runtime versions, state and invoke +sequence. The recorded baseline run is in `evidence/lifecycle/`. -## Controlled results +**Four reload cases are out of date.** They still expect a saved-TV reconnect to stop on a +Connected / Open dashboard interstitial, but a saved-TV connect now opens the Dashboard directly +once the device profile resolves (first-time and Add-a-TV connections keep the interstitial). +Against current source those four time out waiting for the Connected heading; the other nine pass. +Update their assertions before relying on them. -All **13 focused browser cases passed**. The table groups related cases. +## The matrix -| Case | Observed result | What it establishes | +| Case | Expected result | What it establishes | | --- | --- | --- | -| Hidden/visible at virtual 5, 35, and 50 seconds (3 cases) | Diagnostics and live serial retained; zero hidden probes; one probe on visible; no new connect | Frontend visibility handler does not reset the router; crossing 30 seconds in browser has no native meaning | -| Lost fixture socket on resume | One reconnect attempt fails; prior Diagnostics screen remains; `isConnected=false`; Retry shown; no further auto-dial over 90 virtual seconds | Failed recovery is bounded in this loss episode | -| Visible heartbeat | No probe at 44,999 ms; one at 45,000 ms | Timer cadence, not a promise of a 45-second completed disconnect diagnosis | -| Reload with empty/retained fixture backend (2 cases) | Pending new connect has empty serial, no health cache, and onboarding screen; successful handshake/profile reaches Connected; Open dashboard lands on Dashboard | Local saved-TV data does not restore live session or prior screen; native presence alone is not adopted | -| Fresh browser context with saved storage | One fresh connect; Connected confirmation on onboarding route; health cache absent | Surviving storage provides connection bookkeeping, not a prior live runtime | -| Explicit Disconnect then reload | Auto-connect preference is `0`; Which TV? waits; no dial for 90 virtual seconds | Intentional disconnect survives reload while storage is available | -| Two saved TVs with a retained fixture connection | Which TV? waits; no dial; frontend remains disconnected | Multi-TV launch requires a choice even if the mock native layer has a connection | -| Empty saved list | Find your TV; no scan or connect | Clean start is user-driven | -| Browser Back from Diagnostics | Spare browser entry consumed; Dashboard reached | JavaScript history behavior only; Android root Back outcome untested | -| Completed mock Volume up then reload | Exactly one `send_key` total | That completed mock action is not replayed; interrupted native mutations are not covered | - -The existing **9 browser regressions passed**. `npm run check` reported **0 errors -and 0 warnings**, and `npm run build` passed. Vite emitted a separate missing -parent `v2/.svelte-kit/tsconfig.json` warning in this fresh mobile-only workspace; -that warning is included in the raw output. `npm ci --ignore-scripts` installed -74 packages and reported four high-severity dependency audit findings; dependencies -and lockfiles were not changed or audited further in this lifecycle review. - -The three existing pure native lifecycle tests also passed, compiled directly: +| Hidden/visible at virtual 5, 35 and 50 seconds (3 cases) | Diagnostics and live serial retained; no hidden probes; one probe on visible; no new connect | The visibility handler does not reset the router; crossing 30 seconds in a browser has no native meaning | +| Lost fixture socket on resume | One reconnect attempt fails; prior screen remains; `isConnected=false`; Retry shown; no further auto-dial over 90 virtual seconds | Failed recovery is bounded per loss episode | +| Visible heartbeat | No probe at 44,999 ms; one at 45,000 ms | Timer cadence, not a promise of a 45-second disconnect diagnosis | +| Reload with empty/retained fixture backend (2 cases) | A fresh connect with empty serial and no health cache | Saved-TV data does not restore the live session or prior screen; native presence alone is not adopted | +| Fresh browser context with saved storage | One fresh connect; health cache absent | Surviving storage provides connection bookkeeping, not a prior live runtime | +| Explicit Disconnect then reload | Auto-connect preference `0`; Which TV? waits; no dial for 90 virtual seconds | Intentional disconnect survives reload while storage is available | +| Two saved TVs with a retained fixture connection | Which TV? waits; no dial | Multi-TV launch requires a choice even if the native layer has a connection | +| Empty saved list | Find your TV; no scan or connect | A clean start is user-driven | +| Browser Back from Diagnostics | Spare browser entry consumed; Dashboard reached | JavaScript history only; Android root Back is untested | +| Completed mock Volume up then reload | Exactly one `send_key` | A completed action is not replayed; interrupted native mutations are not covered | + +The pure native lifecycle tests compile directly: ```sh -# From repository root: -mkdir -p /tmp/so-vtm.8-lifecycle -rustc --edition=2021 --test v2/mobile/src-tauri/src/remote_lifecycle.rs \ - -o /tmp/so-vtm.8-lifecycle/remote-lifecycle-tests -/tmp/so-vtm.8-lifecycle/remote-lifecycle-tests --nocapture +# From the repository root: +mkdir -p /tmp/lifecycle +rustc --edition=2021 --test v2/mobile/src-tauri/src/remote_lifecycle.rs -o /tmp/lifecycle/tests +/tmp/lifecycle/tests --nocapture ``` -They verify cleanup is claimed once, resume invalidates the pending epoch, and a -newer suspend invalidates an older epoch. On macOS the Android-only handler is -excluded. This is not Android compilation or proof that the timer closes a real -channel. Full Rust workspace, cross-OS CI, APK, pairing, and physical background -matrices were not rerun in this documentation/evidence pass. +They verify cleanup is claimed once, resume invalidates the pending epoch, and a newer suspend +invalidates an older one. On macOS the Android-only handler is excluded, so this is not Android +compilation or proof that the timer closes a real channel. -## Implemented persistence boundary +## What persists across a restart | State | Location | Reconstruction behavior | | --- | --- | --- | | Router stack/current screen | `src/lib/router.svelte.ts`, runes singleton | Starts at `onboarding`; no durable route restore | -| Device, host, liveness, apply flag, health/bloat caches | `src/lib/session.svelte.ts`, runes singleton | Starts empty/idle; connections must be resolved through the API; no operation resume journal | -| Saved TVs | `atv.savedDevices.v1` in localStorage | Host, port, label, type, optional hardware ID, last-used time; at most 16 written rows; equal hardware ID is the only cross-endpoint identity, while ID-less rows match only exact host+port; does not prove current reachability | -| Launch auto-connect preference | `atv.autoConnect.v1` in localStorage | `0` disables; exactly one saved TV with enabled preference may dial; multiple TVs wait | +| Device, host, liveness, apply flag, health/bloat caches | `src/lib/session.svelte.ts`, runes singleton | Starts empty; connections must be resolved through the API; no operation resume journal | +| Saved TVs | `atv.savedDevices.v1` in localStorage | Endpoint, label, type, optional hardware id or local id and fingerprint, last-used time; at most 16 rows; does not prove current reachability | +| Launch auto-connect preference | `atv.autoConnect.v1` in localStorage | `0` disables; only a lone saved TV with the preference enabled may dial | | Remote compatibility preference | `atv.remote.forceShell` in localStorage | Remote transport preference only | -| ADB identity and license | Rust-managed app data, not the router | Key/validated license may survive ordinary restart; this pass did not read private app data or verify Android storage retention | -| Fast-remote control channels | Shared native session registry | 30-second suspension cleanup; no frontend route/session serialization | - -Storage loss, uninstall/clear-data, profile read failure, Android renderer death, -and interrupted backend mutation outcomes are not covered by the passing browser -cases. Saved connection metadata must never be treated as a live device, and -unfinished mutations must never be automatically replayed to emulate persistence. - -The identity rule in that table is the Mechanic-local `so-fb3.1.2` integration -(`savedDevices.ts` `163472a8`), not behavior exercised by this document's -baseline 13-case lifecycle run. It intentionally retains conflicting or -unverified histories as separate rows, including two entries at one address; -that makes the saved count greater than one and suppresses single-TV auto-dial. -`so-fb3.1.3` tracks the remaining screen consequence: Onboarding and Devices -must distinguish those rows for rendering, progress, current-TV filtering, and -forget actions. Until that lands, a shared host can highlight or hide the wrong -row, and an identical endpoint can make the two-argument forget operation target -the MRU identity rather than the row the user selected. None of this establishes -the physical cause of feedback #7. - -## Android wiring inspected, not executed - -Repository source paths: - -- `src-tauri/gen/android/app/src/main/java/com/atvoptimizer/mobile/MainActivity.kt`: - `onCreate` enables edge-to-edge then delegates to `TauriActivity`. It has no - app-specific screen/session save/restore implementation. -- `src-tauri/gen/android/app/src/main/AndroidManifest.xml`: `singleTask` activity - with explicit handled `configChanges`, including orientation and screen size. - Rotation therefore cannot simply be assumed to recreate this Activity. +| ADB identity and license | Rust-managed app data | May survive an ordinary restart; Android storage retention is unverified | +| Fast-remote control channels | Shared native session registry | 30-second suspension cleanup; no frontend serialization | + +Storage loss, uninstall/clear-data, profile read failure, renderer death and interrupted backend +mutations are not covered by the browser cases. Saved connection metadata must never be treated as +a live device, and unfinished mutations must never be replayed automatically to emulate +persistence. + +## Android wiring (read, not executed) + +- `src-tauri/gen/android/app/src/main/java/com/atvoptimizer/mobile/MainActivity.kt`: `onCreate` + enables edge-to-edge and delegates to `TauriActivity`. There is no app-specific screen or session + save/restore. +- `src-tauri/gen/android/app/src/main/AndroidManifest.xml`: a `singleTask` activity with explicit + handled `configChanges`, including orientation and screen size, so rotation cannot be assumed to + recreate the Activity. - `src-tauri/src/lib.rs`: the Android run loop forwards Tauri window events to `remote_lifecycle::handle_window_event`. -- `src-tauri/src/remote_lifecycle.rs`: Suspended schedules an epoch-bound 30-second - cleanup; Resumed invalidates the epoch. The sole cleanup target is - `AppState::drop_all_remote_sessions` in `../crates/core/src/commands/state.rs`, - which drains/closes remote control sessions. It does not save or reset the - frontend router or serialize the ordinary wireless connection. -- `src/App.svelte`: probes on visible resume and every 45 seconds while visible; - the probe requires a frontend device. This is independent of the native grace. - -Pinned dependency sources from the local Cargo registry were also inspected; -versions match `../Cargo.lock`: - -| Source | Static wiring | -| --- | --- | -| Wry 0.55.1 `src/android/kotlin/WryActivity.kt` | ProcessLifecycleOwner observer calls Rust pause/resume; Activity pause/resume delegates to the WebView; destruction notifies Rust; saved state includes an Activity ID | -| Tao 0.35.3 `src/platform_impl/android/ndk_glue.rs` and `mod.rs` | Pause maps to Suspended, later resume to Resumed; initial resume is specially suppressed | -| tauri-runtime-wry 2.11.2 `src/lib.rs` | Mobile suspend/resume events become window events delivered to the run callback | -| Tauri 2.11.2 `mobile/android-codegen/TauriActivity.kt` and `mobile/android/src/main/java/app/tauri/AppPlugin.kt` | Tauri disables Wry's generic Back handler and supplies its own: WebView goes back when possible, otherwise delegates to Android; a registered back-button listener has a separate branch | - -An upstream Activity ID or WebView lifecycle callback is not app-level restoration -of Svelte state. Native delegation at root is also not proof of process death or -of the exact behavior of Back on this Pixel/Android version. The browser tests do -not invoke those Kotlin callbacks. Actual Activity recreation might or might not -replace JavaScript; the observed reset applies when JavaScript is reconstructed. - -## Physical reproduction gate and minimal owner evidence - -**Gate:** keep the restart report open until an owner-driven, explicitly authorized -physical session identifies the leaving action and correlates it with the runtime -lifecycle. Do not request pairing codes repeatedly, attach to a device, install a -new APK, change developer settings, clear data, force-stop, send TV keys, or replay -Optimize while this host-only assignment remains in force. - -The minimum useful owner report can come from ordinary use of the installed build: - -1. Record build/commit if known, phone Android and WebView versions if available, - one versus multiple saved TVs, whether Disconnect was used, starting screen, - and whether any operation was still pending. Unknown values remain unknown. -2. Name the exact action: **Home**, switching through **Recents without dismissal**, - **Android Back at the root**, **swiping the app away**, or **locking the phone**. - Record whether reopening used the launcher icon or the Recents card and the - elapsed time. These actions must not be collapsed into “closed the app.” -3. Record what returned: prior screen, splash, Find your TV, Which TV?, Connecting, - Connected/Open dashboard, or Lost/Retry; also whether the correct TV name stayed. - A short owner-captured before/after recording or timestamped description is enough - to classify the visible symptom; no TV mutation is needed. -4. If available in that authorized session, correlate the timestamps with app - `app suspended`, `app resumed`, and `background grace expired` logs. To distinguish - process death, Activity recreation, and renderer loss, capture PID/process-start, - Activity create/destroy, and renderer/document identity evidence. Existing remote - epoch logs alone cannot make this distinction; report it as unknown when absent. - Any additional instrumentation belongs to a separate mechanic-reserved Sol task. - -A later controlled physical matrix should separate Home/Recents-switch returns at -roughly 5 seconds and over 30 seconds from Back-at-root, Recents dismissal, and -recreation/process-loss cases. Lock/unlock is another distinct row. Do not induce -process death or change lifecycle developer options without authorization. First -use a read-only screen and an idle TV; an interrupted mutation test is separate -scope. A live fast-remote grace test is also separate from this frontend screen -retention report and still requires the physical Phase 4 evidence. - -Interpretation after evidence: same document plus a route reset points toward a -frontend transition; a new document explains reconstruction but not why Android -recreated it; retained screen with Lost/Retry points toward connection recovery. -Neither a splash nor a reconnect by itself proves process death. No persistence -implementation is justified solely by this host matrix. +- `src-tauri/src/remote_lifecycle.rs`: Suspended schedules an epoch-bound 30-second cleanup; + Resumed invalidates the epoch. The only cleanup target is `AppState::drop_all_remote_sessions` + in `../crates/core/src/commands/state.rs`. It does not touch the router or the ordinary wireless + connection. +- `src/App.svelte`: probes on visible resume and every 45 seconds while visible, independent of the + native grace. + +Upstream, at the versions in `../Cargo.lock`: Wry's `WryActivity.kt` forwards process and Activity +pause/resume and destruction to Rust; Tao maps pause to Suspended and a later resume to Resumed; +tauri-runtime-wry delivers those as window events; Tauri's `TauriActivity.kt`/`AppPlugin.kt` +replace Wry's Back handler (WebView goes back when it can, otherwise Android handles it). None of +that is app-level restoration of Svelte state, and the browser tests do not invoke those Kotlin +callbacks. + +## Physical reproduction gate + +The minimum useful report can come from ordinary use of an installed build: + +1. Record the build, the phone's Android and WebView versions if available, one versus multiple + saved TVs, whether Disconnect was used, the starting screen, and whether any operation was + pending. Unknown values stay unknown. +2. Name the exact action: **Home**, switching through **Recents without dismissal**, **Android Back + at the root**, **swiping the app away**, or **locking the phone**. Record whether reopening used + the launcher icon or the Recents card, and the elapsed time. Don't collapse these into "closed + the app". +3. Record what came back: the prior screen, splash, Find your TV, Which TV?, Connecting, Connected, + or Lost/Retry, and whether the right TV name stayed. A short before/after recording or a + timestamped description is enough; no TV mutation is needed. +4. Where possible, correlate the timestamps with the app's `app suspended`, `app resumed` and + `background grace expired` logs. Telling process death, Activity recreation and renderer loss + apart needs PID/process-start, Activity create/destroy and document identity evidence; the + remote epoch logs alone cannot make that distinction, so report it as unknown when absent. + +A controlled matrix should then separate Home/Recents returns at about 5 seconds and over 30 +seconds from Back at the root, Recents dismissal, recreation/process loss, and lock/unlock. Use a +read-only screen and an idle TV first; an interrupted-mutation test is separate scope, and so is the +live fast-remote grace test (`FAST-REMOTE-PLAN.md` Phase 4). Don't induce process death or change +lifecycle developer options without the owner's go-ahead. + +Reading the result: the same document plus a route reset points to a frontend transition; a new +document explains the reset but not why Android recreated it; a retained screen with Lost/Retry +points to connection recovery. Neither a splash nor a reconnect proves process death on its own. diff --git a/v2/mobile/PAIRING-PLAN.md b/v2/mobile/PAIRING-PLAN.md index 9b1f7f89..310cd011 100644 --- a/v2/mobile/PAIRING-PLAN.md +++ b/v2/mobile/PAIRING-PLAN.md @@ -4,8 +4,9 @@ The six-digit-code flow, implemented clean-room in Rust inside the vendored `adb (`vendor/adb_client/src/message_devices/tcp/pairing/`) and called from `WirelessAdb::pair` in `mobile/src-tauri/src/wireless_adb.rs`. -Before this, `pair` returned "isn't supported yet" and users had to fall back to Network -debugging on `:5555`, which modern Google TV builds do not offer. This closes that gap. +Modern Google TV builds don't offer Network debugging on `:5555`, so this flow is the only way the +app can reach them. **It has never run against a real adbd**; until the manual test below passes, +treat it as unverified and keep Network debugging as the documented fallback. ## Licensing @@ -22,8 +23,7 @@ protocol facts those sources establish, each of which is cited below. ## Protocol, point by point, with the file each was verified in -Everything below was read from the live AOSP / BoringSSL trees while implementing, not from -memory. +Everything below was read from the live AOSP / BoringSSL trees, not from memory. ### 1. TLS 1.3 with a self-signed client certificate, any server certificate accepted @@ -244,8 +244,8 @@ adb_client::tcp::pair(addr: SocketAddr, code: &str, private_key_path: &Path) `WirelessAdb::pair` normalizes the code (strips spaces and dashes, requires exactly six digits — a malformed code is rejected locally rather than burning the single guess the device allows per displayed code), ensures the persisted RSA key exists, and runs `pair` on -`spawn_blocking`. Nothing else in `WirelessAdb` changed; pairing does not create or replace a -connection, so the frontend still calls `connect` afterwards exactly as before. +`spawn_blocking`. Pairing does not create or replace a connection, so the frontend calls +`connect` afterwards. Timeouts: a 5-second TCP connect bound, then a single 30-second deadline for the whole exchange, re-armed onto the socket's read and write timeouts before **every** syscall @@ -269,7 +269,7 @@ the property that limits an attacker to one password guess per connection, and i ## What the tests prove, and what they do not -`cd vendor/adb_client && cargo test --lib` (53 tests, all green): +`cd vendor/adb_client && cargo test --lib`: **Proven:** @@ -306,27 +306,25 @@ the property that limits an attacker to one password guess per connection, and i **Not proven — and this is the honest gap:** -- **No end-to-end run against a real device.** There is no Android device reachable from this - environment, so the TLS handshake against `adbd`'s pairing server, the exporter value both - sides compute, and the device's acceptance of our `PeerInfo` are all unverified in practice. +- **No end-to-end run against a real device.** The TLS handshake against `adbd`'s pairing server, + the exporter value both sides compute, and the device's acceptance of our `PeerInfo` are all + unverified in practice. - **SPAKE2 wire compatibility with BoringSSL is verified against the *algorithm*, not against the *binary*.** The KAT pins the Rust against an independent Python model of the same source, which catches transcription and arithmetic mistakes but not a *misreading* shared by both. Residual risk is concentrated in three places: (a) that - `x25519_ge_scalarmult_small_precomp` really consumes the unreduced 256-bit scalar the way I - read it — checked by reading the loop, which walks bits `63..0` of four 64-bit limbs; (b) that + `x25519_ge_scalarmult_small_precomp` really consumes the unreduced 256-bit scalar as read + here — checked by reading the loop, which walks bits `63..0` of four 64-bit limbs; (b) that `password_hash` in the transcript is the full 64 bytes rather than the 32-byte reduced scalar — checked in `crypto/curve25519/internal.h`, `uint8_t password_hash[64]`; (c) the - little-endian `u64` length prefix in `update_with_length_prefix`. I would rate the odds of a - first-try success as good but not certain. + little-endian `u64` length prefix in `update_with_length_prefix`. The odds of a first-try + success are good but not certain. - **The exporter semantics under rustls** (`context: None` versus BoringSSL's `use_context = false`) are argued from both implementations' source, not measured. -- **Pre-existing latent risk, unchanged by this work:** `ADBRsaKey::android_pubkey_encode` - builds the modulus and `rr` fields with `BigUint::to_bytes_le`, which drops high-order zero - bytes. If `rr` happens to have a zero top byte (~1 chance in 256 per key) the base64 decodes - to 523 rather than 524 bytes and *both* pairing and ordinary RSA `A_AUTH` would be rejected by - the device. Not introduced here and not fixed here (`adb_rsa_key.rs` is outside this change's - file ownership), but worth a follow-up. +The public-key encoding pairing sends is fixed-width: `ADBRsaInternalPublicKey::into_bytes` pads +the modulus and `rr` to the full word count, because `BigUint::to_bytes_le` drops high-order zero +bytes and adbd rejects a 523-byte key (about 1 key in 256) for both pairing and ordinary RSA +`A_AUTH`. ## The manual end-to-end test @@ -379,28 +377,14 @@ code is readable on the phone screen rather than across the room. 4. **Expected:** pairing succeeds and the connect that follows needs no on-TV approval. 5. Check `tracing` output for `wireless pairing succeeded` and the logged `peer` GUID. -Until at least the Pixel run has been done, treat code pairing as new-and-unproven — which is -what the onboarding callout now says, alongside the Network-debugging fallback. +Until at least the Pixel run has been done, treat code pairing as unproven, which is +what the onboarding callout says, alongside the Network-debugging fallback. -## Files changed +## Where the code lives -- `vendor/adb_client/src/message_devices/tcp/pairing/{mod,packet,spake2,cipher}.rs` — new -- `vendor/adb_client/src/message_devices/tcp/mod.rs` — export `pair`, `PairingError`, - `PairingOutcome`, `PeerInfo` -- `vendor/adb_client/Cargo.toml` — add `curve25519-dalek`, `subtle`, `sha2`, `hkdf`, `aes-gcm` -- `vendor/adb_client/SHIELD-OPTIMIZER-PATCH.md` — document the addition -- `mobile/src-tauri/src/wireless_adb.rs` — implement `pair`, add `normalize_pairing_code` and - `pair_error_message` plus their tests -- `mobile/src/screens/Onboarding.svelte` — the pairing-step callout copy only -- `mobile/PAIRING-PLAN.md` — this file - -## Validation run - -``` -cargo fmt --check # from v2/ -cargo clippy -p atv-optimizer-mobile --all-targets -- -D warnings -cargo test -p atv-optimizer-mobile # 21 passed -cargo check -p atv-optimizer-mobile --target aarch64-linux-android # with the NDK env -cd vendor/adb_client && cargo clippy --lib -- -D warnings && cargo test --lib # 53 passed -cd mobile && npm run check # 0 errors, 0 warnings -``` +- `vendor/adb_client/src/message_devices/tcp/pairing/{mod,packet,spake2,cipher}.rs`, exported from + `vendor/adb_client/src/message_devices/tcp/mod.rs` (`pair`, `PairingError`, `PairingOutcome`, + `PeerInfo`); listed in `vendor/adb_client/SHIELD-OPTIMIZER-PATCH.md`. +- `mobile/src-tauri/src/wireless_adb.rs` — `WirelessAdb::pair`, `normalize_pairing_code`, + `pair_error_message` and their tests. +- `mobile/src/screens/Onboarding.svelte` — the pairing step and its callout copy. diff --git a/v2/mobile/README.md b/v2/mobile/README.md index 96504653..9d19627a 100644 --- a/v2/mobile/README.md +++ b/v2/mobile/README.md @@ -26,18 +26,16 @@ this file is only an orientation. [`BACKLOG.md`](BACKLOG.md) is the ordered queu Shell, push/pull, screencap and pairing all run through it. - **The Kotlin plugin (`tauri-plugin-atv-adb/`) is mDNS discovery only** — Android's `NsdManager`, no third-party native libraries. -- The GPLv3 `libadb-android` transport this app originally used was removed in `3e8bcfc`; it - blocked selling a closed-source product and hung on older Shield adbd. See - [`TRANSPORT-LICENSING-RESEARCH.md`](TRANSPORT-LICENSING-RESEARCH.md) for the decision record and - [`HANDOFF.md`](HANDOFF.md) §3 for the current story. +- The transport carries no GPL code, because the app is sold as a closed-source product. + [`HANDOFF.md`](HANDOFF.md) §2 explains what that rules out. - **Legacy network debugging (`:5555`, no code) is the proven path.** Android 11+ code pairing (SPAKE2) is implemented in the vendored crate but is **not yet verified against a real adbd** — see [`PAIRING-PLAN.md`](PAIRING-PLAN.md). ## Layout -- `src/` — Svelte 5 frontend: 14 screens in `screens/`, 8 components in `components/`, and the - typed API / session / router / saved-device logic in `lib/`. +- `src/` — Svelte 5 frontend: 14 screens in `screens/`, shared components in `components/`, and + the typed API / session / router / saved-device logic in `lib/`. - `src-tauri/src/` — `lib.rs` (command registration), `wireless_adb.rs` (the transport), `wireless_commands.rs`, `file_commands.rs`. - `tauri-plugin-atv-adb/` — the mDNS discovery plugin. @@ -47,17 +45,18 @@ this file is only an orientation. [`BACKLOG.md`](BACKLOG.md) is the ordered queu ``` npm run check # svelte-check, must be 0 errors / 0 warnings npm run build -npm test # browser suite (99 tests) — real Svelte screens, Tauri invokes stubbed +npm test # browser suite: real Svelte screens, Tauri invokes stubbed ``` -CI runs all three on every push (`.github/workflows/v2-tests.yml`, `frontend-mobile` job). +CI runs all three on every pull request and every push to `main` that touches `v2/` +(`.github/workflows/v2-tests.yml`, `frontend-mobile` job). ## What is still unproven -Nothing in `v2/mobile` has been verified on a phone or TV since `44d2d66`. The browser suite +Nothing in `v2/mobile` has been verified on a phone or TV in its current form. The browser suite exercises the real screens against a mocked Tauri layer, which is worth a lot but is not a device. The open physical gates are listed in [`BACKLOG.md`](BACKLOG.md); the largest are code pairing -against real adbd, the fast-remote device matrix ([`FAST-REMOTE-PLAN.md`](FAST-REMOTE-PLAN.md)), -and the app-restart behaviour Bryan reported ([`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md)). +against a real adbd, the fast-remote device matrix ([`FAST-REMOTE-PLAN.md`](FAST-REMOTE-PLAN.md)), +and the app-restart report ([`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md)). Release and signing: [`RELEASE.md`](RELEASE.md). Licensing: [`LICENSING.md`](LICENSING.md). diff --git a/v2/mobile/RELEASE-POLISH-RECEIPTS.md b/v2/mobile/RELEASE-POLISH-RECEIPTS.md deleted file mode 100644 index 366f1d1e..00000000 --- a/v2/mobile/RELEASE-POLISH-RECEIPTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mobile release-polish completion receipts - -Original reconciliation: 2026-10-17 through PR202. Updated on 2026-10-19 against `mobile-parity-pass` at `d388cad1e3da9c17e0a5c0bdeec52022fd8d9970` for PR204 and the reviewed PR206 main integration draft. GitHub merged-PR metadata, final-head check runs and linked review comments were read for each row. These are completed development-branch scopes, not release or physical-device acceptance. The [durable checklist](RELEASE-POLISH.md) retains the remaining gates. - -Each CI link reports six successful jobs for the listed final head: Rust on Ubuntu/macOS/Windows, desktop and mobile frontend checks, and simulated-device E2E. Review receipts describe independent source review; they do not imply the reviewer reran tests, inspected pixels or used a device. Where a PR was integrated again, the final integration receipt supersedes the earlier-head review/CI status. PR181's exact-head automated source-review completion is the available receipt. GitHub's automated summary can retain finding flags (including PR182/184/190/202); the linked independent scope approval does not rewrite those historical summaries or claim every review thread was resolved. - -| Merged scope | Final PR head | Merge commit | Final review / CI | -|---|---|---|---| -| [PR181](https://github.com/bryanroscoe/shield_optimizer/pull/181) — Fix mobile settings readback and system-bar spacing | `3c15c2147a0e4b2cf0768592d33b8f682811bcbc` | `6f3deefa5e46a03d292aa4ef1e84927440b03766` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/181#issuecomment-6228770166) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/38996153794) | -| [PR182](https://github.com/bryanroscoe/shield_optimizer/pull/182) — Lock mobile background scroll through nested app dialogs | `1dd9bef7f949f66e4a6b6e94ba11e5c319c9baa0` | `4d18b24ef5ce9b2d00e05c4f6f19d9a0a63f99a6` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/182#issuecomment-6229103597) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/38998647459) | -| [PR183](https://github.com/bryanroscoe/shield_optimizer/pull/183) — Prevent stale app sheets targeting a new TV connection | `fb30a5ce9c2cce04ce65ec4aa83da658f377b20f` | `cd2244b970e2d579627e89bc3fb59a44da41d75e` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/183#issuecomment-6229206394) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/38999174214) | -| [PR184](https://github.com/bryanroscoe/shield_optimizer/pull/184) — Clarify used capacity and sensor-aware thermal context | `ca7f4b118fd156d5299b2b5d7a2752e30111a3da` | `3a1881e9b73c768be3a417abbea14cf524cfbe40` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/184#issuecomment-6229462630) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39001182304) | -| [PR185](https://github.com/bryanroscoe/shield_optimizer/pull/185) — Improve mobile diagnostic report readability | `330275bd9858c9ea6ef8e22ab8df424350a498ef` | `56aa7c56450464b2060c27838373029b6b90c14f` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/185#issuecomment-6229684694) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39001843909) | -| [PR186](https://github.com/bryanroscoe/shield_optimizer/pull/186) — Fix mobile modal handoff focus and stock takeover ownership | `f3a69eca41100c727db3820c49f8a3d297be5a56` | `83f69b77e2b19b643e21ae456d0f597e90ef7d83` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/186#issuecomment-6230054781) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39004884597) | -| [PR187](https://github.com/bryanroscoe/shield_optimizer/pull/187) — Write the Daydream component before enabling it | `a7ec71516b80abb68837df23c491f589ca2750a1` | `0136c34b84e0ef30d48305841e7e1e7ca9b990e3` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/187#issuecomment-6230213083) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39005792456) | -| [PR188](https://github.com/bryanroscoe/shield_optimizer/pull/188) — Keep mobile thermal threshold wording conservative | `dd752bbd60efe8dbec9f5da6a827b80f266b8c2e` | `e05891dfbd33bb513eafb6794dcfd03c41d19ba4` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/188#issuecomment-6230305396) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39006778524) | -| [PR190](https://github.com/bryanroscoe/shield_optimizer/pull/190) — Clarify private saves and clipboard-only report actions | `1006f22bb24ae6a3871396f2f49d2023571ef4f2` | `dccfe99a805ffcb0c11f32a4fa9f227306ba1cf3` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/190#issuecomment-6230445672) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39006905652) | -| [PR191](https://github.com/bryanroscoe/shield_optimizer/pull/191) — Keep the mobile Diagnostics header usable at narrow widths | `0acba8a1bc5bce67f076e4e4227553ef40c97c89` | `5e80c0efd6620060bb03d5c354815990303628ab` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/191#issuecomment-6230606702) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39008777481) | -| [PR192](https://github.com/bryanroscoe/shield_optimizer/pull/192) — Preserve explicit Android RAM accounting in mobile diagnostics | `2bcd3084fc71874c3d5fc909e2388e7e25c54c3d` | `9265e5fdbf4c9c73f7cea5522e50c50dbcd90fe0` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/192#issuecomment-6230736433) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39009711282) | -| [PR193](https://github.com/bryanroscoe/shield_optimizer/pull/193) — Prevent screenshot captures from overwriting earlier saves | `c911d8be54b37a325922a927ce1d09eb45fb614b` | `b095f697b2a24c3d2fb8f56cef7056586b6355ef` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/193#issuecomment-6230822359) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39010618976) | -| [PR194](https://github.com/bryanroscoe/shield_optimizer/pull/194) — Show density-only overrides in the mobile display summary | `3bc3865afffbdc29022766f82640bccf277e1156` | `35b5deaa044bb2218c720eeaa64858486ffc195f` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/194#issuecomment-6230896763) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39010961139) | -| [PR195](https://github.com/bryanroscoe/shield_optimizer/pull/195) — Name mobile screensavers while preserving raw component details | `804c3ac70373239c97bcafe1ab97bc6d5fedf8c8` | `ba7c9d78d31b0a579f5ffd03d92cd403ebf47227` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/195#issuecomment-6231024167) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39011826136) | -| [PR196](https://github.com/bryanroscoe/shield_optimizer/pull/196) — Keep mobile scaling presets usable with enlarged text | `af537cc26204d60e13a00bd3256281243bb32a13` | `a73787a36bbf44311de2596e9a01c8232a049a02` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/196#issuecomment-6231130630) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39012607251) | -| [PR197](https://github.com/bryanroscoe/shield_optimizer/pull/197) — Use guarded Home verification for mobile setup-helper disables | `a909cec0f0368a6505e8be27f64205021f6f11da` | `4148f035c20450751d92d894456162cbc5ab837b` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/197#issuecomment-6231315788) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39013600547) | -| [PR198](https://github.com/bryanroscoe/shield_optimizer/pull/198) — Keep visible APK measurements in mobile app reports | `fd89c0fc9638d1df4b7af6313f949d6e69262dc8` | `60a1934c5e4c575a81bd124698b9a9f6203e5fe8` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/198#issuecomment-6231482040) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39014713923) | -| [PR201](https://github.com/bryanroscoe/shield_optimizer/pull/201) — Integrate reviewed desktop quit and installer hardening into mobile | `93e9d46bed9935cd60ac84f0f0535fae320f58ea` | `d14363bc76c05f2342289d9503e95175b2d44da2` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/201#issuecomment-6233713669) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39030576353) | -| [PR202](https://github.com/bryanroscoe/shield_optimizer/pull/202) — Fix mobile Restore detail action and stale removal selections | `63e5b85355633c3f5f7453a4551e34cd247e5eb0` | `b4546e8b810cfbb9c2729dafa9dbd7c2d51c9c2d` | [Review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/202#issuecomment-6234325588) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39034842743) | -| [PR204](https://github.com/bryanroscoe/shield_optimizer/pull/204) — Preserve unavailable safety in mobile app reports | `b7405034aaba11f911d9e0916a221148238c84cd` | `d388cad1e3da9c17e0a5c0bdeec52022fd8d9970` | [Final review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/204#issuecomment-6235587609) · [6/6 CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39043175482) | - -Original evidence READMEs and raw logs remain historical: their local counts, missing-dependency notes and then-pending review statements are not replaced by current results. PR185's later review/CI/merge closes the report-layout branch gate, PR198's final integration closes its conflict/review gate, and PR201's exact-lock CI closes its disclosed local-cache gate. PR202 records 334 local mobile tests and five planner tests; later PR204 records 343 mobile tests and nine focused cases. Those counts belong to their historical implementation receipts, not older PRs or this integration. PR204's linked final review/CI supersedes the pending wording in its original evidence without changing those logs. - -No new screenshots/video or gallery generation applied to the original documentation-only reconciliation. Existing evidence remains linked from the checklist. Native export wiring, icon preference, inventory, physical Android/vendor/lifecycle acceptance, desktop shared-client/updater acceptance, store/signing and release gates remain unverified as documented there. - -Historical PR203 documentation validation: all 31 local links/anchors resolve; 60 GitHub links match the fetched PR/comment/check-run metadata. All 19 listed merge commits are ancestors of the reconciled mobile base, and each final PR head has six successful checks. The final Rust log contains exactly eight passing desktop process-lifetime fixtures. Checklist table/stale-status checks and `git diff --check` pass. These are documentation/receipt checks; no product tests or visual captures were rerun. - -## Reviewed main scope in the mobile integration draft - -PR206 head `ef72305198af19d7457b97e02cd71cb00b00f7f5` passed independent source review according to the parent handoff and [all six exact-head CI jobs](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39185141432). [PR206](https://github.com/bryanroscoe/shield_optimizer/pull/206) was squash-merged into main as `c78176d5ea640a0e230633a3f0b0bc0720c01af4`. Its public automated summary still reported running when inspected; that is not presented as a completed automated review or a substitute for the parent's independent review. This mobile draft merges that main commit normally, preserving both mobile ancestry and the reviewed main commit. It does not claim the integration is already merged to the default mobile branch. - -The [integration receipt](evidence/launcher-protection-integration-2026-10-19/README.md) separates new host checks from historical PR206 evidence and fresh exact-head CI. Physical Android, native exports, shared-client/updater, store/signing and release gates remain open as listed in the checklist. diff --git a/v2/mobile/RELEASE-POLISH.md b/v2/mobile/RELEASE-POLISH.md deleted file mode 100644 index 41ebcb1a..00000000 --- a/v2/mobile/RELEASE-POLISH.md +++ /dev/null @@ -1,43 +0,0 @@ -# Mobile release polish checklist - -Reconciled 2026-10-19 against mobile branch `d388cad1e3da9c17e0a5c0bdeec52022fd8d9970` (PR204); this integration includes reviewed main `c78176d5ea640a0e230633a3f0b0bc0720c01af4` as a draft. Release polish takes priority over new features. This supplements, and does not replace, [BACKLOG.md](BACKLOG.md). Owner observations below are reports, not completed device verification. Work sequentially in focused PRs. Do not infer permission to access a phone from it being powered on. - -[PR203](https://github.com/bryanroscoe/shield_optimizer/pull/203) merged the documentation reconciliation at `0c78c06823b4827aa3a4345b526f688b2813d739`. PR204 subsequently merged the report-safety follow-up at `d388cad1`; completed PR202/203 scopes and their historical receipts remain intact. - -The implementation status below means branch-complete within each reviewed scope, not released or physically accepted. [Final review, six-job CI and merge receipts](RELEASE-POLISH-RECEIPTS.md) identify the exact heads; original evidence retains its historical counts and limitations. The original documentation reconciliation did not rerun product checks. This integration's separate validation and remaining gates are recorded in [its receipt](evidence/launcher-protection-integration-2026-10-19/README.md). - -| Priority / finding | Current status / evidence | Scope checks / remaining acceptance | -|---|---|---| -| P1 App-sheet actions retain stale selection across reconnect / TV switch | Reproduced in Diagnostics and Optimize: Host rebound an old package to a new connection. Merged PR183 freezes the opening identity, clears parent/intent/paywall/read state and requires a new selection (frontend opening scope). Already-dispatched IPC/native generation enforcement is outside that fix. [Mock evidence](evidence/sheet-identity-2026-10-17/README.md). | Both screens: disconnect, same-endpoint reconnect, switch, failed reconnect/liveness loss, pending safety/measurements, nested Disable/Uninstall/paywall; no mutation from old opening. Fresh-selection mock write bound to current target; combine with PR182 lock/focus cleanup. Phone validation remains separate. | -| P1 Settings look unselected, including Private DNS | PR181 merged at `6f3deefa` after completed source review and six passing CI jobs: display presets now have active-state logic, animation values are normalized, and Custom DNS distinguishes editor state from applied mode. Explicit unset/custom/error states are covered. [Mock evidence](evidence/settings-polish-2026-10-17/README.md). | Mocked click/navigation/reconnect readback, unknown/custom/error and stale-reply guards are covered. Owner phone readback and interaction acceptance remains open. | -| P1 Content under phone status bar | The original overlap combined CSS safe-area env values with an edge-to-edge Android Activity lacking native inset handling. PR181 merged native container padding for bars/cutout at `6f3deefa`; source review, six-job CI and Android debug APK compilation passed. Physical verification remains open. | Phone portrait/landscape, cutout, gesture/3-button navigation, keyboard opening/closing; no overlap or doubled padding. Native compile is necessary but not physical proof. | -| P2 Density-only scaling override labeled as default | Reproduced at physical 3840x2160/640 with density override 300: old summary showed 640 and device default. Focused summary-only fix now shows 300 and density override; preset matching and writes are unchanged. [Simulated before/after evidence](evidence/density-only-label-2026-10-17/README.md). Independently reviewed with all six CI jobs passing; merged in PR194 at `35b5deaa`. PR181 remains intact. | Density-only fixture shows effective override density and Custom/default semantics; matching preset, missing size, defaults and read-error behavior retained. No setting writes from readback. | -| P2 Display scaling preset row at enlarged text | Reproduced 271px content inside 240px at 320px with card text doubled. Separate CSS-only slice wraps this row and retains 44×44px minimum targets; [before/after simulated evidence](evidence/scaling-preset-wrap-2026-10-17/README.md). Final source review and exact CI passed; PR196 merged at `a73787a`. Density-summary PR194 merged at `35b5deaa`; screensaver naming PR195 merged at `ba7c9d78`. | At 320/384px with 100/200% text, preserve every label, selected preset, Tab order, confirmations and returned focus without clipping or setting writes. Phone accessibility acceptance remains open. | -| P1 Background-process limit is confusing | Reported cap comes from `CUR_MAX_CACHED_PROCESSES`; old `global.background_process_limit=2` is separate. Merged PR181 wording distinguishes cap, process count and legacy deletion; source review and six-job CI passed, with no universal vendor claim. Physical vendor/readback acceptance remains open. | Cap 32/value 2, cap absent, legacy absent, deletion success/error; removal must not claim to change the cap. | -| P1 App-details sheet scrolls background | Merged PR182 provides shared lock/focus ownership and visible wrapped keyboard focus in long sheets; PR183 is validated with it. PR186 merged at `83f69b77` completes StockTakeoverDialog registration and sequential menu→report focus handoff, including out-of-order teardown. Final independent reviews and six-job CI passed for these scopes; physical verification remains open. | Lock background while open; sheet itself scrolls; restore prior position/focus on close, navigation and teardown; nested dialogs/reconnect cannot strand a lock. Test touch/wheel and keyboard with mocks, then phone. | -| P2 RAM/storage bars appear inconsistent | Confirmed both fill by used capacity; colors were static green/amber, not health thresholds. Independently reviewed accounting follow-up (PR192, all six CI jobs passed; merged at `9265e5f`) preserves explicit Used when Used + Free exceeds Total, covered by the checked-in Shield fixture and [simulated evidence](evidence/ram-accounting-2026-10-17/README.md). Metrics slice makes direction/percentages explicit, uses common neutral fill and separates missing data. [Mock evidence](evidence/metric-context-2026-10-17/README.md). PR184 merged at `3a1881e9` after independent source review and six passing CI jobs; PR192 is the later accounting follow-up. | 0/50/100%, missing/invalid values, free/available complement and consistent accessible labels/colors; cache use never becomes a pressure score. Phone/large-text check remains open. | -| P2 Temperature guidance too vague | Metrics slice retains sensor/status context, excludes known non-Celsius/limit/cooling objects, shows matched CPU/GPU reported points and removes generic20–100 hotness scale. No new device reads or universal thresholds. Separate wording follow-up uses a neutral reported sensor threshold and explicitly allows throttling below it because HAL/severity provenance is absent. [Mock wording evidence](evidence/thermal-threshold-wording-2026-10-17/README.md). PR184 merged sensor/parser context at `3a1881e9`; PR188 merged conservative wording at `e05891df`. Final independent source reviews and six-job CI passed; physical/vendor acceptance remains open. | Current/sensor/device severity separation; legacy unknowns;30/100°C fixtures; exact family/index/length matching and NaN slots; missing/invalid status never normal. No actual thermal/device validation claimed. | -| P2 Screensaver raw component / Restore previous | Separate display-only fix uses exact owner-package catalog names, Custom screensaver for unknowns and a keyboard-accessible raw Current/Previous disclosure. [Known/custom/Off/missing simulated evidence](evidence/screensaver-labels-2026-10-17/README.md); independently reviewed with all six CI jobs passing and merged in PR195 at `ba7c9d78`. Original-value identity rules and exact restore writes remain intact; ordered-write fix was merged in PR187. | Names never guessed; raw component values retained; catalog error/stale replies, malformed/list values, known/custom/Off/missing states covered. Phone acceptance remains separate. | -| P2 Playback and profiling report readability | Dedicated pass merged in PR185 at `56aa7c56`: grouped playback/resource sections, mode/interface rows, wrapping and scalable hierarchy preserve data, precision, units, warnings and exports. Independent source review and all six CI jobs passed. [Narrow/large-text screenshots, short video and fidelity fixtures](evidence/report-readability-2026-10-17/README.md) record historical local validation (263 mobile tests); physical Android confirmation remains open. | Mock fixtures cover narrow widths, enlarged browser text, long lists, missing values and overflow; screenshots/video are recorded. Physical Android font scaling, pinch/scroll and readability confirmation remain open. | -| P2 Diagnostics header overflows at 320px | Reproduced on merged base dccfe99a. A focused Diagnostics-only CSS fix wraps the header, keeps every control and uses scalable title/Live text with 44px minimum targets. [Mock narrow/large-text evidence](evidence/diagnostics-header-2026-10-17/README.md); independently approved with all six CI jobs passing and merged in PR191 at `5e80c0e`. Original measurement was 304px content inside 272px; phone verification remains open. | At 320px/384px and enlarged text, wrap title/actions without horizontal page overflow; preserve accessible action labels, tap targets and safe-area spacing. Focused browser regression and visual evidence are complete; physical scaling/touch/inset acceptance remains open. | -| P2 Screenshot/export destination and findability | [Merged read-only diagnosis](EXPORT-DISCOVERABILITY.md) traces private destinations, clipboard reports and generated provider scope. The clarification-only UI labels private screenshot/file destinations, explains preview Close retention, and identifies Copy as clipboard text. [Mock UI evidence](evidence/export-discoverability-2026-10-17/README.md). PR190 merged clarification-only UI at `dccfe99a` after completed independent source review and six passing CI jobs. Separate source-reviewed shared screenshot-retention fix (PR193, independently reviewed with all six CI jobs passing; merged at `b095f69`, Android acceptance open) stages complete PNGs and publishes without overwriting same-second or concurrent captures; [deterministic filesystem evidence](evidence/screenshot-retention-2026-10-17/README.md). Native Open/Share/SAF remain unwired; broader platform scope remains a decision. Do not promise Files visibility. | Trace exact app-private path read-only in code; success tells user what was saved and supported open/share/export choices. Save/cancel/error tested. No new broad storage permissions; scope Android picker/share implementation explicitly. | -| P2 Launcher icon appears top-heavy | [Current vs 1.8px downward optical comparison](evidence/settings-polish-2026-10-17/icon-optical-comparison.png) under simulated masks. Assets unchanged for preference review. | Agree subtle direction at launcher size; preserve safe zone/design; if adopted, SVG/vector/all density assets consistent. Actual launcher check separate from simulated masks. | -| P3 Whole-app pinch zoom | Owner is comfortable leaving it enabled. No viewport maximum-scale/user-scalable restriction added. Existing Remote pad has localized gesture handling. | Check zoom with scrolling, tap targets, safe areas, keyboard and modal lock; preserve Android accessibility text scaling. Browser emulation is not physical gesture proof. | -| P3 Many unreviewed apps | Inventory existing BACKLOG / issue history and shared `crates/core/data/app-lists/{common,shield,googletv,known-names}.json` before requesting new source/device reads. No app inventory obtained from a phone. | Classify existing review coverage and missing evidence per package/model/version; unknown stays Unknown; catalog changes individually sourced and safety-tested. Obtain explicit scope before any device interaction. | -| P2 Setup-helper row bypasses guarded Home verification | Source-verified row path used generic launcher disable. Focused routing fix sends setup-helper confirmations to the existing guarded helper command; [mock routing/rollback evidence](evidence/setup-helper-disable-2026-10-17/README.md). Independently reviewed with all six CI jobs passing; merged in PR197 at `4148f035`. | Preserve captured target, busy/refresh/error handling; verify mocked Home failure/re-enable ordering, rollback failure and stale reconnect/switch completion. No physical-device recovery claimed. | -| P2 App report omits visible APK-only fallback | Source-verified Apps report path used diskstats only. Focused fix passes a successful package/session/measurement-bound APK receipt from the detail sheet; [simulated value/preview evidence](evidence/apk-report-fallback-2026-10-17/README.md). Final integrated-head independent source review and all six CI jobs passed; PR198 merged at `60a1934c`. Its final local receipt records 326 mobile tests, check/build, and preserved source/evidence across integration; physical Android validation remains open. | Exact app bytes with apk_files source; data/cache remain unknown; opt-in sharing, diskstats precedence, failure/pending/zero, re-measure, package and connection invalidation covered. No device operations or new queries. | -| P2 Restore details offer Disable / inconclusive removal retains stale selection | PR202 merged at `b4546e8b` after exact-head independent source review and six passing CI jobs. Enable plans now map to disabled state; landed-but-false Disable/Uninstall responses clear child/parent selection in Optimize and Diagnostics while retaining the error and refresh. Confirmed uninstall retains Reinstall; target/session/fresh-safety guards remain. [Mock screenshots, two short videos and regressions](evidence/detail-state-2026-10-17/README.md) record 334 mobile and five core planner tests plus check/build. | Branch-complete in the mocked frontend scope; real Android mutations, transport/native cancellation and physical recovery are not established. | -| P2 Failed fresh sheet safety replaced by stale catalog verdict in reports | PR204 merged at `d388cad1` with exact-head independent source review and six passing CI jobs: explicit null from the sheet now supersedes an older catalog verdict, so report preview/copy remain unavailable unless the existing guarded retry returns a fresh verdict. Absent sheet state still permits the catalog fallback. [Mock regressions and simulated evidence](evidence/report-safety-2026-10-17/README.md). | [Final review receipt](https://github.com/bryanroscoe/shield_optimizer/pull/204#issuecomment-6235587609) and [exact-head CI](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39043175482) complete PR204's branch gate. Its historical local receipt records 343 mobile tests; physical safety/device acceptance remains open. Unknown and unavailable remain distinct, without a new safe/unsafe claim. | -| P2 Firmware rejects changing protected stock launcher (issue #205) | [PR206](https://github.com/bryanroscoe/shield_optimizer/pull/206) merged to main at `c78176d5` after independent source review reported by the parent and [six passing exact-head CI jobs](https://github.com/bryanroscoe/shield_optimizer/actions/runs/39185141432). This draft integrates its shared guidance and qualified README promises into mobile with existing polish preserved. [Original simulated evidence](../evidence/firetv-protection-205-20261019/README.md) remains historical; [integration receipt](evidence/launcher-protection-integration-2026-10-19/README.md) records the combined checks. | Protection guidance requires explicit evidence for the exact package; exit 255 alone is inconclusive. Raw failures, precautionary restore outcome and actual Home readback remain separate. Reporter versions/build diagnostics and physical acceptance remain open; independent review/fresh CI are required for this integration. | -| Integration: desktop quit/installer hardening on mobile base | PR201 merged at `d14363bc` after final independent source review and six exact-lock/platform CI jobs passed. Original mobile tree is preserved; quit orchestration remains desktop-only. [Integration evidence](../../docs/INTEGRATION-MOBILE-MAIN-2026-10-17.md) retains local-cache limitations and historical logs. | Exact-lock CI gate is closed for PR201's head. Real shared-client/custom-server/updater/restart behavior across desktop platforms and physical Android acceptance remain open. | -| Store / signing / release readiness | This checklist and the reviewed development-branch merges do not approve publication. Existing [release-readiness backlog](BACKLOG.md#p2--release-readiness), [release procedure](RELEASE.md) and [licensing plan](LICENSING.md) remain authoritative. | Preserve outstanding store/checkout and entitlement acceptance, production signing/key custody, signed build/release measurements and owner release authorization. No store purchase, signed release or publication is verified here. | - -## Physical Android acceptance remains open - -Owner-driven phone/TV checks still cover system bars/cutout/rotation/navigation mode/keyboard, accessibility font scaling and pinch gestures, touch scrolling and modal focus, reconnect/liveness and native lifecycle, settings/vendor readback, measurements and real recovery. Browser screenshots and enlarged root text are simulated; debug compilation is not a device pass. Native Open/Share/SAF exports, optical icon preference and app-inventory work remain open at the scope stated above. See [LIFECYCLE-EVIDENCE.md](LIFECYCLE-EVIDENCE.md) and [BACKLOG.md](BACKLOG.md). - -## Historical first-branch validation - -The original `fix/mobile-settings-polish-20261017` receipt was based on `mobile-parity-pass` `5f4fc14`, restricted to settings readback/copy, native safe-area handling, tests/evidence and this checklist. That receipt describes its original scope; subsequent completion comes from the separately linked PRs, not from retroactively broadening that branch's validation. - -No install, real-device interaction, network/device setting change, credential operation or publication was performed for this documentation update. Screenshots/video and gallery regeneration are not applicable: no UI, assets or runtime behavior changed; existing simulated evidence remains linked rather than recaptured as a new acceptance claim. diff --git a/v2/mobile/RELEASE.md b/v2/mobile/RELEASE.md index f2106501..44678911 100644 --- a/v2/mobile/RELEASE.md +++ b/v2/mobile/RELEASE.md @@ -160,12 +160,11 @@ $ANDROID_HOME/build-tools/36.0.0/apksigner verify --print-certs \ answers. Include user-initiated external navigation and review the resolved SDK graph. 4. Promote internal → closed → production once a real device has been exercised end to end. -## Testing-candidate gates (feature freeze, 2026-10-16) +## Testing-candidate gates This is a preparation checklist, not approval to create credentials, upload or install a build. -No purchase, trial or pricing policy is selected. Keep the existing feature set frozen. +No purchase, trial or pricing policy is selected. Release polish takes priority over new features. -- [ ] Integrate the independently reviewed reconnect fix (PR167) into the chosen candidate. - [ ] Record exact commit, lockfiles, SDK/NDK/JDK versions and selected ABIs. - [ ] Run host Rust fmt/clippy/tests, mobile `npm test`, check and build. Host CI is not an Android build. - [ ] Resolve and review the Android release runtime dependency graph and its notices (see @@ -202,14 +201,13 @@ No purchase, trial or pricing policy is selected. Keep the existing feature set Android 11+ SPAKE2 code pairing is implemented in `src-tauri/src/wireless_adb.rs` and the vendored transport. Signed Ed25519 offline licensing, persistence and debug-only development keys are -implemented; see `LICENSING.md`. The old claims that these implementations were missing were stale. -Neither implementation establishes current phone/TV end-to-end readiness. +implemented; see `LICENSING.md`. Neither implementation establishes current phone/TV end-to-end readiness. ### Still missing or unresolved SAF import/export, mobile release automation, verified signed APK/AAB evidence, complete resolved Android dependency notices, privacy/store disclosures, and a selected commerce channel remain open. -The preflight-tools workflow tests the APK checker with synthetic artifacts/mocked SDK outcomes; +The `mobile-release-preflight.yml` workflow tests the APK checker with synthetic artifacts/mocked SDK outcomes; it does not build/sign an APK, validate the native Android transport, or publish anything. ### Current external requirements to verify at submission @@ -226,41 +224,15 @@ Check current rules again before submission: For local-network access, SDK36 and SDK37 have different requirements; do not add/request a future permission speculatively: https://developer.android.com/privacy-and-security/local-network-permission -## Open Shield issues at the 2026-10-16 release audit +## Open issues to recheck before a candidate -This is a dated triage snapshot, not a claim that an open issue is resolved. Re-read the tracker -before cutting a candidate; close only after its actual acceptance evidence is reviewed. +Re-read the tracker before cutting a candidate; close an issue only after its actual acceptance +evidence is reviewed. | Issue | Release disposition / exact remaining evidence | |---|---| | [#93 Saved-TV discovery/reconnect](https://github.com/bryanroscoe/shield_optimizer/issues/93) | Physical gate: Shield without mDNS, rotated Google TV port, changed address, cached identity/name isolation and honest reconnect results. Browser coverage does not close this. | | [#105 Restart/lifecycle report](https://github.com/bryanroscoe/shield_optimizer/issues/105) | Physical gate: distinguish Home/background, Back, Recents, recreation and process death. Do not claim restored live state or replay mutations from browser reconstruction. | | [#123 Glance/Daydream](https://github.com/bryanroscoe/shield_optimizer/issues/123) | Current screensaver controls are not proof that the reporter's server-delivered Glance variant is covered. Obtain exact package/device behavior and reporter confirmation; retain vendor restore/reconnect tests. | -| [#161 Launcher sources/default](https://github.com/bryanroscoe/shield_optimizer/issues/161) | Desktop source-site work merged in PR165; mobile parity includes source links. Confirm the requested installed/current-default case and release availability; do not reimplement or auto-close. | -| [#162 Logcat](https://github.com/bryanroscoe/shield_optimizer/issues/162) | Desktop Logs merged in PR166. Confirm requested filtering/live-display acceptance and release availability; it is not a reason to add mobile features during this freeze. | - -PR167 reconnect safety was merged into `mobile-parity-pass` at -`f7897c1292d702fdfad1b77c3193312a641475b2` after independent review and six passing PR checks. -Its integration does not clear the physical gates above. Release-preparation PR169 includes that -ancestry by a normal merge. The existing test workflow triggers pushes to `main`, not feature -branches; a missing postmerge feature-branch run must not be reported as a passing run. - -### Preparation validation receipt (2026-10-16) - -On the release-preparation branch after merging `f7897c1`: 218 mobile browser/unit tests passed, -13 APK-verifier tests passed, mobile check reported 0 errors / 0 warnings and build succeeded. -The neutral paywall was inspected at 384×812. Notices regeneration inventoried 313 Cargo crates; -Android/JVM declarations are listed separately without claiming full transitive resolution. -The installed SDK apksigner rejected an unsigned synthetic archive through the CLI (exit 1, -no success receipt). Positive signing/SDK outcomes in unit tests are mocked: no genuine signed -release artifact or Android runtime was validated. This receipt is not release authorization. - -### Modal polish follow-up (2026-10-17, merged PR186) - -The separate modal follow-up registers `StockTakeoverDialog` with shared scroll/focus ownership -and preserves original-row focus through sequential `AppActionMenu` → Report transitions. -Regression coverage includes cancellation, async replacements, nested/out-of-order teardown, -short-screen keyboard access and restored scroll. See -[evidence and verification limits](evidence/modal-handoffs-2026-10-17/README.md). -These two P2s passed independent source review and all six CI jobs, and PR186 merged at -`83f69b77e2b19b643e21ae456d0f597e90ef7d83`. Host simulation does not clear physical-device gates. +| [#161 Launcher sources/default](https://github.com/bryanroscoe/shield_optimizer/issues/161) | Desktop shows source-site links for installed and current-default launchers, and mobile parity includes them. Confirm the requested case and release availability; do not reimplement or auto-close. | +| [#162 Logcat](https://github.com/bryanroscoe/shield_optimizer/issues/162) | Desktop has a read-only Logs tab; mobile does not expose it. Confirm the requested filtering/live-display acceptance and release availability; it is not a reason to add mobile features before release. | diff --git a/v2/mobile/TRANSPORT-LICENSING-RESEARCH.md b/v2/mobile/TRANSPORT-LICENSING-RESEARCH.md deleted file mode 100644 index 0d21f593..00000000 --- a/v2/mobile/TRANSPORT-LICENSING-RESEARCH.md +++ /dev/null @@ -1,316 +0,0 @@ -# ATV Optimizer — Transport Licensing & Architecture Research - -> **Decision record — adopted, and the outcome is now shipped.** `adb_client` (MIT) replaced -> libadb-android in `3e8bcfc`; SPAKE2 pairing landed in `5e76b24`. Passages below written in the -> present tense about libadb describe the *old* transport, not the current one — see -> [`HANDOFF.md`](HANDOFF.md) §3. - -Question: how should the phone/tablet app drive an Android TV over **Android-11 wireless -debugging** (SPAKE2 pairing-code + TLS-1.3 + RSA auth + shell/exec) **without shipping GPLv3 -code**, so the product can be sold closed-source? Current transport is -`io.github.muntashirakon:libadb-android` (GPL-3.0), which (a) hangs on `exec:` reads against an -older Nvidia Shield's adbd and (b) is license-incompatible with a commercial closed-source app. - -Date: 2026-07-04. Author: research agent. **Not legal advice — get counsel to sign off on the -licensing conclusions before shipping.** - -Confidence legend: **[CONFIRMED]** = verified against primary source; **[LIKELY]** = strong -indirect evidence; **[UNCERTAIN]** = needs verification. - ---- - -## Executive summary (recommendation) - -1. **Drop libadb-android.** It is GPL-3.0(-or-later) per its own repo — a hard blocker for a - closed-source paid app. **[CONFIRMED as blocker; license per repo — verify exact SPDX.]** - -2. **The single most useful finding:** the Rust crate **`adb_client` (MIT)** already implements - the *hard, reliability-sensitive* half in pure Rust — direct-to-device **TLS-1.3 connect + RSA - AUTH + OPEN/OKAY/WRTE/CLSE stream multiplexing + shell / exec / install / push / pull** — with - **no adb binary and no adb server** (`adb_client::tcp::ADBTcpDevice`). It uses `rustls` + - `rcgen`. This is exactly the layer where libadb-android is buggy, and it drops cleanly into the - Tauri **Rust** side, matching the app's "logic in Rust, thin Kotlin" architecture. **[CONFIRMED - by reading the crate source.]** - -3. **The one gap:** `adb_client` does **NOT** implement the Android-11 **SPAKE2 pairing** handshake - in pure Rust. Its `pair()` only proxies `host:pair:<code>:<addr>` to a *running adb server* - (`server/commands/pair.rs`) — useless without an adb binary. Direct `ADBTcpDevice` assumes the - device is **already paired/trusted**. So a first-run pairing step must be built. **[CONFIRMED by - source: the only pairing code path goes through the server proxy.]** - -4. **Recommended architecture (primary):** `adb_client` (MIT) for connect + streaming, **plus a - small clean-room SPAKE2 pairing module** (one-time flow), referencing the **Apache-2.0** AOSP - `packages/modules/adb/pairing_*` sources. Pairing is the only real net-new work (~1–2 weeks; - the hard part is BoringSSL-compatible SPAKE2 over Ed25519 + the TLS keying-material export). - -5. **Fastest-to-ship fallback:** bundle an **aarch64 `adb` binary built from AOSP source - (Apache-2.0)**, exec it from the APK's `nativeLibraryDir`, run its local server, and drive it - (pair/connect/shell) via `adb_client`'s server mode. This gets **native, battle-tested SPAKE2 - pairing for free**, at the cost of a ~5–10 MB per-arch binary, a server-process lifecycle, and - text-output parsing. Termux proves adb runs on unrooted Android. **[CONFIRMED feasible.]** - -6. **Do NOT redistribute Google's prebuilt platform-tools `adb`:** (a) those are *host* binaries - (x86-64 Linux/macOS/Windows), not Android-arm, so they won't even run on the phone; and (b) the - **Android SDK License Agreement §3.4 forbids redistribution** of the packaged SDK. Building - `adb` from AOSP source sidesteps *both* problems and is Apache-2.0 clean. **[CONFIRMED.]** - -7. **atvtools reality (previous guess was wrong):** it is a **phone/tablet-side app** (both - **iOS** and **Android** editions) that talks to the TV over **wireless ADB across the LAN** — - *not* Shizuku, not an accessibility service, not loopback, not a TV-side app. It is - **closed-source / proprietary**, sold freemium on Google Play (and App Store). Its iOS edition - proves a from-scratch (non-libadb) wireless-ADB client is viable and shippable on stores. - **[CONFIRMED it's phone-side over wireless ADB; iOS+Android; closed-source. Exact ADB impl - not public — UNCERTAIN.]** - -8. **Feasibility verdict:** the app connects phone→TV **outbound over the LAN** — it does *not* - need loopback, root, or Shizuku, and needs no elevated privilege on the phone. That makes the - pure-Rust path (#4) clean and the only genuinely novel code the SPAKE2 pairing handshake. - ---- - -## Q1 — License of Google's / AOSP `adb`; can a commercial app ship the prebuilt binary? - -### AOSP `adb` source: Apache-2.0 **[CONFIRMED]** -- AOSP's preferred license is **Apache-2.0**, and the `adb` module - (`platform/packages/modules/adb`, historically `system/core/adb`) carries the standard "The - Android Open Source Project" Apache-2.0 headers (SPDX `Apache-2.0`). - Source: <https://source.android.com/license> -- Apache-2.0 permits commercial, closed-source redistribution of source/derivatives provided you - keep the license/NOTICE and attribution. So **building `adb` yourself from AOSP source and - shipping it in a paid closed-source product is permitted.** - -### Google's *prebuilt* platform-tools package: redistribution NOT permitted **[CONFIRMED]** -- The downloadable "SDK Platform-Tools" package (which contains the prebuilt `adb`/`fastboot`) is - governed by the **Android Software Development Kit License Agreement**. **§3.4** states you may - not "copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse - engineer, disassemble, or create derivative works of the SDK or any part of the SDK" — *except - as required by applicable third-party licenses.* - Source: <https://developer.android.com/studio/terms> -- The "third-party licenses" carve-out is why people say the underlying Apache-2.0 `adb` is - redistributable — but the safe, unambiguous route is to **rebuild from AOSP source** (governed - purely by Apache-2.0) and comply with its NOTICE/attribution, rather than reship Google's ToS- - bound package. - -### Extra reason not to use the prebuilt: wrong architecture **[CONFIRMED]** -- Google's platform-tools `adb` binaries are **host** binaries (x86-64 desktop OSes). There is no - official Google-shipped **Android arm64** `adb`. To run `adb` *on the phone* you must build it - for `aarch64` from AOSP source (or use a community build — see Q2). - Sources: <https://developer.android.com/tools/releases/platform-tools>, - <https://github.com/qhuyduong/arm_adb>, <https://archlinuxarm.org/packages/aarch64/android-tools> - -**Bottom line:** Bundling `adb` is legally fine **if built from AOSP source (Apache-2.0)** for -arm64. Reshipping Google's prebuilt package is both technically wrong (host arch) and ToS-barred. - ---- - -## Q2 — Feasibility: running an `adb` binary inside an unrooted Android app - -**Verdict: feasible and proven (Termux does it), but with real packaging friction.** Note the ATV -Optimizer's own connection is phone→TV *over the LAN*, so it does **not** need the loopback/root -trick that most "on-device adb" guides describe — it just needs adb to make outbound TLS -connections and (optionally) run a local server on 127.0.0.1. - -Key facts: -- **W^X (Android 10+):** apps cannot `exec()` from writable dirs. The supported path is to place - the executable in the **APK's native-lib dir** and exec from `getApplicationInfo().nativeLibraryDir`. - Requirements: name it `lib*.so` (only that pattern is extracted), set - **`android:extractNativeLibs="true"`** so it lands on disk, and set `LD_LIBRARY_PATH` if it needs - non-system `.so`s. **[CONFIRMED]** - Sources: <https://medium.com/android-knowledge-store/execute-native-executable-on-android-d6e897e71898>, - <https://www.androidbugfix.com/2022/01/android-can-execute-process-for-android.html> -- **Local server / localhost bind:** `adb` starts a server on `127.0.0.1:5037` by default; binding a - loopback TCP port from an app sandbox is allowed (no special permission). **[LIKELY — standard - sandbox behavior; Termux relies on it.]** -- **`adb pair` / `adb connect` from on-device:** works. Termux users routinely run - `adb pair 127.0.0.1:<port>` / `adb connect …` on unrooted phones. For the ATV Optimizer the target - is the TV's LAN IP rather than loopback, which is strictly simpler (outbound only). - Sources: <https://xdaforums.com/t/termux-adb-running-adb-within-android-using-termux-wireless-adb.4724780/>, - <https://gist.github.com/kairusds/1d4e32d3cf0d6ca44dc126c1a383a48d>, - <https://www.howtogeek.com/you-dont-need-a-pc-for-adb-anymoreheres-how-to-do-it-in-termux/> -- **Getting an arm64 build:** community build scripts / prebuilts exist - (`bonnyfone/adb-arm`, `qhuyduong/arm_adb`, Arch Linux ARM `android-tools`, Termux - `android-tools`), or build from AOSP. - Sources: <https://github.com/bonnyfone/adb-arm>, <https://github.com/qhuyduong/arm_adb> - -**Pitfalls:** ~5–10 MB binary per ABI (arm64 + maybe armeabi-v7a); `extractNativeLibs=true` -inflates install size and is otherwise-discouraged; you own the adb-server process lifecycle -(start/kill, port conflicts, `adb kill-server`); you parse human-oriented text output; and you must -build/track an arm64 adb across Android versions. Workable, but heavier than a library. - ---- - -## Q3 — How atvtools (`dev.vodik7.atvtools`) actually works - -**Corrected mechanism [CONFIRMED at the architecture level]:** -- It is a **phone/tablet-side controller app**, published by developer **tvDev** for **both Android - (`dev.vodik7.atvtools`, 100k+ installs) and iOS**. It manages Android TV / Fire TV / Nvidia Shield - devices remotely. -- It connects to the TV over **wireless ADB across the local network** — you enable Developer - options + (wireless/USB) debugging on the TV, then the app connects to the TV's IP. It then runs - ADB services: **sideload APKs, push files, remote/mouse input, and shell commands (e.g. disabling - preinstalled apps)** — i.e. the same `pm`/`dumpsys`/shell surface ATV Optimizer uses. - Sources: <https://play.google.com/store/apps/details?id=dev.vodik7.atvtools>, - <https://tvdevinfo.com/atvtools> (describes the iOS edition: "same Wi-Fi network as the Android - TV/Fire TV", enable Developer options + USB debugging, connect by IP, shell commands to disable - preinstalled apps). -- **It is NOT Shizuku, NOT an accessibility service, NOT a loopback/on-device-adb trick, and NOT a - TV-side app.** It is a networked wireless-ADB client to a *separate* device — the same category as - ATV Optimizer. -- **Distribution & license:** closed-source / proprietary, freemium (free tier + paid premium), via - Google Play and the App Store. No public source repo or OSS license found. - Source: <https://chrome-stats.com/d/dev.vodik7.atvtools> - -**Why this matters:** the **iOS** edition cannot use libadb-android (Java/Android-only), so tvDev -necessarily has a non-libadb wireless-ADB implementation (their own client, or a -C/Swift/portable library). That is direct precedent that a from-scratch wireless-ADB client is -shippable on app stores as a commercial product. **[The exact library/implementation they use is -not public — UNCERTAIN.]** - ---- - -## Q4 — Permissively-licensed libraries for the Android-11 wireless-debugging flow - -### `adb_client` (Rust) — **MIT** — the strongest option **[CONFIRMED by source review]** -Repo: <https://github.com/cocool97/adb_client> · crates.io: <https://crates.io/crates/adb_client> -(v3.2.2, actively maintained — 279+ commits, 41 releases). - -What it *does* provide, **pure-Rust, no adb binary/server** (`adb_client::tcp::ADBTcpDevice`): -- Direct TCP connect to a device, **TLS-1.3 upgrade via `rustls`**, self-signed cert generated from - the RSA keypair via **`rcgen`** (`certificate_from_pk`, `PKCS_RSA_SHA256`) — i.e. the ADB - **STLS/AUTH** path. -- **Stream multiplexing + `shell`, `shell_command` (exec-style), `install`, `push`, `pull`, `stat`, - `reboot`, `framebuffer`, etc.** via the `ADBDeviceExt` trait. -- mDNS discovery (`adb_client::mdns`), useful for finding the TV's dynamic TLS connect port. -- Evidence of active TLS work: PR #202 "fix: incorrect private key used for TLS upgrade" (2026-05), - PR #21 "Add pairing and connecting features over tcp", PR #30 "Fix pairing". - -**Critical limitation:** its **pairing** (`server/commands/pair.rs`) is implemented **only** as a -proxy command (`host:pair:<code>:<addr>`) to a **running adb server** — there is **no pure-Rust -SPAKE2 pairing**. `ADBTcpDevice` (the no-server path) therefore requires the device to be -**already paired**. So `adb_client` covers connect+streaming brilliantly but leaves the **first-run -SPAKE2 pairing** to you. **[CONFIRMED: SPAKE2 not present; only server-proxy pairing exists.]** - -This limitation is *exactly the opposite* of libadb-android's problem: libadb does pairing but -streams unreliably; `adb_client` streams reliably but doesn't pair. That asymmetry drives the -recommended hybrid. - -### `spake2-java` (MuntashirAkon) — for the pairing piece -Repo: <https://github.com/MuntashirAkon/spake2-java>. **BoringSSL-compatible SPAKE2 in Java**, -explicitly tested against Android-11 wireless-debugging pairing. Self-described **unaudited**. It is -what libadb-android uses. License is MuntashirAkon's (**likely GPL-family — VERIFY**; if GPL it's a -blocker, but it's a useful *reference* for a clean-room port). **[UNCERTAIN license.]** - -### Rust `spake2` crate (RustCrypto/Brian Warner) — NOT drop-in compatible **[LIKELY]** -A general Rust `spake2` crate exists, but ADB uses **BoringSSL's `spake25519`** variant with -specific M/N generator points; a generic SPAKE2 crate uses different constants and will **not -interoperate** on the wire without matching BoringSSL's exact parameters. Treat pairing as a -custom/ported implementation, not "add a crate." -Source (compatibility caveat): <https://crates.io/crates/adb-wireless>, -<https://www.androidauthority.com/android-17-adb-wi-fi-2-0-3678411/> (notes Google itself rewrote -ADB Wi-Fi internals in a ~4,000-line Rust lib in Android 17 — confirms the space is Rust-friendly, -but that lib handles reconnection/network monitoring, not a reusable public SPAKE2 pairing API). - -### Other crates (weaker) -`adb-wireless`, `adbqr`, `radb`, `Rust-ADB` — smaller/less proven; `adb_client` is the mature one. -No **Apache/MIT Kotlin/Java** library was found that does the full pairing flow *and* is -permissively licensed (the Java options in this space, incl. libadb-android and spake2-java, are -GPL-family). **[CONFIRMED no permissive Kotlin/Java full-flow lib surfaced.]** - ---- - -## Q5 — Clean-room reimplementation scope - -Pieces of the ADB client protocol and their difficulty: - -| Piece | What it is | Difficulty | Covered by `adb_client`? | -|---|---|---|---| -| CNXN banner + protocol version | Initial handshake, feature negotiation | Easy | Yes | -| RSA key AUTH (`AUTH` token/signature/pubkey) | Trust establishment on connect | Easy–Med | Yes | -| Stream mux OPEN/OKAY/WRTE/CLSE | Multiplexed logical streams; **CLSE/EOF handling is the libadb bug** | Med | **Yes** (this is the win) | -| `shell:` / `exec:` / `shell,v2:` services | Command execution + stdout/stderr/exit | Med | Yes | -| TLS-1.3 wrap of the connect stream (STLS) | Conscrypt/rustls mutual TLS after CNXN on Android 11+ | Med | Yes (`rustls`) | -| **Android-11 pairing: SPAKE2 over TLS-1.3 pairing service** | 6-digit code → SPAKE2 (BoringSSL `spake25519`) key exchange over a TLS channel using the **exported keying material** as context, then exchange/enroll the RSA public key | **HARD** | **No** | - -**The only hard, uncovered part is the pairing handshake.** Its sharp edges: -1. **BoringSSL-exact SPAKE2** (`spake25519`, Ed25519 group, specific M/N seed points). Must match - byte-for-byte or the device rejects it. Reference: AOSP `pairing_auth` and MuntashirAkon's - `spake2-java` (which ports BoringSSL). **[CONFIRMED this is the crux.]** -2. **TLS exported-keying-material binding:** the pairing runs *inside* a TLS-1.3 session and mixes - the TLS export key into the SPAKE2 password/context — you need a TLS stack that exposes - `export_keying_material` (rustls does). -3. **Pairing packet framing** (`pairing_connection` / `pairing_packet` state machine) — mechanical - once SPAKE2 + TLS export are right. - -You **can** reference the **Apache-2.0** AOSP `packages/modules/adb/pairing_auth/`, -`pairing_connection/` sources to build this cleanly (Apache-2.0 permits derivative closed-source -work with attribution) — so it need not even be a strict clean-room; it can be a licensed port. -**[CONFIRMED Apache-2.0 permits this.]** - -**Effort estimate [UNCERTAIN — engineering judgment]:** -- Connect + shell/exec/streaming: **~0** (use `adb_client`, MIT). -- SPAKE2 pairing module in Rust (port BoringSSL spake25519 + TLS-export binding + packet framing, - test against a real Shield): **~1–2 weeks** for someone comfortable with crypto/protocol work; - add buffer for device-specific quirks. Full from-scratch reimplementation of *everything* - (ignoring adb_client) would be **~4–8 weeks** and is not recommended given adb_client exists. - ---- - -## Ranked recommendation - -1. **`adb_client` (MIT) for connect/stream + clean-room-or-ported SPAKE2 pairing (Apache-2.0 - reference).** Best fit: pure Rust in the Tauri layer, no GPL, the reliability-critical stream - code is maintained upstream, and the buggy CLSE/EOF path leaves libadb behind. Only novel work is - the one-time pairing handshake. **Recommended primary.** - -2. **Bundle an AOSP-built (Apache-2.0) arm64 `adb`; run its server; drive via `adb_client` server - mode (or shell-out).** Gets native, proven SPAKE2 pairing "for free"; licensing is clean if built - from source. Downsides: per-arch binary size, `extractNativeLibs=true`, server-process lifecycle, - text parsing, arm64-adb maintenance. **Recommended fallback / fastest prototype**, or a hybrid: - bundle adb *only* to perform the one-time pairing, then use pure-Rust `adb_client` for all - ongoing connect/shell traffic. - -3. **Full clean-room reimplementation of the entire ADB client** referencing Apache-2.0 AOSP. Only - if #1's crate dependency is undesirable. Larger effort, little marginal benefit over #1. - -4. **Redistribute Google's prebuilt platform-tools `adb`.** ✗ Rejected — wrong architecture (host, - not Android-arm) *and* SDK ToS §3.4 bars redistribution. - -5. **Keep libadb-android.** ✗ Rejected — GPL-3.0 blocks a closed-source paid product, and it's the - source of the current exec-stream hang. - ---- - -## What is confirmed vs uncertain - -**Confirmed:** AOSP `adb` is Apache-2.0; SDK ToS §3.4 bars redistributing the prebuilt package; -Google ships no arm64 host adb; adb runs on unrooted Android (Termux); Android 10+ requires exec -from nativeLibraryDir with `extractNativeLibs=true`; `adb_client` is MIT and implements TLS connect -+ RSA auth + shell/exec/stream but only server-proxy pairing (no pure-Rust SPAKE2); atvtools is a -closed-source phone-side (iOS+Android) wireless-ADB LAN client, not Shizuku/accessibility/loopback. - -**Uncertain / verify before committing:** exact SPDX of libadb-android and spake2-java (assumed -GPL-family); whether any Rust SPAKE2 crate can be parameterized to BoringSSL `spake25519` -compatibility (assessed: not off-the-shelf); atvtools' exact ADB implementation/library (not -public); the pairing-module effort estimate; and — because this gates the ability to *sell* the -product — **all licensing conclusions should be reviewed by legal counsel.** - ---- - -## Sources -- AOSP content license (Apache-2.0 preferred): https://source.android.com/license -- Android SDK License Agreement (§3.4 redistribution): https://developer.android.com/studio/terms -- Platform-tools release notes (host binaries): https://developer.android.com/tools/releases/platform-tools -- `adb_client` repo (MIT): https://github.com/cocool97/adb_client -- `adb_client` crates.io: https://crates.io/crates/adb_client -- `adb_client` docs.rs: https://docs.rs/adb_client -- spake2-java (BoringSSL-compatible, unaudited): https://github.com/MuntashirAkon/spake2-java -- libadb-android: https://github.com/MuntashirAkon/libadb-android -- atvtools Play listing: https://play.google.com/store/apps/details?id=dev.vodik7.atvtools -- atvtools (iOS) dev site: https://tvdevinfo.com/atvtools -- atvtools metadata (proprietary, freemium): https://chrome-stats.com/d/dev.vodik7.atvtools -- Execute native binary on Android 10+: https://medium.com/android-knowledge-store/execute-native-executable-on-android-d6e897e71898 -- W^X / nativeLibraryDir exec: https://www.androidbugfix.com/2022/01/android-can-execute-process-for-android.html -- Termux wireless ADB on-device: https://xdaforums.com/t/termux-adb-running-adb-within-android-using-termux-wireless-adb.4724780/ -- Termux adb pairing gist: https://gist.github.com/kairusds/1d4e32d3cf0d6ca44dc126c1a383a48d -- arm64 adb build/prebuilt: https://github.com/bonnyfone/adb-arm , https://github.com/qhuyduong/arm_adb , https://archlinuxarm.org/packages/aarch64/android-tools -- Android 17 ADB Wi-Fi 2.0 / Rust rewrite: https://www.androidauthority.com/android-17-adb-wi-fi-2-0-3678411/ diff --git a/v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md b/v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md deleted file mode 100644 index af9b02c7..00000000 --- a/v2/mobile/UNKNOWN-DIAGNOSTICS-UX.md +++ /dev/null @@ -1,154 +0,0 @@ -# Unknown app diagnostics: local collection and review - -> **Implemented.** This was the pre-implementation contract; it shipped in `d02de57` as -> `src/lib/unknownDiagnostics.ts`, covered by `tests/unknownDiagnostics.test.mjs`. - -Navigator UX handoff for `so-fb3.6` / `.6.1`; registry ownership `so-fb3.7` -belongs to Mechanic. This is the pre-implementation design/source-review contract; -the later `.6.1` integrated-local implementation and its separate test evidence are -tracked in `HANDOFF.md`. -Source inspected at `48cba2384ca3938e6655b77ccf85716dd86cca5f` in -`crew/navigator`. No app/device interaction, build or tests in this pass. -`.6` is P1: `.6.1` local diagnostics is immediate bounded work and must not wait -for an external reporting backend. `.7` establishes durable registry maintenance; -`.1` discovery and `.2` canonical safety/identity retain implementation priority. - -## Minimum local experience - -Add an **Unknown app diagnostics** card in Settings (`screens/More.svelte`), -separate from the current Debug log. Suggested copy: - -> Keeps a small local record of apps we haven't reviewed and processes we -> couldn't match to an app. Nothing is sent automatically. - -Show **N distinct records**, then **Review records** and **Clear records**. -Empty state: “No unknown apps or unmatched processes have been recorded.” This -does not mean that all installed apps were scanned, reviewed or found safe. -Collection uses actual completed app/health reads and `.2` canonical outcomes; -opening Settings must not trigger device discovery, inventory scans or extra ADB -calls. There is no periodic model, background service or upload job. - -The review panel shows a local snapshot with: - -- **App not reviewed** or **Unmatched process**, exact validated token and a - plain reason from a fixed reason-code vocabulary. -- First/last recorded times and occurrence count; explain that repeat refreshes - are grouped, so the count is observations rather than distinct devices/apps. -- App and registry versions, plus generic device family/OS only when available - through the approved typed fields. Missing registry version reads “Not - available,” never a fabricated version or a substituted app version. - -Title the available action **Copy diagnostic JSON** when the implementation only -copies text. Clipboard output is not a downloaded file, a sent report or an -Android share/export implementation. Provide visible selectable JSON when -clipboard access fails: “Couldn't copy. You can select and copy the text below.” -If a real local file export is supported, label it **Save diagnostic JSON** and -show the actual result; cancellation is not success. Never claim the export was -shared with developers merely because a local copy was created. - -The preview must be the exact snapshot copied/saved; new observations can update -the record count but must not silently alter an open payload. **Refresh preview** -explicitly takes a new snapshot. Suggested note: - -> Includes app or process identifiers, observation times and version information. -> Review the contents before sharing. No network addresses, device serials, -> license keys or full debug logs are included. - -Identifiers may reveal which unknown apps were encountered, so do not call this -anonymous. No account, destination, consent dialog for a nonexistent service, or -purchase flow is needed for the local minimum. - -**Clear records** confirmation: “Delete the diagnostics stored in this app? -Copies you saved or shared will remain.” Confirm label: **Clear records**. -On success, clear the preview and report “Local diagnostics cleared.” Collection -may record future encounters; this is not an opt-out or a change to app risk, -saved TVs, device state, license or ordinary debug logs. A storage-clear failure -must remain visible and must not claim deletion. Prevent in-flight persistence -of an older buffer from restoring cleared records. - -For unavailable/full storage, use “Couldn't save diagnostics on this device.” -The TV screen and app actions continue normally. If a bounded collector evicts -older records, say “Only the most recent records are kept”; export metadata can -record truncation without leaking rejected input. Do not promise complete history -or survival after uninstall/clear-data. - -## Collection boundary accepted from Mechanic - -Use a typed collector alongside `lib/log.ts`, with schema/version, generic -family/OS when available, kind (`installed_package` or `unresolved_process`), -validated package/process token, reason code and first/last/count. Repeated -observations deduplicate by the agreed version/family/kind/token/reason key; the -count saturates and both record count and serialized byte size are bounded. -Mechanic/Sol must specify the numeric limits in the implementation and evidence. - -Do not export raw command arguments/errors or copy the existing combined log. -At baseline `lib/log.ts::summarizeArgs` redacts several secret-name patterns but -still retains ordinary `serial`/host arguments, and `More.svelte::combinedLog` -includes native logs. That mechanism cannot satisfy this diagnostic allowlist -unchanged. No serial, host/IP/MAC, auth/license/input, user-defined TV name, raw -shell output, unrelated inventory or full build fingerprint belongs here. - -Strict token validation rejects paths, control strings and address-like values. -Rejected raw values must not be smuggled into a reason/error field. Render token -text safely and never treat it as HTML, a command, URL or instruction. Reports -are untrusted evidence. A process such as `system` remains inspect-only, never a -package action target. A transient failed risk lookup is not canonical Unknown; -if collected as a lookup failure, it needs a distinct approved reason code. - -The collector must consume `.2` Unknown and process-resolution results, without -introducing frontend classification. A stale response from a previous device or -connection generation must not create a falsely attributed record. Deduplication -across devices with the same generic family is intentional under the minimal -schema: show observation counts, not unique-device counts, and do not add device -identifiers to resolve that ambiguity. - -## App detail and registry-review follow-up - -The immediate Settings preview/export does not wait on an external reporting -service or on rewriting AppDetailSheet. That screen is reserved by `.2`. -Once `.3` details and a separate reservation are ready, **Review diagnostic -record** can open the same local preview for an Unknown item. An unresolved -process has a process-details view with no package mutation actions. - -A later **Report an app** flow may add a user-written reason and optional selected -context, but its payload/destination must be visible before an explicit send. -Keep free-form text out of the automatic minimal collector. Never silently -upload local records, infer consent from export, or mark an app reviewed because -someone reported it. - -Mechanic's `.7` workflow is event-driven: a diagnostic export or new source -evidence starts strict batch validation/deduplication, a bounded Beads correction, -source/risk/device-scope review, tested versioned registry changes, then integration -and a separate release handoff. A new record remains Unknown during triage. -Protected overrides optional/reviewed catalog entries at every step. Store review -provenance and actual status; “Under review” requires a real tracked review, and -“Available in update” requires a released version. Local evidence or an accepted -patch is not a delivered registry change. - -App purpose, effects, recovery and risk wording should come from the same reviewed -registry revision in Apps, memory details and Optimize. Registry review must -state what is known and what is device/build dependent. No separate screen-side -list or report-triggered automatic recommendation. - -## Bounded implementation acceptance and next owner - -Mechanic reserves the collector module/tests and Settings UI after the current -dispatch hold is explicitly lifted. `.1` discovery and `.2` safety/identity keep -priority; no third or overlapping worker is justified. Collector scaffolding can -remain disjoint, but hooks require the actual `.2` typed result. Navigator reviews -copy and payload consistency at the resulting milestone. -No new user implementation permission is required once Mechanic has the approved -runtime path and file reservations; the current gate is dispatch activation. - -Meaningful lightweight tests should cover: unknown installed package versus -unresolved `system`; duplicate polls/count saturation and record/byte eviction; -token/address/path rejection; absent/full/corrupt storage; immutable preview/export -snapshot; clear racing a pending save; stale TV/session results; clipboard/export -failure without false success; no sensitive fields in serialization; no changes -to safety, entitlement, session or mutations. Use synthetic identifiers, not a -real app inventory. Browser/layout and full code gates remain deferred under the -current host-use constraint and must be reported separately when actually run. - -This handoff does not itself implement the collector, the `.7` runnable batch -triage tool, external submission, or desktop UI. Track those deliverables and -their test limits explicitly rather than closing `.6` from documentation alone. diff --git a/v2/mobile/USER-FEEDBACK-2026-09-05.md b/v2/mobile/USER-FEEDBACK-2026-09-05.md deleted file mode 100644 index 950f34c5..00000000 --- a/v2/mobile/USER-FEEDBACK-2026-09-05.md +++ /dev/null @@ -1,94 +0,0 @@ -# Mobile feedback — 2026-09-05 - -> **Status, 2026-09-16: items 1-6 shipped in `d02de57`; item 7 (restart) is still open.** -> The unchecked boxes below are the original list as written, kept for traceability. Where each -> landed: #1/#2 `src/screens/Optimize.svelte` + `tests/dashboard-optional-review.test.mjs`; -> #3 `src/screens/Apps.svelte` + `tests/apps-filter.test.mjs`; #4 `src/screens/Tweaks.svelte`; -> #5 `src/screens/Onboarding.svelte`; #6 `src/screens/Backups.svelte` + `tests/backups.test.mjs`. -> **#7 remains physically unresolved** — see [`LIFECYCLE-EVIDENCE.md`](LIFECYCLE-EVIDENCE.md). - -Reported by Bryan after installing the debug build from `main` at `44d2d66` on a -Pixel 10 Pro. These are user observations and requested improvements, not verified -root causes. All acceptance items remain open; this document records TODOs and any -explicitly labeled local WIP status. It does not authorize broader implementation or -claim physical-device or released-build validation. - -## 1. Dashboard summary feels confusing and too passive - -Reported wording, approximately: “0 active, system optimized. Only 13 flow apps are -recommended.” The meaning of “flow apps” in the reported wording needs checking -against the actual screen. The overall message feels odd and does not guide the user -through useful next steps. - -- [ ] Check the exact displayed copy and what each count represents. -- [ ] Make the distinction between known recommended bloat and other optional apps - clear; zero recommended apps active should not imply there is nothing left to review. -- [ ] Provide a more hands-on next step into a guided review of installed apps. - -## 2. Optimize needs a guided optional-app review - -Optimize does not ask what the user uses or suggest optional apps they may not need. -Amazon Prime Video appeared to be running but was not suggested for removal. Bryan -wants a review of known “semi-bloated” apps, not only the existing recommended list. - -- [ ] Investigate why Prime Video was absent from recommendations on the selected TV; - distinguish running, installed, enabled, and recommended states. -- [ ] Design a guided review that asks about usage/preferences and offers optional - apps or app categories to keep or disable. -- [ ] Explain why each candidate is offered and distinguish disable from uninstall. -- [ ] Keep optional choices explicit: being installed or running alone does not make - an app unwanted. Preserve core safety gates and JSON-backed package definitions. - -## 3. Apps needs a system-app visibility filter - -- [ ] Add a clear show/hide system apps control to the Apps list. -- [ ] Make the active filter and its effect on search/results understandable. - -## 4. Match content frame rate has an odd “30fps” indicator - -- [ ] Reproduce and inspect the “30fps” text beside the tweak. -- [ ] Clarify whether it represents an actual reading, an option, or decorative copy; - correct misleading text and explain the setting's behavior. - -## 5. Previously connected TVs should open Dashboard directly - -Selecting a TV that has connected before should not require an extra “Open -dashboard” button after a successful connection. - -Local implementation note (unpublished WIP `f989bdc`; test and documentation -refinements are uncommitted): the saved-TV path now opens Dashboard only after the -current connection attempt succeeds and its device profile is confirmed. Browser-only -regression coverage in the worktree exercises success, failure, cancellation, stale -responses, and pending-versus-completed teardown. This has not been validated on a -physical TV or included in a released build, so the acceptance items remain open. - -- [ ] After a confirmed successful connection to a previously connected TV, navigate - directly to Dashboard. -- [ ] Preserve connecting, cancellation, authorization, and error handling; do not - navigate on a failed connection or a stale response from another attempt. - -## 6. Backup scope and app selection are confusing - -The wording sounds like it will back up all apps, but the action appears to update -only one. The list also includes many system-type apps the user would probably -never restore. Neither the actual action scope nor a backend defect is confirmed. - -- [ ] Reproduce the flow and align the description, button labels, selection, and - completion feedback with the actual single-app or multi-app scope. -- [ ] Clearly distinguish an APK backup from a backup of app data/settings or the - entire device; describe restore limitations. -- [ ] Make useful restore candidates easier to find and provide a clear way to hide - or reveal system apps, with appropriate restore warnings. - -## 7. Briefly leaving and reopening the app restarts the experience - -Bryan reports that briefly closing the app and reopening it restarts the whole -thing. It is not yet known whether this means background/resume, an activity or -WebView recreation, process death, or explicit dismissal from Recents. - -- [ ] Capture exact reproduction steps and lifecycle/log evidence on the Pixel. -- [ ] Investigate loss of screen/session state and unnecessary onboarding or reconnect - after a brief interruption. -- [ ] Resume the prior screen/session when valid, and recover gracefully when Android - actually killed the process or the TV connection was lost; never show a false - connected state or automatically replay mutations. diff --git a/v2/tools/emulator/README.md b/v2/tools/emulator/README.md index 6286c5ce..42c9b639 100644 --- a/v2/tools/emulator/README.md +++ b/v2/tools/emulator/README.md @@ -35,33 +35,22 @@ emulator registers with. - `dumpsys meminfo`, `screencap` - 10 catalogued packages are present, spanning `safe`/`medium`/`high` and both `disable` and `uninstall`, plus about 130 uncatalogued packages — a good surface - for the Unknown-safety work (#97) + for Unknown-safety labelling - None of this needs root ### Two limits that bite -**1. The image is not classified as a TV.** It reports -`ro.build.characteristics=emulator`, with no `tv`. Confirmed by running -`detect_device_type()` from `crates/core` against the harvested properties: - -``` -emulator as shipped -> Unknown -with `tv` characteristic -> GoogleTv -``` - -`AppListBundle::for_device` serves the `common` list only for `Unknown` -(`unknown_returns_common_only` in `engine/app_lists.rs` pins this), so the 3 -`shield` entries above are not offered and the `googletv` list is never reached. -`emulator -prop ro.build.characteristics=tv` does **not** fix it — init refuses to -overwrite a `ro.*` property the image already set, and the property still reads -`emulator` after a boot with that flag. - -The real TV signal on this image is the package-manager feature set -(`android.hardware.type.television`, `android.software.leanback`), which -`DeviceProperties` does not currently carry. Teaching detection to read it would -make the emulator classify correctly and is arguably a more honest TV signal than -a build string — but it changes the one canonical detection function, so it is a -deliberate decision, not a test-harness convenience. +**1. TV classification comes from the leanback feature, not the build string.** The image +reports `ro.build.characteristics=emulator`, with no `tv`, and +`emulator -prop ro.build.characteristics=tv` does **not** change that: init refuses to overwrite a +`ro.*` property the image already set. Its real TV signal is the package-manager feature set +(`android.hardware.type.television`, `android.software.leanback`). Detection +(`crates/core/src/engine/detection.rs`) reads `pm has-feature android.software.leanback` as well +as the characteristics, and a leanback `true` is enough to call a device a TV, so the image should +classify as a TV and get the matching app lists. That has not been re-measured on the emulator; +check the profile the app shows after `./tv-emulator.sh start`. If it comes back Unknown, +`AppListBundle::for_device` serves the `common` list only (`unknown_returns_common_only` in +`engine/app_lists.rs` pins this). **2. `adb root` is refused.** The API 34 `android-tv` image is a `user` build (`ro.debuggable=0`, `ro.build.type=user`). So adbd cannot be told to listen on @@ -96,13 +85,13 @@ since the scan will find nothing. Unlike the Playwright browser loop, the emulator renders real safe-area insets, so layout at the top and bottom edges is trustworthy here. -What this does **not** cover, and still needs the Pixel: +What this does **not** cover, and still needs a real phone: - **mDNS discovery.** QEMU user-mode networking does not carry multicast, so the scan flow finds nothing. Add the TV by IP:port instead. This is expected from how the NAT works; it was not measured here. - Real background/doze transitions — relevant to the fast-remote 30-second - background grace and to restart feedback #7 (#105). The emulator reproduces the + background grace and to the app-restart report (#105). The emulator reproduces the *JavaScript* lifecycle faithfully, which is what `evidence/lifecycle/reproduce.mjs` already models; it does not reproduce Android killing the process under real memory pressure. @@ -110,13 +99,10 @@ What this does **not** cover, and still needs the Pixel: ## What the emulator cannot settle -The three unconfirmed user reports are all vendor-firmware specific and stay on the -physical script in `docs/RELEASE-DECISION-2026-09-09.md` §4: - -- **#87 / #112** Sony XBR-55X850D launcher — Android 8 Sony firmware -- **#88 / #94** TCL QM7L Pro pairing — the TCL Android 14 pairing service -- **#89 / #92** macOS removable-volume prompts — a host-side TCC issue, nothing to - do with the device +Vendor-firmware and host-OS behavior stays on the physical script in +`docs/DEVICE-TEST-SCRIPT.md`: Sony Android 8 launcher switching, the TCL Android 14 +pairing service, and macOS removable-volume (TCC) prompts, which are a host-side issue +with nothing to do with the device. -The emulator does let you rehearse the mechanics of D5–D9 before you touch the -Shield, which is worth doing since the script has never been run. +The emulator does let you rehearse the mechanics of desktop steps D5–D9 before you +touch a Shield. From 0cd1b86492838d53c4b3b5389d2d741d7490a1eb Mon Sep 17 00:00:00 2001 From: Bryan Roscoe <bryanroscoe@gmail.com> Date: Thu, 22 Oct 2026 23:26:30 -0500 Subject: [PATCH 3/3] Point the quit-admission evidence at the archived quit-hardening doc --- v2/evidence/quit-install-admission-20261017/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/evidence/quit-install-admission-20261017/README.md b/v2/evidence/quit-install-admission-20261017/README.md index ef11da16..a586a3af 100644 --- a/v2/evidence/quit-install-admission-20261017/README.md +++ b/v2/evidence/quit-install-admission-20261017/README.md @@ -43,4 +43,4 @@ The first parallel process-suite run hit PID-startup failures in three existing Evidence is from macOS host tests. Linux/Windows CI and independent review remain gates; these logs do not establish physical shared-client, updater or Android-device acceptance. Mobile-main integration remains a separate task. -There is no visible UI change, so screenshots/video are not applicable. Historical simulated settings evidence and all six draft PR links are preserved in [ADB-QUIT-HARDENING.md](../../ADB-QUIT-HARDENING.md); those drafts and branches remain intact. +There is no visible UI change, so screenshots/video are not applicable. Historical simulated settings evidence and all six draft PR links are preserved in the archived [ADB-QUIT-HARDENING.md](https://github.com/bryanroscoe/shield_optimizer/blob/ae0436e/v2/ADB-QUIT-HARDENING.md) (the current quit rules are in [`v2/HANDOFF.md`](../../HANDOFF.md)); those drafts and branches remain intact.