Files
Ferrous-Solitaire/docs/ANDROID.md
T
2026-06-08 19:14:48 -07:00

8.3 KiB

Android build — developer setup

This doc captures the toolchain install + build invocation for the Android target. Steps are runnable on a fresh Debian 13 (trixie) box; later sections document physical-device validation, supported platform surfaces, and remaining Android follow-ups.

Status (2026-06-09): Android build plumbing, app-directory storage, JNI keystore wiring, and safe-area layout fixes have landed. The remaining release gate is a physical-device smoke test; AVD tap injection does not exercise the real touch path reliably enough for launch verification.


1. Toolchain install (Debian 13 / trixie)

Run as one block. Will pull ~15-20 GB of disk between APT, the SDK, the NDK, the system image, and Rust target sysroots. Requires sudo.

# 1. JDK 21 (Android tooling needs JDK 17+; Debian 13 default is 21).
sudo apt update && sudo apt install -y openjdk-21-jdk-headless unzip wget

# 2. SDK directory + Google's cmdline-tools bootstrap.
export ANDROID_HOME="$HOME/Android/Sdk"
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -O /tmp/cmdline-tools.zip \
  https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
rm /tmp/cmdline-tools.zip

# 3. Persist env vars.
{
  echo ''
  echo '# Android dev'
  echo 'export ANDROID_HOME="$HOME/Android/Sdk"'
  echo 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264"'
  echo 'export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))"'
  echo 'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
} >> ~/.bashrc
source ~/.bashrc

# 4. Accept SDK licences (interactive prompts answered by `yes |`).
yes | sdkmanager --licenses

# 5. Platform packages — ~5 GB.
sdkmanager \
  "platform-tools" \
  "platforms;android-34" \
  "build-tools;34.0.0" \
  "ndk;26.3.11579264" \
  "emulator" \
  "system-images;android-34;google_apis;x86_64"

# 6. AVD for testing (one-time).
echo no | avdmanager create avd \
  -n bevy_test \
  -k "system-images;android-34;google_apis;x86_64" \
  -d pixel_7

# 7. Rust cross-compile targets.
rustup target add \
  aarch64-linux-android \
  armv7-linux-androideabi \
  x86_64-linux-android \
  i686-linux-android

# 8. cargo-apk.
cargo install cargo-apk

Sanity:

java --version | head -1            # openjdk 21.0.x
adb --version | head -1             # 35.x or higher
sdkmanager --list_installed | head  # build-tools, emulator, ndk, platforms, system-images
avdmanager list avd | head          # bevy_test
rustup target list --installed | grep android  # 4 targets
cargo apk --help | head -5

If sdkmanager --version errors with JAVA_HOME is not set, the env section in step 3 didn't apply to your shell — source ~/.bashrc again or open a new terminal.

Optional: emulator runtime libs

The Android emulator is dynamically linked against X11/GL/audio. If emulator -list-avds works but emulator -avd bevy_test complains about libX11.so.6, install:

sudo apt install -y \
  libx11-6 libxcursor1 libxrandr2 libxi6 libxinerama1 libxxf86vm1 \
  libgl1 libnss3 libpulse0 libxcomposite1

Headless emulator launch:

emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
adb wait-for-device && adb devices
# Stop later:
# adb -s emulator-5554 emu kill

Headless + software rendering is fine for "does it boot" smoke tests but useless for perf measurement — use a physical Pixel-class device over USB for real numbers.


2. Build the APK

cargo apk build -p solitaire_app --target x86_64-linux-android

Output:

target/debug/apk/ferrous-solitaire.apk

Targets shipped via [package.metadata.android].build_targets in solitaire_app/Cargo.toml:

Target Use
aarch64-linux-android Real phones (modern 64-bit ARM)
armv7-linux-androideabi Older 32-bit ARM phones
x86_64-linux-android The bevy_test AVD on this dev box

Build any of them with --target <triple>.

Known cosmetic warning

After the APK is signed cargo-apk panics with:

thread 'main' panicked: Bin is not compatible with Cdylib

This happens AFTER the APK is on disk and signed. cargo-apk tries to also wrap the desktop [[bin]] target alongside the [lib]. The APK is valid — the panic is cosmetic. Always use --lib, which is the canonical build command (see CLAUDE.md §15.1):

cargo apk build -p solitaire_app --lib

Root cause: upstream cargo-apk bug — it does not skip [[bin]] targets when building for Android. No in-repo fix is possible; --lib is the accepted workaround.


3. Install + run

Physical device:

adb devices                                      # confirm connection
adb install -r target/debug/apk/ferrous-solitaire.apk
adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic"

Emulator:

emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
adb wait-for-device
adb install target/debug/apk/ferrous-solitaire.apk
# ... same start + logcat steps as above.

If adb install errors with INSTALL_FAILED_NO_MATCHING_ABIS, the emulator is x86_64 but the APK was built for arm — rebuild with the x86_64-linux-android target, or add an x86_64 system image to the AVD.


4. Physical-device smoke test

Run this on a real phone, preferably a modern 64-bit ARM device with gesture navigation enabled.

Build and install:

cargo apk build -p solitaire_app --target aarch64-linux-android --lib
adb install -r target/debug/apk/ferrous-solitaire.apk
adb logcat -c
adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic|WindowInsets"

Pass criteria:

  • App launches without panic or ANR.
  • Safe-area insets arrive after the first few frames and shift HUD/modal content away from the status and gesture bars.
  • Every modal's Done button remains above the gesture bar: Settings, Help, Pause, Win Summary, and Leaderboard-related dialogs.
  • Drag-and-drop works on tableau, waste, foundation, and stock/recycle paths.
  • Tap-to-select and one-tap modes both respond correctly on card stacks.
  • Leaderboard panel opens, "Set Name" saves, and the "Public name" label updates while the panel remains open.
  • Rotate the device once, then repeat one modal and one drag operation.
  • Close and relaunch the app; settings/progress still load.

Record the device model, Android version, APK commit, and pass/fail notes in the release notes or session handoff. If a failure occurs, keep the filtered logcat and note the exact screen/control path that reproduced it.


5. Platform support matrix

Desktop-only crates and call sites are gated so the workspace cross-compiles. Each gate is documented at its call site.

Surface Desktop Android
Bevy windowing x11 + wayland android-native-activity (NativeActivity glue)
Clipboard ("Copy share link") arboard writes URL Toast surfaces the URL inline
OS keychain (JWT tokens) keyring v4 → Secret Service / Keychain / Credential Store Android Keystore via JNI
Data directory Platform data dir Android app files dir
App entry point bin target → solitaire_app::run() cdylib target loaded by NativeActivity

Remaining Android follow-ups:

  • Touch UX pass — hit-target sizes, modal scaling on small screens, app lifecycle (suspend / resume), font scaling.
  • JNI ClipboardManager for share links.
  • Google Play Games sign-in (the solitaire_gpgs crate referenced in older docs doesn't yet exist).

6. Iteration loop

# Edit code…
cargo build -p solitaire_app                           # desktop sanity
cargo clippy --workspace --all-targets -- -D warnings  # gate
cargo test --workspace                                 # gate
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
adb install -r target/debug/apk/ferrous-solitaire.apk    # `-r` reinstalls
adb logcat -c && adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
adb logcat | grep -iE "RustStdoutStderr|solitaire"

adb logcat is the canonical way to see Bevy / Rust panic output — they end up in the RustStdoutStderr tag.