# Android client — device-QA checklist (A36 / plan §7) Everything below is **device/emulator-only** — it could NOT run in the build environment (no emulator, no Firebase project, no real host). The pure logic underneath each item is JVM-unit-tested (484 tests, Kover ≥80% on the pure modules); this checklist is what a human runs on real hardware before shipping. ## Deploy artifacts to provide first (not built here) - [ ] `app/google-services.json` — the Firebase client config for FCM (`PushCoordinator` guards its absence with `runCatching`, so the app runs without it; push just won't register). - [ ] `google-services` Gradle plugin re-enabled once `google-services.json` exists (A13 deliberately omitted it — applying it without the json fails the build). - [ ] `https://terminal.yaojia.wang/.well-known/assetlinks.json` with the **release** signing-cert SHA-256 (for the verified App Link `autoVerify`). Server-side: run `A33` `src/push/fcm.ts` with the `FCM_*` env group (service-account key path + project id). ## A34 — instrumented E2E vs a real `npm start` host (write as `androidTest`, run on device) - [ ] `attach → attached → output` round-trip timing. - [ ] reconnect replays the ring buffer (F5/F6); no dropped bytes on the multi-MB replay. - [ ] spawn-failure → `exit(-1)` shows the spawn-failure banner copy. - [ ] kill via `DELETE /live-sessions/:id` (swipe-to-kill) removes the row (404 = already-gone = success). - [ ] `POST /hook/decision` resolves a held gate (from a push Allow/Deny). - [ ] **bad Origin rejected** (F9) — the one non-skippable CSWSH defence; a foreign Origin 401s the WS. ## A35 — one macrobenchmark / Espresso happy path - [ ] pair → attach → type → approve, on a real device. ## Terminal (A16/A17/A21) - [ ] glyph rendering + 24-bit true-color + cursor against a live Claude Code TUI (decide the §6.8 WebView fallback ONLY if fidelity diverges — not expected). - [ ] IME/CJK composition; text selection → `ActionMode` → clipboard; http/https link tap. - [ ] **real font-metric → grid resize** (`TerminalRenderer.mFontWidth/mFontLineSpacing` are package-private → measured on-device, fed to the JVM-tested `TerminalGridMath`); resize parity vs web/iOS on the same device sizes (R5). - [ ] **config-change (rotation/fold) re-binds the surviving emulator** with no blank + no replay round-trip (scrollback CONTENT survives; scroll OFFSET resets — accepted); real-background → generation bump → fresh emulator replays. - [ ] key-bar above the IME (soft keyboard never pops on a key-bar tap); hardware chords; DECCKM arrows emit `ESC O A` under app-cursor mode (vim/htop). - [ ] `FLAG_SECURE` + privacy cover blanks the recents thumbnail on `ON_STOP`. - [ ] off-main chunked append does not ANR on a multi-MB replay. ## Push (A30/A31 · R1/S2) - [ ] **S2 spike:** data-only high-priority delivery latency/loss on ≥2 real handsets (incl. one Xiaomi/Huawei/Samsung) under Doze / OEM battery managers / force-stop — quantify loss, drive the "delivery not guaranteed while backgrounded" user-facing copy. - [ ] **Deny** from the lock screen (BroadcastReceiver, no app open, expedited POST). - [ ] **Allow** → translucent `excludeFromRecents` trampoline hosts `BiometricPrompt` → POST on auth success; cancel/error/no-enrolled-auth → no POST (fail-safe). - [ ] single-use token: a retried-after-success decision POST returns 403 (idempotency). - [ ] `POST_NOTIFICATIONS` runtime prompt (API 33+); token registers to every paired host + self-heals on rotation / new host / removal. ## Pairing / cert / storage (A19/A27/A11/A12) - [ ] CameraX QR scan + ML-Kit decode; CAMERA-denied → manual URL entry. - [ ] §5.4 warning tiers on real hosts; tunnel host cert-gate refuses without a device cert (retry can't bypass); public host explicit-ack. - [ ] SAF `.p12` import → **real AndroidKeyStore** (non-exportable) + Tink AEAD; bad passphrase keeps the prior identity (validate-before-persist); rotate presents the new cert on the **next handshake with no relaunch** (re-reading `X509KeyManager` + `connectionPool.evictAll()`); remove. - [ ] DataStore host list / `LastSessionStore` set-on-adopted / clear-on-exited. ## Nav / deep links / adaptive (A32/A26/A29) - [ ] cold-start AND warm deep link `webterminal://open?host=&join=` + the verified App Link route to the right destination; invalid UUID ignored. - [ ] cold-start route (no host → pairing, else sessions); continue-last-session banner re-opens. - [ ] adaptive: compact = stack, expanded/tablet = list+detail (`NavigationSuiteScaffold` + `ListDetailPaneScaffold`); pointer secondary-click context menu on a tablet (sw≥600). ## Known minor gaps (tracked, non-blocking — see PROGRESS_ANDROID.md) - [ ] push body-tap opens the app (not yet the specific gate — the notification `openAppIntent` doesn't carry the sessionId; the gate is still visible in the terminal). MEDIUM. - [ ] no "view diff" affordance from ProjectDetail → the `DiffScreen` nav destination has no inbound link. - [ ] no host-remove UI action (so `PushRegistrar.unregisterHost` isn't invoked). - [ ] A21 hosts the Termux `TerminalView` via a reflection getter (works; `com.termux.view.TerminalView` is `implementation`-scoped in `:terminal-view`) — clean fix = add `libs.termux.terminal.view` to `:app` and delete the shim.