9.5 KiB
Android setup, emulator and deployment
Everything here is per-user and removable. Nothing is installed system-wide, no package manager outside mise is involved, and no NDK is needed at any point.
What installs what
It is not mise alone, and the distinction matters when something breaks.
| Layer | Provided by | Pinned? | Lives in |
|---|---|---|---|
| Zig, JDK, Gradle, SDK command-line tools | mise, from .mise.toml |
yes, by version | ~/.local/share/mise/installs/ |
| SDK packages: platforms, build-tools, platform-tools, emulator, system images | sdkmanager, via the mise tasks below |
partly - see below | inside the mise android-sdk install directory |
| The AVD (the emulator's virtual device) | avdmanager, via a mise task |
n/a | .tmp/avd in this repo, gitignored |
mise install is necessary but not sufficient. It gives you sdkmanager; it does not give
you an SDK you can build against. mise has no idea the SDK packages exist, so they are not
captured by mise install on another machine and are lost if the android-sdk tool is
reinstalled. The four tasks below exist to make that step repeatable, and they are the
answer to "is it mise only": it is now, in the sense that every command is a mise task,
but the pinning is weaker than the [tools] table.
platforms;android-35, build-tools;35.0.0 and the system image are pinned by exact
version. platform-tools and emulator have no version component in their package ids
and always resolve to whatever is current - the one genuinely unpinned thing here.
Disk cost, which is not small: about 450MB for the build packages, and about 4.6GB more if you want the emulator (821MB emulator, 3.8GB system image).
One-time setup
mise install # Zig, JDK 21, Gradle 8.14.5, SDK command-line tools
mise run android-sdk # platform-tools, platforms;android-35, build-tools;35.0.0
Add the emulator only if you want one; a physical device needs neither of these:
mise run android-emulator # emulator + 16KB-page system image, ~4.6GB
mise run android-avd # creates the tally35 AVD in .tmp/avd
The system image is system-images;android-35;google_apis_ps16k;x86_64, chosen on
purpose. ps16k means 16KB memory pages, which Android 15 introduced and which
zig build android aligns the shared libraries for. It is the stricter target: 16KB
alignment is also valid on a 4KB device, so a library that loads there loads anywhere.
A libc-free library is more likely to break on it than on anything else, which is the
point.
Build
zig build android # native libs, one .so per ABI, into zig-out/android
cd android && gradle assembleDebug # packages them into the APK
The APK lands at android/app/build/outputs/apk/debug/app-debug.apk.
Gradle never invokes Zig. jniLibs.srcDirs points at zig-out/android, so Gradle only
packages whatever is sitting there. After changing engine or JNI code you must run
zig build android yourself, or Gradle will cheerfully package a stale library and the
app will run old arithmetic.
Run it in the emulator
Two steps, because booting the emulator and installing the app are separate. A freshly booted AVD has no Tally on it.
mise run android-run & # boots tally35; `adb emu kill` to stop
mise run android-install # builds libs + APK, waits for boot, installs, launches
android-install re-runs zig build android every time on purpose, so the APK never
carries a stale library. It waits for sys.boot_completed, so it is safe to start while
the emulator is still booting. It works the same against a phone.
The app goes missing in two situations, both expected:
mise run android-avdrecreates the AVD with--force, which wipes its storage.gradle connectedAndroidTestuninstalls the app when it finishes (AGP's default).
Either way, mise run android-install puts it back.
The manual equivalent, if you want the pieces:
adb wait-for-device
adb shell getprop sys.boot_completed # wait until this prints 1
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n dev.lerch.tally/.MainActivity
For a headless run - useful over ssh, or when you only want the instrumented tests:
mise run android-run -- -no-window
Hardware acceleration needs /dev/kvm readable, which in practice means being in the
kvm group (id -nG | grep kvm). Without it the emulator falls back to software
rendering and is unusably slow rather than broken.
If you run the emulator outside the mise task, export the AVD location first or it will
not find tally35, because it is deliberately not in ~/.android:
export ANDROID_AVD_HOME="$PWD/.tmp/avd"
Deploy to a phone over USB
On the phone: Settings > About phone > tap Build number seven times, then Settings > System > Developer options > USB debugging on.
adb devices # the phone appears; accept the prompt on its screen
mise run android-install # or: adb install -r <apk> && adb shell am start ...
Until you accept the "Allow USB debugging?" prompt on the phone, adb devices lists it as
unauthorized and every other command fails. The APK is signed with the local debug
keystore (~/.android/debug.keystore), which is fine for sideloading onto your own device
and not for distribution.
Deploy to a phone over Wi-Fi
Yes, and there are two mechanisms. The modern one needs no cable at all.
Wireless debugging, Android 11 and later
On the phone: Developer options > Wireless debugging > on. Tap
Pair device with pairing code. It shows a six-digit code and an
ip:port for pairing - note that this port is different from the one on the main
Wireless debugging screen, which is the one you connect to afterwards.
adb pair 192.168.1.50:37419 # the pairing ip:port; it prompts for the code
adb connect 192.168.1.50:42133 # the ip:port from the Wireless debugging screen
adb devices # shows 192.168.1.50:42133 device
Then install and am start exactly as over USB. Pairing is remembered, so later
sessions need only adb connect; the port changes on reboot, so re-read it from the
phone. adb disconnect when finished.
adb pair exists in platform-tools 37.0.1, which is what mise run android-sdk
installs. Verify with adb --help | grep pair if in doubt.
adb over TCP, any version, needs one cable first
adb tcpip 5555 # over USB; restarts adbd listening on TCP
# unplug the cable now
adb connect 192.168.1.50:5555
This is the older path and it is worth knowing what it gives up: it is unauthenticated
beyond the existing adb key and stays open on a fixed port until the phone reboots or you
run adb usb. On a network you do not control, prefer adb pair.
Both paths need the phone and the computer on the same network, with client isolation off
- many guest networks and some mesh setups block the connection silently.
Tests
zig build test # 965 host tests, including the C ABI
zig build jvm-test # proves the JNI table against a desktop JVM, no device
cd android && gradle connectedAndroidTest # 10 instrumented tests, needs a device or emulator
gradle connectedAndroidTest uninstalls both APKs when it finishes, which is AGP's
default. Reinstall before launching the app by hand, or you get
Activity class ... does not exist.
When it goes wrong
Error: An error occurred while creating AVD: ~/.android/avd - on this machine
~/.android/avd is a dangling symlink into a Flatpak Android Studio config directory that
no longer exists. avdmanager follows it, fails, and reports the path with no explanation.
The android-avd task sidesteps it with ANDROID_AVD_HOME; deleting the dead symlink
also works.
UnsatisfiedLinkError: dlopen failed: cannot locate symbol ... - the native library
referenced something Bionic provides and this library does not link. See design 6.3; the
audit is zero DT_NEEDED, zero undefined dynamic symbols, no TLS segment, per ABI:
readelf -d zig-out/android/arm64-v8a/libtally.so | grep NEEDED # expect nothing
readelf --dyn-syms -W zig-out/android/arm64-v8a/libtally.so | awk '$7=="UND"'
Check all three ABIs. One instance of this reproduced on arm64-v8a alone, so an x86_64 emulator passed while phones would have failed.
The app runs but answers look stale - zig build android was not re-run, so the APK
carries an old libtally.so.
Nothing is wrong but you want to know the boundary is alive - the title bar reads
Tally 0.1.0, and that string comes from tally_version through JNI. If the title
renders, the library loaded and a string round-tripped.
Logs:
adb logcat -d | grep -iE 'tally|UnsatisfiedLink|FATAL'
Which library a device actually loaded, and whether it has 16KB pages:
adb shell getprop ro.product.cpu.abi
adb shell getconf PAGE_SIZE
Removing all of it
adb emu kill # stop a running emulator
rm -rf .tmp/avd # the virtual device
mise run android-emulator --help # (no uninstall task; use sdkmanager directly)
sdkmanager --uninstall "emulator" "system-images;android-35;google_apis_ps16k;x86_64"
mise uninstall java gradle android-sdk # the tools themselves
Nothing outside ~/.local/share/mise, ~/.android and this repo's .tmp/ is touched.
~/.android holds the adb key and debug keystore and is shared with any other Android
tooling on the machine, so it is left alone.