mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-11 03:13:40 +00:00
chore(plans): consolidate all plans at repo root
Plans were split across three trees: plans/, web/plans/, and android/plans/. Merge them into plans/ at the repo root. Sweep the shipped Android experience pass and the two reports for it, carrying the residual forward into plans/todo.md: the device QA checklist (open only because this environment has no device or emulator), the clipped web PWA icons defect, and the untested native assumptions. Merge android/plans/todo.md in the same pass, dropping items the experience pass already delivered -- haptics, back button, safe-area insets, launcher icon, and splash -- each verified against MainActivity.java, web/src/app.css, and the res/ tree. Correct two stale claims while merging: web/ is a plain directory in this monorepo, not a git submodule, and the serialize-javascript override is held by @rollup/plugin-terser rather than workbox-build. The retired native Kotlin/Compose port plan moves as-is; it is superseded by the Capacitor wrapper but not yet swept.
This commit is contained in:
1 parent
1c1bab8733
commit
0691665b3a
20 files changed
-889
No files matched your search
@@ -1,120 +0,0 @@
|
||||
# Lô tô Android — TODO
|
||||
|
||||
## Direction change (2026-05-10)
|
||||
|
||||
Repo migrated from **native Kotlin/Compose port** to a **Capacitor wrapper**
|
||||
around the upstream `tiennm99/loto` SvelteKit PWA. Native code lives in git
|
||||
history at commits `9a35686` and `fe52232` — recoverable but no longer the
|
||||
ship target.
|
||||
|
||||
Reason: maintenance parity. Loto evolves; the wrapper rebuilds and ships
|
||||
without re-porting features.
|
||||
|
||||
## Done in 2026-05-10 session
|
||||
|
||||
- [x] Migrate to Capacitor wrapper, push to main (commit `dab4f7e`)
|
||||
- [x] CI green: Node 22 + JDK 21 (commits `95358f2`, `4fde3f1`) — debug APK 4.9 MB
|
||||
- [x] BlueStacks/emulator compat: `<uses-feature required=false>`, WebView debug auto-enabled in debug builds (commit `1ec4863`)
|
||||
- [x] Optional Play Store auto-publish wired into `release.yml`, gated on `PLAY_SERVICE_ACCOUNT_JSON` (commit `793f009`)
|
||||
|
||||
## P0 — Smoke test on real device
|
||||
|
||||
- [x] CI build-debug workflow assembles APK (validated by run `25610204367`)
|
||||
- [ ] Sideload the latest artifact onto a real device (API 24, API 31, API 36) and BlueStacks
|
||||
- [ ] Smoke test cold start: app loads `https://localhost/`, no white flash > 1s
|
||||
- [ ] Smoke test offline: enable airplane mode, kill app, relaunch — must work
|
||||
- [ ] Smoke test audio: tap any number → Vietnamese voice plays
|
||||
- [ ] Verify both voices switch correctly (`hoai-my` ↔ `nam-minh`)
|
||||
|
||||
## P0 — Wrapper-specific concerns
|
||||
|
||||
- [ ] First-launch SW registration: confirm Workbox precaches the bundled audio
|
||||
(should be idempotent since assets already on-disk, but verify no errors)
|
||||
- [ ] Back button behavior: Capacitor default closes app — decide whether to
|
||||
add `App.addListener('backButton', …)` for in-app history navigation
|
||||
- [ ] Safe-area / status-bar inset: SvelteKit UI may render under the status bar.
|
||||
Check; add `<StatusBar>` plugin if needed
|
||||
- [ ] Audio focus: WebView `<audio>` doesn't claim AUDIOFOCUS_GAIN. Test what
|
||||
happens when a phone call comes in mid-call (does playback duck/pause?)
|
||||
- [ ] Verify `android:supportsRtl="true"` (Capacitor default) doesn't flip the
|
||||
Vietnamese UI
|
||||
|
||||
## P1 — Polish before public release
|
||||
|
||||
- [ ] Replace placeholder launcher icon (Capacitor ships a generic Material icon).
|
||||
Use loto's existing `static/icons/` rasters; consider `@capacitor/assets`
|
||||
to generate adaptive + splash from a single source
|
||||
- [ ] Splash screen: `@capacitor/splash-screen` plugin with brand colors
|
||||
- [ ] Status bar styling: brand-colored, light/dark text via `@capacitor/status-bar`
|
||||
- [ ] App-store screenshots (3–5 phone shots): cold start, mid-game, master panel,
|
||||
settings, bingo modal
|
||||
|
||||
## P1 — Distribution: signing + first GitHub Release
|
||||
|
||||
- [ ] Generate release keystore: `keytool -genkey -v -keystore ~/.android/loto-release.jks -keyalg RSA -keysize 4096 -validity 36500 -alias loto`
|
||||
- [ ] Add GitHub secrets: `KEYSTORE_BASE64` (= `base64 -w0 loto-release.jks`), `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD`
|
||||
- [ ] Tag `v1.0.0` and push → `release.yml` builds signed AAB+APK, attaches to GH Release
|
||||
(Play Store step skips silently because `PLAY_SERVICE_ACCOUNT_JSON` not yet set)
|
||||
|
||||
## P1 — Google Play Store (ordered, do in sequence)
|
||||
|
||||
**Phase A — one-time Play Console setup (cannot be automated, ~$25 + 1–7 day review):**
|
||||
|
||||
- [ ] Sign up: https://play.google.com/console/signup ($25 one-time fee)
|
||||
- [ ] Create app entry with package name `com.miti99.loto` (must match `applicationId`)
|
||||
- [ ] Prepare store assets:
|
||||
- [ ] Icon 512×512 PNG
|
||||
- [ ] Feature graphic 1024×500 PNG/JPG
|
||||
- [ ] 2–8 phone screenshots (master panel, player board, settings, bingo modal, cold start)
|
||||
- [ ] Short description ≤ 80 chars
|
||||
- [ ] Full description ≤ 4000 chars (Vietnamese + optional English)
|
||||
- [ ] Privacy policy URL — host on GH Pages even though app is offline (required by Play)
|
||||
- [ ] Fill content rating questionnaire (Casino/Card → likely Everyone or Teen)
|
||||
- [ ] Fill target audience + data safety form (declare "No data collected" — fully offline)
|
||||
- [ ] **Manually upload** the signed AAB from the v1.0.0 GH Release to **Internal Testing track**
|
||||
(Google policy: first upload must be manual; CI auto-publish only works for v1.0.1+)
|
||||
- [ ] Submit for review (1–7 days for first app)
|
||||
|
||||
**Phase B — enable CI auto-publish (after Phase A approved):**
|
||||
|
||||
- [ ] Play Console → Setup → API access → link/create Google Cloud project
|
||||
- [ ] In GCP Console: create Service Account → Keys → Add Key → JSON → download
|
||||
- [ ] Back in Play Console API access: grant the service account *Release manager* + *View app information*
|
||||
(or *Admin* for simplicity)
|
||||
- [ ] Add full JSON content as GH secret `PLAY_SERVICE_ACCOUNT_JSON`
|
||||
- [ ] **Bump `versionCode` to 2** in `android/app/build.gradle` before next tag
|
||||
(Play rejects duplicate versionCodes — every release must increment)
|
||||
- [ ] Tag `v1.0.1` and push → CI builds + uploads to **Internal track** automatically
|
||||
- [ ] Promote internal → closed → open → production via Play Console UI when ready
|
||||
(or change `track: internal` in `release.yml` to `production` to fully automate)
|
||||
|
||||
**Open question: versionCode footgun** — every release must increment `versionCode` in
|
||||
`android/app/build.gradle` before tagging. Options to consider:
|
||||
- Manual discipline (current state)
|
||||
- Pre-tag check workflow that fails if `versionCode` wasn't bumped
|
||||
- Auto-bump from the git tag (e.g., `v1.0.5` → `versionCode = 5`) as a Gradle plugin or sed step in CI
|
||||
|
||||
## P2 — Supply-chain hygiene
|
||||
|
||||
- [ ] `npm audit` — wrapper repo (currently 0 vuln)
|
||||
- [ ] `npm audit` — loto submodule (currently 2 high — bump and re-pin)
|
||||
- [ ] Dependabot / Renovate for both `package.json` and the loto submodule SHA
|
||||
|
||||
## P2 — Future features (not committed)
|
||||
|
||||
- [ ] Add `@capacitor/share` for sharing the called-numbers card
|
||||
- [ ] Add `@capacitor/haptics` for bingo confirmation
|
||||
- [ ] Background audio (play numbers while screen off) — WebView audio is
|
||||
foreground-only; would need a native plugin or service shim
|
||||
- [ ] iOS target (`npx cap add ios`) — same wrapper, ~zero extra code
|
||||
|
||||
## Decisions record
|
||||
|
||||
- **Architecture**: Capacitor 8 wrapper, not TWA — chosen for first-launch
|
||||
offline guarantee (TWA needs network for first PWA fetch)
|
||||
- **Loto integration**: git submodule pinned to a SHA, not npm package — loto
|
||||
isn't published; submodule keeps the wrapper reproducible
|
||||
- **No INTERNET permission**: enforces "fully offline" as a hard guarantee.
|
||||
Add back if any remote feature is introduced
|
||||
- **Native port retired**: lives at commits `fe52232`/`9a35686` if someone
|
||||
ever wants the ExoPlayer + Compose UI back
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,137 +0,0 @@
|
||||
# Phase 1 — Tier 1 defects
|
||||
|
||||
APK-only bugs. None reproduce in a desktop browser.
|
||||
|
||||
## W1 · VIBRATE permission
|
||||
|
||||
`android/android/app/src/main/AndroidManifest.xml` — add:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
```
|
||||
|
||||
Normal permission: no runtime prompt, no Play data-safety change. Caller is
|
||||
`PlayerBoard.svelte:341-346`, already guarded by `prefers-reduced-motion`.
|
||||
|
||||
## W2 · Screen wake lock
|
||||
|
||||
New `web/src/lib/wake-lock.js` (plain `.js` — no reactive state, matches
|
||||
`game-logic.js` / `player-auto-cross.js` convention; runes files are only those
|
||||
holding `$state`).
|
||||
|
||||
API — single entry point so callers cannot desync:
|
||||
|
||||
```js
|
||||
export function setWakeLock(on) // idempotent
|
||||
export function _resetForTest() // test-only teardown
|
||||
```
|
||||
|
||||
Behaviour:
|
||||
|
||||
- `setWakeLock(true)` → `navigator.wakeLock.request("screen")`, installs a
|
||||
`visibilitychange` listener that re-acquires on return to visible.
|
||||
Android silently releases the lock whenever the page hides — without the
|
||||
re-acquire the lock is dead after the first backgrounding.
|
||||
- `setWakeLock(false)` → releases sentinel, removes listener.
|
||||
- Generation token guards the async gap: a `request()` that resolves after the
|
||||
caller flipped to `false` must release immediately rather than leak a lock.
|
||||
Same pattern as `activeToken` in `voice.js`.
|
||||
- No `navigator.wakeLock` → no-op (old WebViews at minSdk 24).
|
||||
- `request()` rejects on battery saver / hidden page → swallow, do not throw.
|
||||
|
||||
Consumer — `web/src/lib/MasterPanel.svelte`, new `$effect`:
|
||||
|
||||
```js
|
||||
$effect(() => {
|
||||
setWakeLock(autoRunning || hasGame);
|
||||
return () => setWakeLock(false);
|
||||
});
|
||||
```
|
||||
|
||||
`autoRunning` (line 47) and `hasGame` (lines 55-57) already exist locally.
|
||||
MasterPanel only mounts in master/both mode, so player-only mode never holds a
|
||||
lock — intended: player taps keep their own screen awake.
|
||||
|
||||
Tests — new `web/src/lib/wake-lock.test.js`: acquire, release, re-acquire on
|
||||
`visibilitychange`, no-op when unsupported, late-resolve after `false` releases.
|
||||
|
||||
## W3 · Back button
|
||||
|
||||
### Web half — `web/src/lib/overlay-history.js` (new)
|
||||
|
||||
Each open overlay pushes one sentinel history entry. `popstate` closes the
|
||||
newest. Works in the browser too — browser back closes the modal, a real PWA win.
|
||||
|
||||
```js
|
||||
export function pushOverlay(close) // returns dispose() for programmatic close
|
||||
```
|
||||
|
||||
- LIFO stack of `{ id, close }`.
|
||||
- `popstate` → pop top, run its `close()`, leave history alone.
|
||||
- `dispose()` (Escape / Xong / backdrop) → remove entry, then `history.back()`
|
||||
to drop the sentinel, with a suppression counter so the resulting `popstate`
|
||||
does not re-run `close()`.
|
||||
- Single shared `popstate` listener, installed on first push, removed when the
|
||||
stack empties.
|
||||
|
||||
Consumers:
|
||||
|
||||
- `PlayerBoard.svelte` — `showCongrats` modal.
|
||||
- `SettingsButton.svelte` — settings sheet.
|
||||
|
||||
Both already have Escape handlers in an `$effect`; the overlay push belongs in
|
||||
the same effect so open/close stays one code path.
|
||||
|
||||
Tests — new `web/src/lib/overlay-history.test.js`: single open/close, nested
|
||||
LIFO ordering, programmatic close does not double-fire, stack drains.
|
||||
|
||||
### Native half — `MainActivity.java`
|
||||
|
||||
```java
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override public void handleOnBackPressed() {
|
||||
WebView wv = getBridge().getWebView();
|
||||
if (wv != null && wv.canGoBack()) { wv.goBack(); return; } // pops a sentinel
|
||||
confirmExit();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`history.pushState` adds to the WebView back-forward list, so `canGoBack()` is
|
||||
true exactly while an overlay is open, and `goBack()` fires `popstate`.
|
||||
|
||||
Exit confirm: `androidx.appcompat.app.AlertDialog`, strings from
|
||||
`strings.xml` (`exit_title`, `exit_message`, `exit_confirm`, `exit_cancel`),
|
||||
positive button → `finish()`.
|
||||
|
||||
Also add `android:enableOnBackInvokedCallback="true"` to `<application>`.
|
||||
Default at targetSdk 36; explicit is self-documenting.
|
||||
|
||||
**Unconfirmed, device-only:** whether the activity theme at dialog time is
|
||||
AppCompat-derived. Activity theme is `AppTheme.NoActionBarLaunch`
|
||||
(parent `Theme.SplashScreen`); Capacitor's splash flow normally swaps to
|
||||
`AppTheme.NoActionBar` post-splash. If the dialog throws on a non-AppCompat
|
||||
theme, pass an explicit dialog theme to the builder. Flagged in QA checklist.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `android/android/app/src/main/AndroidManifest.xml` | VIBRATE, enableOnBackInvokedCallback |
|
||||
| `android/android/app/src/main/java/com/miti99/loto/MainActivity.java` | back callback + confirm dialog |
|
||||
| `android/android/app/src/main/res/values/strings.xml` | 4 exit-dialog strings |
|
||||
| `web/src/lib/wake-lock.js` | new |
|
||||
| `web/src/lib/wake-lock.test.js` | new |
|
||||
| `web/src/lib/overlay-history.js` | new |
|
||||
| `web/src/lib/overlay-history.test.js` | new |
|
||||
| `web/src/lib/MasterPanel.svelte` | wake-lock effect |
|
||||
| `web/src/lib/PlayerBoard.svelte` | overlay push for bingo modal |
|
||||
| `web/src/lib/SettingsButton.svelte` | overlay push for settings sheet |
|
||||
|
||||
## Validation
|
||||
|
||||
- `pnpm test` — new suites green, existing 6 suites unaffected.
|
||||
- `pnpm lint`, `pnpm build`.
|
||||
- Device QA: tap buzzes; reduced-motion silent; 3-min auto-call, screen stays
|
||||
lit; background/return keeps it lit; back closes modal; back at root confirms;
|
||||
confirm dialog renders (theme check).
|
||||
@@ -1,132 +0,0 @@
|
||||
# Phase 2 — Tier 2, designed defensively
|
||||
|
||||
Suspected but unconfirmed without a device. Every change here is correct whether
|
||||
or not the bug reproduces, and costs nothing if it does not.
|
||||
|
||||
## W4 · Safe-area insets
|
||||
|
||||
`web/src/app.html` — viewport meta gains `viewport-fit=cover`:
|
||||
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
```
|
||||
|
||||
`web/src/app.css` — pad `body` by the insets:
|
||||
|
||||
```css
|
||||
body {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
```
|
||||
|
||||
Why `body` and not the page container: `html` carries the background colour and
|
||||
`body` is already `transparent` + `min-h-full flex flex-col`, so padding here
|
||||
insets content while the background stays full-bleed. `fixed inset-0` overlays
|
||||
(bingo modal, settings sheet, inactive-tab curtain) are viewport-relative and
|
||||
deliberately unaffected — their backdrops should cover the whole screen; their
|
||||
content is centred and already clear of the bars.
|
||||
|
||||
Bonus: fixes the iOS PWA notch for free.
|
||||
|
||||
Zero effect where insets are 0 (desktop, older Android).
|
||||
|
||||
## W5 · Font scale
|
||||
|
||||
### Native pin
|
||||
|
||||
`MainActivity.java`, after `super.onCreate`:
|
||||
|
||||
```java
|
||||
getBridge().getWebView().getSettings().setTextZoom(100);
|
||||
```
|
||||
|
||||
Stops the system font multiplier from breaking a 9-column fixed grid.
|
||||
|
||||
### In-app replacement control
|
||||
|
||||
Taking the system control away obliges giving one back. Both ship or neither.
|
||||
|
||||
`web/src/lib/settings-store.svelte.js`:
|
||||
|
||||
- `boardTextScale: 1` in `DEFAULT_SETTINGS`.
|
||||
- `validBoardTextScale(v)` — allowlist `[0.9, 1, 1.15, 1.3]`, same
|
||||
per-key-validator pattern as the existing keys.
|
||||
- `applyBoardTextScale()` sets `--board-text-scale` on `documentElement`,
|
||||
mirroring `applyEmptyCellColor()`; called from `applyAll()`.
|
||||
- Wire into `loadSettings()` and `resetSettings()`.
|
||||
|
||||
`web/src/app.css` — new classes. `.tan-tan-num` sets font-family/weight only;
|
||||
sizes come from Tailwind utilities on the elements, so overriding there would be
|
||||
a specificity fight. Dedicated classes instead, replacing the utilities:
|
||||
|
||||
```css
|
||||
/* Player card cell — was text-xl sm:text-2xl md:text-3xl */
|
||||
.board-num { font-size: calc(1.25rem * var(--board-text-scale, 1)); }
|
||||
@media (min-width: 640px) { .board-num { font-size: calc(1.5rem * var(--board-text-scale, 1)); } }
|
||||
@media (min-width: 768px) { .board-num { font-size: calc(1.875rem * var(--board-text-scale, 1)); } }
|
||||
|
||||
/* Master tracking token — was text-xl sm:text-2xl */
|
||||
.master-num { font-size: calc(1.25rem * var(--board-text-scale, 1)); }
|
||||
@media (min-width: 640px) { .master-num { font-size: calc(1.5rem * var(--board-text-scale, 1)); } }
|
||||
```
|
||||
|
||||
Two classes, not one: the master board has two breakpoint rungs, the player card
|
||||
three. Merging them would silently enlarge the master board on desktop.
|
||||
|
||||
Consumers:
|
||||
|
||||
- `PlayerBoard.svelte:451-462` — swap `text-xl sm:text-2xl md:text-3xl` → `board-num`.
|
||||
- `MasterPanel.svelte:287` — swap `text-xl sm:text-2xl` → `master-num`.
|
||||
|
||||
Scope: grid cells only. Master hero number, called-history chips, and all UI
|
||||
chrome keep their Tailwind sizes — clipping is a grid problem.
|
||||
|
||||
### Settings UI
|
||||
|
||||
`web/src/lib/SettingsButton.svelte` — new fieldset "Cỡ chữ bảng", 4 buttons
|
||||
(Nhỏ / Vừa / Lớn / Rất lớn), same `aria-pressed` button-group pattern as the
|
||||
existing theme/mode pickers.
|
||||
|
||||
Tests — extend `web/src/lib/settings-store.test.js`: default, valid values
|
||||
persist, invalid/out-of-set falls back to 1, reset restores 1.
|
||||
|
||||
## W6 · Volume rocker → media stream
|
||||
|
||||
`MainActivity.java`:
|
||||
|
||||
```java
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
```
|
||||
|
||||
One line. Volume keys adjust media rather than ringtone even before first
|
||||
playback. Unambiguously right for an app whose job is calling numbers aloud.
|
||||
|
||||
## Deferred — audio focus / MediaSession
|
||||
|
||||
Still unconfirmed as broken. Building a MediaSession layer around 1-second clips
|
||||
for a hypothetical problem is speculative work. Revisit with device evidence.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `web/src/app.html` | `viewport-fit=cover` |
|
||||
| `web/src/app.css` | safe-area body padding, `.board-num`, `.master-num` |
|
||||
| `web/src/lib/settings-store.svelte.js` | `boardTextScale` + validator + apply |
|
||||
| `web/src/lib/settings-store.test.js` | coverage for the new key |
|
||||
| `web/src/lib/SettingsButton.svelte` | size picker fieldset |
|
||||
| `web/src/lib/PlayerBoard.svelte` | `board-num` class swap |
|
||||
| `web/src/lib/MasterPanel.svelte` | `master-num` class swap |
|
||||
| `android/.../MainActivity.java` | textZoom pin, volume stream |
|
||||
|
||||
## Validation
|
||||
|
||||
- `pnpm test` — settings-store suite covers the new key; existing 369 lines of
|
||||
settings tests must stay green (this file has the most regression surface).
|
||||
- `pnpm lint`, `pnpm build`.
|
||||
- Device QA: system font at 200% → grid intact; in-app size control changes
|
||||
numbers and survives reload; gesture-nav and status bar clear of gear/footer;
|
||||
volume rocker shows the media slider during a call.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Phase 3 — Store polish
|
||||
|
||||
Independent of phases 1-2. Pure config/resource work.
|
||||
|
||||
## Launcher name
|
||||
|
||||
`android/android/app/src/main/res/values/strings.xml`:
|
||||
|
||||
- `app_name`: `Lo To` → `Lô tô`
|
||||
- `title_activity_main`: `Lo To` → `Lô tô`
|
||||
|
||||
Vietnamese users currently get an ASCII-mangled launcher label while the app
|
||||
itself is "Lô tô — Hội chợ TN1". Short form matches the webmanifest
|
||||
`short_name`. AAPT handles UTF-8; the file already declares
|
||||
`encoding='utf-8'`.
|
||||
|
||||
Leave `package_name` and `custom_url_scheme` alone — identifiers, not labels.
|
||||
|
||||
## Themed icon (Android 13+)
|
||||
|
||||
`android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml` — add a
|
||||
`<monochrome>` layer so the launcher can tint the icon to the user's theme.
|
||||
Without it Android 13+ falls back to the full-colour icon on themed home
|
||||
screens.
|
||||
|
||||
Source glyph: `web/static/icons/source.svg`. Monochrome layers must be a flat
|
||||
silhouette on transparent — no gradients, no background plate. Add as
|
||||
`res/drawable/ic_launcher_monochrome.xml` (vector) reusing the existing
|
||||
foreground geometry.
|
||||
|
||||
## Dark splash
|
||||
|
||||
New `android/android/app/src/main/res/values-night/styles.xml` overriding
|
||||
`AppTheme.NoActionBarLaunch` with a dark splash drawable. The app has a full
|
||||
dark theme; the splash currently flashes light before the WebView paints.
|
||||
|
||||
Needs a `drawable-night/splash.xml` (or a night colour the existing
|
||||
`@drawable/splash` references).
|
||||
|
||||
## Version bump
|
||||
|
||||
`android/android/app/build.gradle`:
|
||||
|
||||
- `versionCode 3` → `4` (Play rejects duplicates — already documented in
|
||||
`android/README.md`).
|
||||
- `versionName "0.0.3"` → `"0.1.0"`. Still internal track; 1.0.0 would
|
||||
overclaim.
|
||||
|
||||
## Docs
|
||||
|
||||
`android/README.md`:
|
||||
|
||||
- Permissions: document VIBRATE and why (haptic cell feedback).
|
||||
- New "Back button" note: closes overlays, confirms exit at root.
|
||||
- New "Screen wake lock" note: held while a round is active.
|
||||
- Version-bump section: reflect the new numbers.
|
||||
- **Leave the "Why no INTERNET permission?" section verbatim.** The offline
|
||||
guarantee is unchanged and the wording is deliberate.
|
||||
|
||||
## Reassessed, no change
|
||||
|
||||
`allowBackup=true` was listed as a gap in the brainstorm. Stored data is grid,
|
||||
crossed cells, and UI settings in localStorage — nothing sensitive, and backup
|
||||
means a player restores their card on a new phone. Correct as-is.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `android/.../res/values/strings.xml` | diacritic app name |
|
||||
| `android/.../res/mipmap-anydpi-v26/ic_launcher.xml` | monochrome layer |
|
||||
| `android/.../res/drawable/ic_launcher_monochrome.xml` | new |
|
||||
| `android/.../res/values-night/styles.xml` | new, dark splash |
|
||||
| `android/.../res/drawable-night/splash.xml` | new |
|
||||
| `android/android/app/build.gradle` | versionCode 4, versionName 0.1.0 |
|
||||
| `android/README.md` | permissions, back button, wake lock, versions |
|
||||
|
||||
## Validation
|
||||
|
||||
- No test surface — resources and docs only.
|
||||
- Device QA: launcher shows `Lô tô`; themed-icon home screen tints correctly;
|
||||
cold start in dark mode does not flash light.
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
status: complete
|
||||
created: 2026-08-14
|
||||
completed: 2026-08-14
|
||||
branch: main
|
||||
source: plans/reports/brainstorm-android-ux-260814-1004-android-experience-pass-report.md
|
||||
report: plans/reports/from-cook-to-review-260814-1317-android-experience-pass-implementation-report.md
|
||||
---
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
1. **Icon scope grew.** Discovered mid-plan that the APK ships Capacitor's
|
||||
stock logo as launcher icon *and* splash. User approved regenerating from
|
||||
`source.svg`. Supersedes the originally-planned monochrome-layer and
|
||||
dark-splash items, which would have themed the wrong logo.
|
||||
2. **`source.svg` is broken.** `font-size="240"` overflows the 512 canvas;
|
||||
`web/static/icons/*.png` are visibly clipped. Android art rendered at a
|
||||
corrected 200/160. Web PWA icons still carry the bug — not fixed, out of
|
||||
approved scope.
|
||||
3. **Wake-lock condition tightened** from `autoRunning || hasGame` to
|
||||
`masterState.remaining.length > 0`. `hasGame` never returns to false once a
|
||||
round starts, which would have pinned the screen on for a finished board.
|
||||
4. **Splash rebuilt rather than duplicated.** 11 density PNGs replaced by one
|
||||
layer-list + night variant + `windowSplashScreen*` for API 31+.
|
||||
|
||||
# Android experience pass
|
||||
|
||||
Close Android-only behaviour gaps in the Capacitor wrapper. Offline guarantee
|
||||
(no INTERNET permission) untouched.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | File | Depends on |
|
||||
|---|-------|------|------------|
|
||||
| 1 | Tier 1 defects | [phase-01-tier1-defects.md](phase-01-tier1-defects.md) | — |
|
||||
| 2 | Tier 2 defensive | [phase-02-tier2-defensive.md](phase-02-tier2-defensive.md) | 1 (shares MainActivity) |
|
||||
| 3 | Store polish | [phase-03-store-polish.md](phase-03-store-polish.md) | — |
|
||||
|
||||
## Resolved before planning
|
||||
|
||||
Brainstorm left W3 (back button) as a fork. Resolved by research:
|
||||
|
||||
- Android 16 / API 36 no longer calls `onBackPressed()` and no longer dispatches
|
||||
`KEYCODE_BACK`. Project targets 36.
|
||||
- `OnBackPressedCallback` is the forward-compatible mechanism and keeps working
|
||||
under predictive back.
|
||||
- Capacitor 8 `BridgeActivity`/`Bridge` show no back handling on `main`, so
|
||||
Capacitor's default "back navigates WebView history" cannot be assumed to
|
||||
survive targetSdk 36 either.
|
||||
|
||||
Consequence: back handling is implemented natively via `OnBackPressedCallback`.
|
||||
This is not a fallback — it is the only mechanism that can be relied on. Kills
|
||||
brainstorm risks 1 and 3 (`web/` never takes a Capacitor dependency).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Cell tap vibrates on device; `prefers-reduced-motion` still suppresses.
|
||||
2. Auto-call runs 3+ min untouched without screen dimming; stop releases lock;
|
||||
background → foreground re-acquires.
|
||||
3. Back closes an open overlay (bingo modal, settings sheet) instead of quitting;
|
||||
back at root shows a Vietnamese exit confirm.
|
||||
4. Player grid legible at 200% system font; in-app board-size control resizes and
|
||||
persists.
|
||||
5. Status bar / gesture nav do not overlap the settings gear or footer.
|
||||
6. Volume rocker controls media stream during a call.
|
||||
7. Launcher shows `Lô tô` with diacritics.
|
||||
8. `pnpm test` green, `pnpm lint` clean, `pnpm build` succeeds.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Multi-device sync, INTERNET permission, audio focus/MediaSession, APK size,
|
||||
`allowBackup` changes, service-worker asset dedup.
|
||||
|
||||
## Verification limits
|
||||
|
||||
No Android device, emulator, or browser in this environment (headless ARM64, no
|
||||
Chrome — see workspace CLAUDE.md). Gradle/Android SDK not installed. So:
|
||||
|
||||
- Verifiable here: unit tests, lint, web build, static review of native code.
|
||||
- Device-only: items 1, 2 (real screen), 3, 4 (system font), 5, 6.
|
||||
|
||||
Phase files carry a manual QA checklist for the device-only items.
|
||||
File renamed without changes.
@@ -1,205 +0,0 @@
|
||||
# Brainstorm — Android experience pass
|
||||
|
||||
- Date: 2026-08-14
|
||||
- Branch: `main` (c6c839f)
|
||||
- Mode: brainstorm (no `--html`, no `--wiki`)
|
||||
- Status: design approved, ready for `/ck:plan`
|
||||
|
||||
## Problem statement
|
||||
|
||||
App ships two targets from one commit: static web (`web/`) and a Capacitor 8 APK
|
||||
(`android/`). Wrapper is thin — bare `BridgeActivity`, zero plugins, no native code.
|
||||
Consequence: several behaviours diverge between browser and APK, and none of the
|
||||
divergences reproduce on desktop where development happens.
|
||||
|
||||
Goal: close the Android-only gaps without breaking the offline guarantee.
|
||||
|
||||
## Locked constraints
|
||||
|
||||
| Constraint | Decision |
|
||||
|---|---|
|
||||
| INTERNET permission | Never. Non-negotiable. Offline is a hard guarantee, not a convention. |
|
||||
| Implementation bias | Web-first. Native only where the web platform cannot reach. |
|
||||
| Multi-device sync | Out of scope this round. |
|
||||
| Play data safety | Must stay "no data collected". |
|
||||
|
||||
## Findings (verified against source)
|
||||
|
||||
### Tier 1 — defects that exist only in the APK
|
||||
|
||||
1. **Haptics dead.** `PlayerBoard.svelte:341-346` calls `navigator.vibrate(10)`.
|
||||
`AndroidManifest.xml` declares no `android.permission.VIBRATE`. Works in Chrome
|
||||
(Chrome holds the permission), silent no-op in the WebView.
|
||||
2. **Screen sleeps mid-round.** Auto-call is `setInterval` at 1–10 s/number
|
||||
(`MasterPanel.svelte:85-104`); a 90-number round is 7.5–15 min. Zero wake-lock
|
||||
usage anywhere (grepped). Caller device dims, timers throttle, calls stop.
|
||||
Worst real-world failure; invisible on desktop.
|
||||
3. **Back button exits instantly.** Bare `BridgeActivity` + single-route SPA = no
|
||||
history, so back quits. Bingo modal and settings close on Escape
|
||||
(`PlayerBoard.svelte:218-226`) but not on back. State survives in localStorage;
|
||||
an in-flight auto-call run does not.
|
||||
|
||||
### Tier 2 — suspected, no device available to confirm
|
||||
|
||||
4. **Edge-to-edge collision.** targetSdk 36 → Android 15+ forces edge-to-edge.
|
||||
No `viewport-fit=cover` in `app.html`, no `env(safe-area-inset-*)` in CSS.
|
||||
Settings gear is absolutely positioned at header `top: 0`.
|
||||
5. **System font scale breaks the grid.** WebView honours system font size. Board is
|
||||
9 fixed columns, `text-xl…text-3xl` in `aspect-[3/4]` cells. Audience skews toward
|
||||
users who enlarge system fonts. Note: unconfirmed whether container-relative units
|
||||
(`cqw`/`clamp`) survive Android's `textZoom` multiplier — likely not.
|
||||
6. **Audio focus.** `new Audio()` in WebView may not take Android audio focus;
|
||||
other apps' audio would play over number calls. Not confirmed broken.
|
||||
|
||||
### Tier 3 — product, deferred by decision
|
||||
|
||||
7. **Every phone is an island.** `active-tab.svelte.js` is explicitly a same-origin
|
||||
same-device coordinator. Real lô tô = one caller, many players. Cross-device sync
|
||||
is the single largest available UX win and is blocked by the offline vow.
|
||||
Owner kept the vow; sync stays out.
|
||||
|
||||
### Checked and dismissed
|
||||
|
||||
- **APK bloat** — audio totals 2.2 MB across both voices. Non-issue.
|
||||
- **Service-worker asset duplication** — SW re-caches ~2 MB already on disk in the
|
||||
APK. Real but not worth engineering time.
|
||||
- **`allowBackup=true`** — stored data is grid, crossed cells, UI settings. Nothing
|
||||
sensitive; backup lets a player restore their card on a new phone. Correct as-is.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Question | Options weighed | Chosen | Why |
|
||||
|---|---|---|---|
|
||||
| Scope | Tier 1 / Tier 2 / Tier 3 / store polish | Tier 1 + Tier 2 + polish | Tier 3 blocked by offline vow |
|
||||
| Offline vow | keep / LAN-only / server | Keep, non-negotiable | Owner decision |
|
||||
| Native code | web-only / web-first / native-free | Web-first, native where forced | Keeps wrapper thin, PWA benefits |
|
||||
| Back button | silent exit / confirm / double-back | Close overlays, confirm exit | Losing a live round warrants one first-party plugin |
|
||||
| Font scale | pin only / pin + setting / CSS only / defer | Pin `textZoom` + in-app size control | Only option that fixes grid without removing a11y |
|
||||
| Wake lock | always / active round / toggle | While round active | Caller device sits untouched; player taps self-serve |
|
||||
| Tier 2 verification | defensive / wait / checklist | Design defensively | Fixes are correct regardless, cost nothing if bug absent |
|
||||
|
||||
## Work items
|
||||
|
||||
### Phase 1 — Tier 1 (no device needed)
|
||||
|
||||
**W1 · VIBRATE permission.** Add `<uses-permission android:name="android.permission.VIBRATE" />`
|
||||
to `android/android/app/src/main/AndroidManifest.xml`. Normal permission, no runtime
|
||||
prompt, no data-safety change. Existing reduced-motion guard unaffected.
|
||||
*Accept:* cell tap buzzes on device; reduced-motion still silent.
|
||||
|
||||
**W2 · Wake lock.** New `web/src/lib/wake-lock.svelte.js` wrapping
|
||||
`navigator.wakeLock.request("screen")`. Must re-acquire on `visibilitychange` —
|
||||
Android auto-releases when the page hides. Consumed by `$effect` in
|
||||
`MasterPanel.svelte` keyed on `autoRunning || hasGame` (both already local, lines
|
||||
47/55-57). No-ops when `navigator.wakeLock` absent (old WebView, minSdk 24).
|
||||
*Accept:* 3+ min auto-call untouched, screen stays lit; stop releases; background →
|
||||
foreground re-acquires.
|
||||
|
||||
**W3 · Back button.**
|
||||
- Web half: new `web/src/lib/overlay-history.js`. Overlay open pushes history entry;
|
||||
single `popstate` listener closes topmost. Careful case: close via button/Escape
|
||||
must pop the sentinel without re-triggering close. Touches `PlayerBoard.svelte`
|
||||
(`showCongrats`) and `SettingsButton.svelte`.
|
||||
- Native half (exit guard): **unresolved fork.** targetSdk 36 enables predictive back
|
||||
by default and ignores legacy `onBackPressed` interception. Verify whether
|
||||
Capacitor 8 `App.addListener('backButton')` still fires before writing code.
|
||||
Fallback: AndroidX `OnBackPressedCallback` in `MainActivity.java` + Vietnamese
|
||||
confirm dialog from `strings.xml`. Fallback also avoids giving `web/` a Capacitor
|
||||
dependency.
|
||||
|
||||
*Accept:* back closes an open modal rather than quitting; back at root during a live
|
||||
round asks first.
|
||||
|
||||
### Phase 2 — Tier 2, defensive
|
||||
|
||||
**W4 · Safe areas.** `viewport-fit=cover` in `web/src/app.html`;
|
||||
`env(safe-area-inset-*)` padding in `app.css` / `+page.svelte` container.
|
||||
Also fixes iOS PWA notch.
|
||||
*Accept:* gear + footer fully visible on Android 15/16 with gesture nav.
|
||||
|
||||
**W5 · Font scale.** `MainActivity.java` pins `textZoom = 100`. Paired with new
|
||||
`boardTextScale` setting (0.9 / 1.0 / 1.15 / 1.3) in `settings-store.svelte.js`,
|
||||
own validator following the existing per-key pattern, UI in the settings sheet,
|
||||
drives a CSS var multiplier on `.tan-tan-num`.
|
||||
*Accept:* grid intact at 200% system font; in-app control resizes numbers; persists
|
||||
across reload.
|
||||
|
||||
**W6 · Volume rocker.** `setVolumeControlStream(STREAM_MUSIC)` in `MainActivity.java`.
|
||||
One line, unambiguously correct for an audio-centric app.
|
||||
|
||||
**Deferred: audio focus / MediaSession.** Unconfirmed problem. Building a MediaSession
|
||||
layer around 1-second clips speculatively violates YAGNI. Revisit with device evidence.
|
||||
|
||||
### Phase 3 — Store polish
|
||||
|
||||
- `strings.xml`: `app_name` → `Lô tô` (diacritics). Currently ASCII-mangled `Lo To`
|
||||
in the launcher while the app itself is "Lô tô — Hội chợ TN1".
|
||||
- Monochrome layer in `mipmap-anydpi-v26/ic_launcher.xml` (Android 13+ themed icons),
|
||||
derived from `web/static/icons/source.svg`.
|
||||
- `values-night/styles.xml` — dark splash. App has a full dark theme; splash flashes light.
|
||||
- `versionCode` + `versionName` bump per existing documented process. Suggest 0.1.0,
|
||||
not 1.0.0 — still internal track.
|
||||
- `android/README.md`: document VIBRATE, wake lock, back-button behaviour. Leave the
|
||||
"no INTERNET permission" section verbatim.
|
||||
|
||||
## Touchpoints
|
||||
|
||||
| File | Work |
|
||||
|---|---|
|
||||
| `android/android/app/src/main/AndroidManifest.xml` | W1 |
|
||||
| `android/android/app/src/main/java/com/miti99/loto/MainActivity.java` | W3 (fallback), W5, W6 |
|
||||
| `android/android/app/src/main/res/values/strings.xml` | W3 (dialog strings), P3 |
|
||||
| `android/android/app/src/main/res/values-night/styles.xml` | P3 (new) |
|
||||
| `android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml` | P3 |
|
||||
| `android/android/app/build.gradle` | P3 version bump |
|
||||
| `web/src/lib/wake-lock.svelte.js` | W2 (new) |
|
||||
| `web/src/lib/overlay-history.js` | W3 (new) |
|
||||
| `web/src/lib/MasterPanel.svelte` | W2 |
|
||||
| `web/src/lib/PlayerBoard.svelte` | W3 |
|
||||
| `web/src/lib/SettingsButton.svelte` | W3, W5 |
|
||||
| `web/src/lib/settings-store.svelte.js` | W5 |
|
||||
| `web/src/app.html` | W4 |
|
||||
| `web/src/app.css` | W4, W5 |
|
||||
| `android/README.md` | P3 docs |
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Predictive back vs Capacitor 8** (W3) — resolve before implementation; fallback
|
||||
identified. Highest-uncertainty item in the plan.
|
||||
2. **`textZoom` pin overrides accessibility** — mitigated only by W5's in-app control.
|
||||
They ship together or not at all; cutting the setting means cutting the pin.
|
||||
3. **`web/` gaining a Capacitor dependency** — if the exit guard uses `@capacitor/app`,
|
||||
the standalone GH Pages build inherits it. Native fallback avoids this.
|
||||
4. **No device in the working environment** — W4/W5 designed on reasoning, not
|
||||
observation. Both safe-by-default; harmless if the bug does not reproduce.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cd web && pnpm test` — settings-store gains `boardTextScale` validator coverage.
|
||||
- New unit coverage: `overlay-history` push/pop/dismiss ordering; `wake-lock`
|
||||
re-acquire on visibility change.
|
||||
- Manual on Android 15/16 device: haptics, 3-min auto-call with screen untouched,
|
||||
back from modal, back at root mid-round, system font at 200%, gesture-nav insets,
|
||||
volume rocker during a call.
|
||||
- `cd android && npm run build && npm run assemble:debug` must stay green.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Multi-device sync, INTERNET permission, audio focus/MediaSession, APK size,
|
||||
`allowBackup` changes, service-worker asset dedup.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. W3 native path — Capacitor 8 `backButton` under predictive back: fires or not?
|
||||
Decides whether `web/` takes a Capacitor dependency.
|
||||
2. Confirm-on-exit trigger — only when a round is live (needs the flag plumbed to
|
||||
native), or unconditionally at root (simpler, mildly noisier)?
|
||||
3. `boardTextScale` steps — are 0.9/1.0/1.15/1.3 the right rungs, or is a continuous
|
||||
slider preferred alongside the existing colour picker?
|
||||
|
||||
## Next step
|
||||
|
||||
`/ck:plan` with this report as input. Default mode recommended over `--tdd`: work is
|
||||
mostly additive across new modules and native config, not a refactor of covered
|
||||
behaviour. Exception — W5 touches `settings-store.svelte.js`, which has 369 lines of
|
||||
existing tests worth preserving.
|
||||
File renamed without changes.
-131
@@ -1,131 +0,0 @@
|
||||
# Android experience pass — implementation report
|
||||
|
||||
- Date: 2026-08-14
|
||||
- Branch: `main` (uncommitted)
|
||||
- Plan: [plans/260814-1317-android-experience-pass/plan.md](../260814-1317-android-experience-pass/plan.md)
|
||||
- Brainstorm: [brainstorm-android-ux-260814-1004](brainstorm-android-ux-260814-1004-android-experience-pass-report.md)
|
||||
|
||||
## Verified here
|
||||
|
||||
| Gate | Result |
|
||||
|------|--------|
|
||||
| `pnpm test` | 8 files, 121 tests pass (was 6 files / 105) |
|
||||
| `pnpm lint` | clean |
|
||||
| `pnpm build` | succeeds; `viewport-fit`, safe-area CSS, `.board-num`/`.master-num`, `--board-text-scale` confirmed present in output |
|
||||
|
||||
## NOT verified — no device, emulator, browser, or Android SDK
|
||||
|
||||
Gradle cannot run here (`ANDROID_HOME` unset, no SDK, no `adb`). **The Java and
|
||||
all Android resources are uncompiled.** Static review only.
|
||||
|
||||
Device QA required for: haptics, wake lock, back button + exit dialog, safe-area
|
||||
insets, system font scale, volume rocker, launcher icon, splash.
|
||||
|
||||
## Delivered
|
||||
|
||||
### Phase 1 — Tier 1 defects
|
||||
|
||||
- **W1 VIBRATE** — `AndroidManifest.xml`. Normal permission; no runtime prompt,
|
||||
no data-safety change.
|
||||
- **W2 wake lock** — new `web/src/lib/wake-lock.js` + 8 tests. Re-acquires on
|
||||
`visibilitychange` (Android drops the lock on hide). Generation token stops a
|
||||
late-resolving `request()` leaking a lock after the caller turned it off.
|
||||
Consumed by a `$effect` in `MasterPanel.svelte`.
|
||||
- **W3 back button** — native `OnBackPressedCallback` in `MainActivity.java` +
|
||||
new `web/src/lib/overlay-history.js` (8 tests). Overlays push a history
|
||||
sentinel; `canGoBack()` therefore means "an overlay is open". Root back shows
|
||||
a Vietnamese confirm dialog.
|
||||
|
||||
### Phase 2 — Tier 2, defensive
|
||||
|
||||
- **W4 safe areas** — `viewport-fit=cover` + `env(safe-area-inset-*)` padding on
|
||||
`body`. Also fixes the iOS PWA notch.
|
||||
- **W5 font scale** — `textZoom = 100` pinned natively; new `boardTextScale`
|
||||
setting (0.9/1/1.15/1.3) with validator, CSS var, and a "Cỡ chữ bảng" picker
|
||||
in the settings sheet. 7 new tests.
|
||||
- **W6 volume rocker** — `setVolumeControlStream(STREAM_MUSIC)`.
|
||||
|
||||
### Phase 3 — Store polish
|
||||
|
||||
- `app_name` → `Lô tô` (was ASCII `Lo To`).
|
||||
- **Launcher icon and splash rebuilt from the brand mark** (see finding below).
|
||||
Legacy + round + adaptive foreground + monochrome at 5 densities; adaptive
|
||||
background as a gradient vector; splash as a themed layer-list with a night
|
||||
variant plus `windowSplashScreen*` for API 31+.
|
||||
- 11 stock splash PNGs and the template robot vector deleted.
|
||||
- `versionCode 4`, `versionName 0.1.0`.
|
||||
- `android/README.md`: permissions, Android-specific behaviour, icon pipeline.
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. The APK was shipping Capacitor's logo (fixed)
|
||||
|
||||
Launcher icon, round icon, adaptive foreground and splash were all the stock
|
||||
Capacitor template — the blue "X" mark on a teal grid plate. The brand icon
|
||||
existed only in `web/static/icons/`. A published Play Store app was showing
|
||||
another project's logo in the launcher and on cold start.
|
||||
|
||||
### 2. `source.svg` overflows its canvas (NOT fixed — web scope)
|
||||
|
||||
`font-size="240"` renders "Lô tô" at ~437px against a 400px inner panel, and the
|
||||
original PNGs were rendered without Roboto Condensed so a wider fallback pushed
|
||||
it past the 512px canvas entirely. **`web/static/icons/icon-192.png`,
|
||||
`icon-512.png` and `icon-maskable-512.png` are visibly clipped** — the `L` and
|
||||
the trailing `ô` are cut off. These are what the PWA, GH Pages install prompt,
|
||||
and the OG/Twitter card images use.
|
||||
|
||||
Android art was rendered at a corrected size (200 legacy / 160 adaptive) with
|
||||
the real font. The web PNGs were left alone — outside approved scope.
|
||||
|
||||
### 3. Predictive back changes the mechanism (resolved during planning)
|
||||
|
||||
Android 16 stops calling `onBackPressed()` and stops dispatching `KEYCODE_BACK`
|
||||
at targetSdk 36. Capacitor 8 shows no back handling in `BridgeActivity`/`Bridge`
|
||||
on `main`, so its default "back navigates WebView history" cannot be assumed to
|
||||
survive either — meaning a pure-web History-API approach could not have worked
|
||||
alone. `OnBackPressedCallback` is the only reliable mechanism.
|
||||
|
||||
### 4. `allowBackup` reassessed — no change
|
||||
|
||||
Flagged as a gap in the brainstorm. Stored data is grid, crossed cells, and UI
|
||||
settings. Nothing sensitive; backup restores a player's card on a new phone.
|
||||
Correct as-is.
|
||||
|
||||
## Risks carried into device QA
|
||||
|
||||
1. **Exit dialog theme.** Activity theme is `AppTheme.NoActionBarLaunch`
|
||||
(parent `Theme.SplashScreen`). Mitigated two ways: explicit
|
||||
`AppTheme.ExitDialog` (`Theme.AppCompat.DayNight.Dialog.Alert`) passed to the
|
||||
builder, and `postSplashScreenTheme` added. If the dialog still throws, the
|
||||
activity theme is the cause.
|
||||
2. **`canGoBack()` baseline.** Assumes the WebView has no history at rest.
|
||||
SvelteKit hydrates with `replaceState`, so it should be false — verify back
|
||||
at the root confirms exit rather than navigating.
|
||||
3. **`textZoom` pin is an accessibility override.** Only defensible alongside
|
||||
the in-app size control. If that setting is ever cut, cut the pin too.
|
||||
4. **Uncompiled Java.** `OnBackPressedCallback` and `Bridge` resolve
|
||||
transitively through `appcompat` / `capacitor-android`; not proven.
|
||||
|
||||
## Device QA checklist
|
||||
|
||||
- [ ] Tap a cell → vibrates; enable reduced-motion → silent.
|
||||
- [ ] Auto-call 3+ min untouched → screen stays lit. Stop → sleeps normally.
|
||||
- [ ] Background during auto-call, return → still lit.
|
||||
- [ ] Finish a round (remaining 0) → lock releases.
|
||||
- [ ] Back with bingo modal open → modal closes, app stays.
|
||||
- [ ] Back with settings open → sheet closes.
|
||||
- [ ] Back at root → Vietnamese exit dialog; "Ở lại" keeps state.
|
||||
- [ ] System font 200% → grid intact; Settings → Cỡ chữ bảng resizes; survives reload.
|
||||
- [ ] Gesture nav + status bar clear of the settings gear and footer.
|
||||
- [ ] Volume rocker during a call → media slider.
|
||||
- [ ] Launcher shows `Lô tô` with the brand icon; themed-icon mode tints it.
|
||||
- [ ] Cold start in dark mode → dark splash, no white flash.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
1. Fix the clipped web PWA icons (finding 2)? Same root cause, different scope —
|
||||
changes GH Pages / PWA / OG card images.
|
||||
2. `boardTextScale` rungs 0.9/1/1.15/1.3 — enough range for users who ran
|
||||
system font at 200%?
|
||||
3. Exit confirm currently fires at root regardless of round state. Worth
|
||||
plumbing "is a round live" to native so an idle app exits without a prompt?
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user