Add a standalone Android app with the same daily reading and config page
build / build (push) Successful in 4m8s
build / build (push) Successful in 4m8s
android/ links engine/src/ and interpreter/src/ via JNI/CMake (real astronomy.c, not the watch's size-constrained substitute), rendering through Jetpack Compose with a burger-menu config page mirroring WatchConfig and a WorkManager-driven daily notification. Independent of the watch app - neither requires the other. build-image now bundles the Android SDK/NDK alongside the Pebble SDK (VERSION bumped to 2), and .gitea/workflows/build.yml/Makefile/ run-image.sh build and publish the APK the same way as the CLI tarball and .pbw.
This commit is contained in:
@@ -123,30 +123,94 @@ push/PR, alongside `make test`/`make package` - the whole job runs
|
||||
inside a container image (`build-image/`, built and pushed with
|
||||
`build-image.sh`/`upload-image.sh`, see "Build image" below) that has
|
||||
the Pebble SDK baked in, so the CI runner itself needs no toolchain
|
||||
beyond Docker access to pull that image. Both the CLI tarball and the
|
||||
`.pbw` are uploaded as build artifacts and published to
|
||||
beyond Docker access to pull that image. The CLI tarball, the `.pbw`,
|
||||
and the Android `.apk` (see "Android app packaging" below) are all
|
||||
uploaded as build artifacts and published to
|
||||
`dl.ladkau.de/files/deck-in-a-dash/` on push.
|
||||
|
||||
### Android app packaging (`make android`)
|
||||
|
||||
A separate target, deliberately **not** a prerequisite of `all` - it
|
||||
shells out to `./gradlew assembleDebug` inside `android/` (via that
|
||||
directory's own Gradle/AGP/NDK toolchain, not this Makefile's plain-`cc`
|
||||
rules) and copies the resulting
|
||||
`android/app/build/outputs/apk/debug/app-debug.apk` to
|
||||
`dist/deck-in-a-dash-<version>.apk`, using the exact same version
|
||||
resolution as `make package`/`make watchapp` above (current git tag, or
|
||||
`make android VERSION=1.2.3` to override). Requires the Android SDK/NDK
|
||||
(`ANDROID_HOME`/`ANDROID_NDK_HOME` set, plus the Python 3 + Pillow
|
||||
`android/app/build.gradle.kts`'s `preBuild` task needs for its own
|
||||
resource-generation scripts - see `android/README.md`) - a machine
|
||||
without it can still build/test/package the CLI and the watch app via
|
||||
every other target in this file.
|
||||
|
||||
See `android/README.md` for the app's own architecture summary; in
|
||||
short, it's a Jetpack Compose Material3 app whose native layer links the
|
||||
same `engine/src/*.c`/`interpreter/src/*.c` sources the watch links (via
|
||||
JNI/CMake, `android/app/src/main/jni/CMakeLists.txt`) - **the real,
|
||||
full-precision `engine/third_party/astronomy/astronomy.c`, not the
|
||||
watch's `lowprec_ephemeris.c` substitute**, since a phone has no
|
||||
equivalent of Pebble's 64KB code+data+bss budget forcing that
|
||||
substitution. Like the watch, the interpreter's real `.c` implementations
|
||||
are linked in directly (`significance.c`/`narrative.c`/`guidance.c`),
|
||||
not `json.c`/`reading_io.c` - there's no JSON pipe between separate
|
||||
binaries here either, just one process computing and rendering its own
|
||||
reading. Unlike the watch (which walks the `DailyReading`/
|
||||
`DailyInterpretation` structs directly across the same C/UI boundary),
|
||||
the JNI bridge (`android/app/src/main/jni/reading_json.c`, a structural
|
||||
port of `interpreter/src/main.c`'s `interp_print_json()`) serializes one
|
||||
complete reading to a JSON string per call - a single call, not
|
||||
fine-grained per-field accessors, since the alternative would mean
|
||||
either re-running real ephemeris trig per field access or pinning a live
|
||||
native object across calls; Kotlin then decodes that JSON with
|
||||
`kotlinx.serialization` rather than maintaining its own JNI accessor
|
||||
surface. Every string in that JSON (titles, narrative, guidance,
|
||||
position/card names and meanings) is already localized via `i18n_get()`,
|
||||
so Kotlin never needs its own planet/aspect/sign display-name table -
|
||||
only the `*_slug` fields exist for the UI's card-art drawable lookup.
|
||||
Card art is regenerated by `android/scripts/gen_card_images.py`
|
||||
(structurally a sibling of `watch/scripts/gen_card_images.py`, same slug
|
||||
table) at 400px wide, lossy WebP - not the watch's 72px PNG, since a
|
||||
phone has no hardware-decoder budget forcing that size, but 21MB of
|
||||
source JPEGs would otherwise bloat the APK; config (birth data, gender,
|
||||
language, notification settings - the same fields as `WatchConfig`) is
|
||||
persisted via Jetpack Preferences DataStore instead of Pebble's
|
||||
key-value persist API. The daily notification uses WorkManager (a
|
||||
self-rescheduling one-shot chain, not `AlarmManager`, since a "roughly
|
||||
this time daily" reminder doesn't need `AlarmManager`'s exact-alarm
|
||||
permission burden) - the Android analog of `watch/src/c/notify.c`'s own
|
||||
daily-wakeup contract.
|
||||
|
||||
**CI needs network access for this target specifically** - unlike the
|
||||
CLI/Pebble build path above (deliberately zero network access needed
|
||||
once `build-image` is pulled), Gradle/AGP/androidx dependency resolution
|
||||
against Google's Maven + Maven Central happens at build time, every
|
||||
build. This is a deliberate, accepted deviation from this image's
|
||||
otherwise-offline build philosophy, not an oversight.
|
||||
|
||||
### Build image
|
||||
|
||||
`build-image/Dockerfile` pins the exact toolchain versions (Pebble
|
||||
Tool, Pebble SDK core, Node.js) needed for `make test`, `make package`,
|
||||
and `make watchapp`, so builds are reproducible independent of whatever
|
||||
happens to be installed on a given machine. Three root-level scripts
|
||||
drive it, each reading registry settings from a gitignored
|
||||
`registry.env` (copy `registry.env.example` to create it - never commit
|
||||
the real file, since it holds registry credentials) and the image tag
|
||||
from `build-image/VERSION`:
|
||||
Tool, Pebble SDK core, Node.js, and the Android SDK/NDK) needed for
|
||||
`make test`, `make package`, `make watchapp`, and `make android`, so
|
||||
builds are reproducible independent of whatever happens to be installed
|
||||
on a given machine. Three root-level scripts drive it, each reading
|
||||
registry settings from a gitignored `registry.env` (copy
|
||||
`registry.env.example` to create it - never commit the real file, since
|
||||
it holds registry credentials) and the image tag from
|
||||
`build-image/VERSION`:
|
||||
|
||||
- `build-image.sh` - builds and locally tags the image (`:VERSION` and
|
||||
`:latest`), no push.
|
||||
- `run-image.sh [VERSION]` - runs `make test package watchapp` inside
|
||||
the locally-built image against a live bind-mount of the repo, wiping
|
||||
generated build artifacts first for CI parity. Use this to validate a
|
||||
`build-image/Dockerfile` change before pushing the image anywhere.
|
||||
- `run-image.sh [VERSION]` - runs `make test package watchapp android`
|
||||
inside the locally-built image against a live bind-mount of the repo,
|
||||
wiping generated build artifacts first for CI parity. Use this to
|
||||
validate a `build-image/Dockerfile` change before pushing the image
|
||||
anywhere.
|
||||
- `upload-image.sh` - logs in and pushes the image (`:VERSION` and
|
||||
`:latest`) to the registry; `.gitea/workflows/build.yml` pulls
|
||||
`:latest` to run its build job (see "Watch app packaging" above).
|
||||
`:latest` to run its build job (see "Watch app packaging"/"Android app
|
||||
packaging" above).
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
Reference in New Issue
Block a user