mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 18:15:53 +00:00
90 lines
3.8 KiB
Markdown
90 lines
3.8 KiB
Markdown
# Lô tô — Android
|
||
|
||
Native Kotlin + Jetpack Compose app for *lô tô hội chợ*, at feature parity
|
||
with the [`web/`](../web) SvelteKit app. Fully offline: the APK declares no
|
||
`INTERNET` permission and bundles everything it needs, including the
|
||
Vietnamese voice clips.
|
||
|
||
The web app is the behavioral spec — game rules, settings contract, and voice
|
||
semantics are ported one-to-one from `web/src/lib/` and its test suites. A
|
||
behavior change on the web side needs a matching Kotlin change here.
|
||
|
||
## Stack
|
||
|
||
- Kotlin 2.x, Jetpack Compose, Material 3, single `:app` module
|
||
- Gradle Kotlin DSL + version catalog (`gradle/libs.versions.toml`), JDK 21
|
||
- `applicationId com.miti99.loto`, minSdk 24, targetSdk 36
|
||
- Manual DI: `LotoApplication` owns the singletons; ViewModels come from
|
||
`LotoViewModelFactory`. No Hilt.
|
||
- Media3 ExoPlayer for voice clips, DataStore Preferences for settings and
|
||
round state
|
||
|
||
## Layout
|
||
|
||
| Package | What it is |
|
||
|---------|------------|
|
||
| `game` | Pure game rules ported from `web/src/lib/game-logic.js` and friends: card generator, row state, draw deck, auto-cross |
|
||
| `audio` | Voice catalog (parses the bundled `manifest.json`) + ExoPlayer wrapper with the web's cancellation semantics |
|
||
| `settings` | `Settings` model + DataStore repository — same fields, defaults, and per-field validation as the web store |
|
||
| `state` | Round persistence, the shared master store, and the three ViewModels |
|
||
| `ui` | Compose screens: player board, master panel, settings sheet, theme |
|
||
|
||
## Audio assets
|
||
|
||
The 184 MP3s (2 voices × 92 clips) and their manifest are **not copied** into
|
||
this project. `app/build.gradle.kts` mounts `../web/static/audio/` (and only
|
||
that directory) as the assets root at build time, so web and Android always
|
||
ship the same clips, and nothing else added to `web/static/` reaches the APK.
|
||
Regenerate them with the web app's `scripts/generate-audio.py`.
|
||
|
||
## Audio focus
|
||
|
||
The player requests **no audio focus**: clips are ~1s speech cues layered
|
||
over whatever the table is already listening to, so music keeps playing and
|
||
incoming calls behave normally. Playback still pauses when audio would become
|
||
noisy (headphones unplugged). See `ExoVoicePlayer`.
|
||
|
||
## Permissions
|
||
|
||
`VIBRATE` only (cell-tap haptics; normal permission, no runtime prompt).
|
||
Media3 would merge `ACCESS_NETWORK_STATE` in; the manifest strips it with
|
||
`tools:node="remove"` to keep the hard offline guarantee.
|
||
|
||
## Build
|
||
|
||
Requires JDK 21 and an Android SDK (compileSdk 36).
|
||
|
||
```bash
|
||
./gradlew :app:lint :app:test :app:assembleDebug # checks + debug APK
|
||
./gradlew :app:bundleRelease # release AAB
|
||
```
|
||
|
||
Debug builds need no signing setup. Release builds sign with a PKCS#12
|
||
keystore read from env vars — the same contract every previous release used:
|
||
|
||
| Env var | Meaning |
|
||
|---------|---------|
|
||
| `LOTO_KEYSTORE_PATH` | absolute path to the `.p12` keystore |
|
||
| `LOTO_KEYSTORE_PASSWORD` | keystore password |
|
||
| `LOTO_KEY_ALIAS` | key alias |
|
||
| `LOTO_KEY_PASSWORD` | key password |
|
||
|
||
Without `LOTO_KEYSTORE_PATH`, release artifacts build unsigned.
|
||
|
||
## Release
|
||
|
||
Releases are tag-driven: bump `versionCode` + `versionName` in
|
||
`app/build.gradle.kts`, tag `v*.*.*`, push. `.github/workflows/android-release.yml`
|
||
guards against a forgotten versionCode bump, runs lint + tests, builds the
|
||
signed AAB + APK, attaches them to the GitHub Release, and uploads the AAB to
|
||
the Play closed-testing track (`alpha`). Secrets and Play setup are documented
|
||
in [`docs/play-store-publishing.md`](../docs/play-store-publishing.md).
|
||
|
||
## Icons and splash
|
||
|
||
Launcher art (adaptive foreground/background, monochrome for Android 13+
|
||
themed icons, legacy mipmaps) was rendered from `web/static/icons/source.svg`
|
||
and is committed under `app/src/main/res/mipmap-*`. The splash uses
|
||
`androidx.core:core-splashscreen` on API 31+ and a layer-list drawable below,
|
||
with light/dark backgrounds matching the web (`#FFFBEB` / `#050813`).
|