Files
DarkRoom/docker/android
dtourolleandClaude Opus 5 3cfa78cde2 Give the app a face and a name on the launcher
There was no icon anywhere, and on Android that was not a missing line in the
manifest. `aapt2 link` was being handed a manifest and nothing else, so the
APK carried no res/ and no resources.arsc — there was no table for an
`@mipmap/...` reference to resolve against even if one had been written.
Packaging now compiles the resource tree first and links the result in, which
is the two steps aapt2 insists on: link reads compiled input only, never a
directory.

That absent table is also why the launcher caption was blank, which had
looked like a second, separate bug. `android:label="DarkRoom"` was there and
correct the whole time, and Settings' App info read it fine; the launcher
could not, because resolving a label goes through the package's Resources and
there were none to open. Nothing about the label changed here. It came back
with the table under it.

`android:icon` then names one drawable for both icon generations, because the
`anydpi-v26` qualifier is what separates them. API 26 and up take the adaptive
icon and its three layers; the third of those, monochrome, is what lets
Android 13 recolour it rather than drop the app out of the themed set. Below
26 the same name lands on a density-matched PNG. `roundIcon` is deliberately
absent — a launcher old enough to read it is one that would ignore the
adaptive XML, and minSdk is 28.

The desktop icon is one `@image-url` on the window, and the only raster asset
in a UI that is otherwise entirely Path. The reasoning at the top of
icons.slint does not reach it: that is about glyphs a font might not carry,
and this image is never drawn by us at all. It goes to the window manager,
which wants pixels and composites them unmasked, so it is pre-shaped with
rounded corners rather than square the way the Android layers are.

Which exposed Slint's resource default. An `@image-url` compiles down to the
absolute path it had on the build machine, to be opened at runtime — already
wrong for Android, where the build happens under /work inside a container and
no such directory exists on the device, and wrong silently, as an image that
loads empty. `EmbedFiles` puts the bytes in the binary instead. It reaches
nothing else, since every glyph is a Path.

Verified on a device: the APK installs and the home screen draws both the
icon and "DarkRoom" under it, where before it had neither. In the link step
the adaptive icon resolves at all six densities and resources.arsc lands
uncompressed, which API 30 requires and the existing zipalign preserves. On
the desktop by reading _NET_WM_ICON off the running window — 256x256, as
handed over. Where that actually shows is narrower than it sounds, and the
comment says so: Wayland ignores the property in favour of matching app_id
against an installed .desktop file, which this repo does not install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 20:25:22 +02:00
..

Android build environment

Reproducible container for cross-compiling DarkRoom to Android. CI and local builds use the same image, so a break appears in one place rather than two.

Use

# Cross-compile for a device ABI
./docker/android/build.sh cargo ndk -t arm64-v8a build --release

# All four ABIs
./docker/android/build.sh cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 build --release

# Type-check without linking
./docker/android/build.sh cargo check --target aarch64-linux-android

# Interactive shell
./docker/android/build.sh

# After editing the Dockerfile
./docker/android/build.sh --rebuild

Prefers podman, falls back to docker. First run builds the image, which takes several minutes; after that it is cached.

In CI

.gitea/workflows/android-image.yml builds this image and pushes it to gitea.tourolle.paris/dtourolle/darkroom-android, which the Android job then runs inside. It is called on every push and finishes in seconds unless something here changed — the image is tagged with the git tree hash of docker/android/, so a rebuild happens when and only when a file in this directory does.

Nothing needs pushing by hand. Editing the Dockerfile is the trigger; latest follows automatically. To force a rebuild without a content change, run the workflow from the Gitea UI with force set to true.

The job runs on the host runner rather than in a container, because it needs the Docker daemon and the runner host's cached registry credentials.

Pinned versions

Component Version Why this one
JDK 17 AGP's stable target. The host's JDK 25 breaks Gradle.
Compile SDK API 36 Required for Play distribution
Min API 28 (Android 9) Floor where Vulkan support is dependable (NFR-COMPAT-1)
Build tools 36.0.0 Matches compile SDK
NDK 27.2.12479018 (r27c) Current LTS
Rust 1.92.0 Slint 1.17 requires it; cargo-ndk 4.1.x needs ≥ 1.86
cargo-ndk 4.1.2

Bumping any of these is a reviewable change, not silent drift.

Compile SDK versus min API

These are different and both matter. ANDROID_API (36) is what the SDK compiles against — the newest APIs available. MIN_API (28) is what native code links against, and determines the oldest device that can run the result.

cargo-ndk defaults to API 21 if not told otherwise, which is far below what this app needs. CARGO_NDK_PLATFORM and the per-target linker paths both pin it to MIN_API. Verify with:

file target/aarch64-linux-android/release/*.so
# → "... for Android 28, built by NDK r27c"

Caching

The cargo registry and target directory persist in ~/.cache/darkroom-android, so rebuilds do not re-download the crate index. Clear with:

rm -rf ~/.cache/darkroom-android

The Android target dir is kept separate from the host's target/ — sharing one directory between host and container builds causes constant rebuilds as they invalidate each other's fingerprints.

What is not here

No emulator, no adb device access. This image cross-compiles; it does not run or deploy. Device testing (spikes S2 and S10, which need two GPU vendors) happens on real hardware — the emulator's GPU behaviour is not representative of Adreno or Mali, which is precisely what those spikes exist to test.