230 lines
9.5 KiB
Markdown
230 lines
9.5 KiB
Markdown
# 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 <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.
|