Some checks failed
relay-tripwire / cross-tenant-tripwire (push) Has been cancelled
ios / package-tests (APIClient) (push) Has been cancelled
ios / package-tests (HostRegistry) (push) Has been cancelled
ios / package-tests (SessionCore) (push) Has been cancelled
ios / package-tests (WireProtocol) (push) Has been cancelled
ios / testsupport-tests (push) Has been cancelled
ios / app-tests (push) Has been cancelled
ios / ipad-tests (push) Has been cancelled
ios / integration-tests (push) Has been cancelled
ios / ui-test (push) Has been cancelled
ios / ios17-floor-tests (push) Has been cancelled
Mirror the iOS P0+P1 client as a Gradle multi-module app. Pure-JVM (Kover ≥80% gated): :wire-protocol (frozen contract + HostEndpoint CSWSH origin + byte-exact codec), :session-core (SessionEngine + reconnect/ping/gate/digest reducers), :api-client, :client-tls (pure PKCS12/keymanager), :transport-okhttp (OkHttp WS+REST). Framework: :app (Compose M3 Adaptive, Hilt, FCM, 11 screens + NavGraph), :terminal-view (Termux terminal-emulator/-view via JitPack, Apache-2.0 — the renderer seam proven headless), :host-registry (DataStore), :client-tls-android (AndroidKeyStore + Tink). Highlights: single-key-home mTLS (non-exportable AndroidKeyStore key + re-reading X509KeyManager + connectionPool.evictAll on rotation, ping-pong single-commit so a failed rotation never clobbers the prior identity); FCM Allow/Deny trust split (Deny=BroadcastReceiver, Allow=trampoline Activity hosting BiometricPrompt); per-consumer-Channel EventBus (R10); config-surviving RetainedSessionHolder; byte-exact KeyByteMap; §5.4 pairing warning tiers. Verified: ~484 JVM tests + Kover ≥80% on the pure modules + :app assembles to an APK; all framework modules assemble. Device behaviors (rendering/IME/FCM/biometric/camera, E2E A34/A35, S2 real-handset FCM spike) deferred to android/DEVICE_QA_CHECKLIST.md per plan §7 (no emulator/Firebase here). Built via multi-agent orchestration (TDD builders → adversarial cross-review → fix → re-verify → independent gate); progress in android/PROGRESS_ANDROID.md.
77 lines
5.4 KiB
Markdown
77 lines
5.4 KiB
Markdown
# 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.
|