# 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-avd` recreates the AVD with `--force`, which wipes its storage. - `gradle connectedAndroidTest` uninstalls 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 && 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.