Files
emulator-tools/README.md
T
irisandClaude Opus 5 5d77599f3f Collect the emulator tooling into one repo
ui-trace, the adb wrapper and its device recorder were loose files under
~/.local, and each Android checkout carried its own copy of the same
emulator boot sequence. This puts them together, with an install script that
symlinks them back so editing the repo is editing what runs.

Two things are new rather than moved. `emu` is the emulator lifecycle --
name, serial, list, up, down -- keyed on the AVD named after the enclosing
checkout, which is the rule that lets several sessions work here at once;
and `adb` now fills in `-s` from that same rule, because with two emulators
attached a bare `adb shell pm list packages` comes back empty rather than
failing, which reads as the app being uninstalled rather than the question
being ambiguous.

`emu up` refuses when the machine has no room. On 2026-08-30 an emulator
started with 2.8 GB available invoked the OOM killer, and what it took was
not the emulator that had just started: it walked the user slice and killed
pipewire, dbus-broker and another session's emulator first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:06:17 -04:00

90 lines
4.0 KiB
Markdown

# emulator-tools
Everything this machine uses to drive an Android emulator, in one place:
which emulator a command means, whether there is room to start it, reading a
screen as text, and taking a screenshot that does not cost a thousand tokens.
./install.sh
symlinks `bin/*` into `~/.local/bin` and `share/ui-trace` into
`~/.local/share`, so editing this repo is editing what runs. It keeps
anything it replaces as `<name>.bak`.
## What is here
| | |
|---|---|
| `emu` | this checkout's emulator: `name`, `serial`, `list`, `up`, `down` |
| `adb` | adb, aimed at this checkout's emulator, with screenshots scaled |
| `ui-trace` | records a screen as text at 60Hz, and drives it |
| `lib/project-avd.sh` | the rules all three share |
| `share/ui-trace/` | the device recorder (`UiTrace.java`) and its build |
## One emulator per checkout, named after it
`emu` and `adb` both work out which AVD you mean from the enclosing git
checkout: in `~/repos/ai-app-2` that is `ai-app-2`. Nothing has to be
configured, and `AVD_NAME` overrides it where the guess is wrong.
This is the rule that lets several agent sessions work here at once. The
emulator used to be the one resource that could not be parallelised: taking
it meant messaging the other sessions, waiting, and handing it back — and
installing an app onto somebody else's running emulator steals the foreground
from whatever they were looking at.
It is also why `adb` fills in `-s`. With two emulators attached, a bare
`adb shell pm list packages` comes back **empty** rather than failing, which
reads as the app having been uninstalled rather than as the question being
ambiguous. An explicit `-s`, `ANDROID_SERIAL`, or a subcommand that is not
about one device (`adb devices`) all turn the defaulting off, and nothing is
guessed when this checkout's emulator is not running: adb's own error is
better than a wrapper picking a stranger's device.
## Refusing to start one
`emu up` checks `MemAvailable` first and refuses if starting an emulator
would leave the machine short, printing what is attached, what is large, and
what usually frees enough. `EMU_FORCE=1` overrides it.
The reason is a real incident rather than tidiness. On 2026-08-30 an emulator
was started on this box with 2.8 GB available; the OOM killer ran, and what
it took was not the emulator that had just started — it walked the user slice
and killed pipewire, dbus-broker and *another session's* emulator first. The
cost of one too many lands on somebody else's work, minutes later, looking
like an unrelated crash.
Measured numbers behind the defaults: a headless x86_64 AVD is about 3.8 GB
resident, this VM has 23 GB with a swap that is routinely full, two emulators
are comfortable, and three were not.
## Reading a screen without screenshots
`ui-trace` drives a small resident recorder on the device and samples the
accessibility tree at 60Hz, so an animation or a settling layout is visible
rather than happening between two samples.
ui-trace record -d 3000 --do 'tap 540 800' -o /tmp/t.txt
ui-trace elements /tmp/t.txt # what is on screen, to pick from
ui-trace show /tmp/t.txt # what moved
ui-trace show /tmp/t.txt -m 'Send|Stop' --field box
Bounds come out in **device** pixels, so they feed straight back into a
`--do tap`. Prefer this to a screenshot for anything positional: two buttons
that measured identically at 171x105 looked different in a screenshot,
because the filled one read as larger than the disabled grey one. Screenshots
are for appearance — colour, weight, whether it looks right.
`adb exec-out screencap -p` still works and comes back scaled to 800px on its
long edge, which is ~380 tokens to read instead of ~1460 and still legible
for layout, contrast and small labels. `ADB_SCREENCAP_MAX_EDGE` raises it for
one call.
## Using it from a project
A project's own `run-android.sh` should build and install; the emulator half
belongs here:
SERIAL=$(emu up)
./gradlew :androidApp:assembleDebug
adb install -r <apk> # already aimed at this checkout's emulator