diff --git a/AGENTS.md b/AGENTS.md index bc283be..02b198e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,12 @@ for TV) that talks to a PeerTube instance over its REST API v1. ## Environment facts -- Host is Arch Linux with Android SDK at `~/Android/Sdk` (platforms 34/35/36, build-tools). +- Host is Arch Linux. The Android SDK the build actually uses is `/opt/android-sdk` + (`ANDROID_SDK_ROOT` — note `local.properties`' `sdk.dir` points to the outdated + `~/Android/Sdk`, which doesn't exist; platforms 34/35/36, build-tools). +- Emulator testing (host emulator + agent container over adb): see + `docs/agent-container.md` — covers start script, ports, and what to reinstall + after a container rebuild. - The Gradle wrapper is checked in (`gradlew` + `gradle/wrapper/`), so builds need only a JDK 17+ (`java`) on PATH. If `java` is missing, tell the user — do not install things on the host without asking. diff --git a/docs/agent-container.md b/docs/agent-container.md new file mode 100644 index 0000000..48a15d2 --- /dev/null +++ b/docs/agent-container.md @@ -0,0 +1,60 @@ +# Agent container ↔ host emulator setup + +How the testing loop between the JTube TV agent container and the host +emulator works, and what needs to exist again after a rebuild. + +## Topology + +- **Emulator**: runs on the **host** (`/opt/android-sdk`, launched by + `scripts/start-emulator.sh`). **Headless is the default**; set + `JTUBE_EMU_MODE=windowed` if you want the Qt window (e.g. for desktop + interaction testing). View it via scrcpy either way. +- **Agent container**: image `agent-container:latest`, container name pattern + `ac--`, launched with **`--network host`** — it shares the + host's network namespace. This is load-bearing: see below. + +## Why host networking makes adb trivial + +With `--network host`, the container's `localhost` *is* the host's localhost. +So inside the container: + +- `adb connect 127.0.0.1:5556` → hits the host's socat bridge → emulator adbd. +- The adb **server** running on 127.0.0.1:5037 is also shared with the host, + so the container inherits whatever authorization the host adb server has. + No separate RSA handshake is needed. (Caveat: whoever's adb server grabs + port 5037 first owns it; all clients share it.) + +## What must be (re)installed after a container rebuild + +1. **`android-tools` in the container** (provides `adb`; installed into the + running container with `sudo pacman -S --noconfirm android-tools`, but that + is ephemeral). To make rebuilds self-sufficient, bake it into the + `agent-container` image at build time. +2. **Nothing else is needed for adb**: key handling is inherited from the + shared host adb server (see above). If the container is ever switched to + bridge networking, use `adb connect rutherford:5556` — remote keys are + handled by the emulator's `ADB_VENDOR_KEYS` (next section). + +## Emulator-side facts (host) + +- Ports: console `5600`, adbd `5601` (`-port 5600` ⇒ console+1), LAN bridge + `socat TCP-LISTEN:5556 → 127.0.0.1:5601` (0.0.0.0, so remote machines can + `adb connect rutherford:5556`). +- Remote key whitelisting: the TV system image is a user build — adb + `push`/`shell` writes to `/data/misc/adb/adb_keys` are blocked. The script + therefore builds `/tmp/jtube-adbkeys-combined` (host key + every + `/tmp/jtube-adbkeys/*.pub`) and launches the emulator with + `ADB_VENDOR_KEYS` pointing at it. To trust a new client: put its + `~/.android/adbkey.pub` into `/tmp/jtube-adbkeys/` **before** launching. +- Logs: `/tmp/jtube-emu.log` (QEMU) and `/tmp/jtube-emu-run.log` (script). +- Viewing: `scrcpy -s emulator-5600` on the host, or from any LAN machine: + `adb connect rutherford:5556 && scrcpy -s 127.0.0.1:5556`. + +## Known quirks + +- Emulator Qt toolbar (back/home buttons) is a detached XWayland window that + misbehaves on Hyprland — drive the emulator via scrcpy or adb instead. +- The TV image is a user build: no `adb root`. +- Under the Android TV AVD, keyboard input in the Qt window works, but the + reliable inputs are scrcpy (`b` = back, `h` = home) or + `adb shell input keyevent`. \ No newline at end of file diff --git a/scripts/start-emulator.sh b/scripts/start-emulator.sh index c10e04c..5995100 100755 --- a/scripts/start-emulator.sh +++ b/scripts/start-emulator.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash # # start-emulator.sh — boot the JTube TV test emulator on the HOST (not inside the -# agent container), with GPU acceleration and a window. Use this when the app should -# be tested over scrcpy (ATD/system quirks make scrcpy fail against headless -# in-container emulators) or when you simply want to watch it. +# agent container), with GPU acceleration, headless by default (drive it via +# adb/scrcpy). JTUBE_EMU_MODE=windowed shows the Qt window. # # Boots on console :5600 (adbd :5601); the agent container bridges # rutherford:5556 -> 127.0.0.1:5601 with socat, so after this script the emulator is @@ -54,22 +53,41 @@ for pid in $(ps aux | awk '/[q]emu-system-x86_64/ {print $2}' 2>/dev/null); do done sleep 1 -ACCEL=() +MODE="${JTUBE_EMU_MODE:-headless}" # headless (default) | windowed WINDOWED=1 -if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then - echo "desktop session detected — emulator window + GPU host" +ACCEL=() +if [ "$MODE" = windowed ]; then + if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then + echo "windowed (JTUBE_EMU_MODE=windowed) — emulator window + GPU host" + else + echo "JTUBE_EMU_MODE=windowed but no display detected — falling back to headless" + WINDOWED=0 + ACCEL+=(-no-window) + fi else WINDOWED=0 ACCEL+=(-no-window) - echo "headless (no display) — running without a window, swiftshader rendering" + echo "headless (default; JTUBE_EMU_MODE=windowed for a window) — no emulator window" fi if [ -e /dev/kvm ]; then - [ $WINDOWED -eq 1 ] && ACCEL+=(-accel kvm) + ACCEL+=(-accel on) echo "/dev/kvm found — hardware acceleration on" fi +# --- build combined adb vendor-keys file ------------------------------------ +# The TV system image is a user build: adb push to /data/misc/adb/adb_keys is +# blocked, so remote keys are trusted via ADB_VENDOR_KEYS at launch instead. +# Drop extra public keys into /tmp/jtube-adbkeys/ (one per file), e.g. from the +# agent container: docker exec sh -c 'cat ~/.android/adbkey.pub' > /tmp/jtube-adbkeys/agent-container.pub +VKEYS=/tmp/jtube-adbkeys-combined +{ + cat "$HOME/.android/adbkey.pub" + cat /tmp/jtube-adbkeys/*.pub 2>/dev/null +} > "$VKEYS" +chmod 600 "$VKEYS" + echo "Booting AVD '$AVD' …" -setsid nohup "$EMU" -avd "$AVD" -port "$PORT" -gpu host "${ACCEL[@]:-}" \ +ADB_VENDOR_KEYS="$VKEYS" setsid nohup "$EMU" -avd "$AVD" -port "$PORT" -gpu host "${ACCEL[@]:-}" \ -no-snapshot > /tmp/jtube-emu.log 2>&1 < /dev/null & echo "emulator PID $! — log: /tmp/jtube-emu.log" @@ -79,21 +97,6 @@ for i in $(seq 1 90); do sleep 5 done -# --- authorize remote adb clients headlessly --------------------------------- -# The host console transport is always trusted; use it to whitelist client keys -# so `adb connect rutherford:5556` from other machines works without a popup. -# Drop extra public keys into /tmp/jtube-adbkeys/ (one per file), e.g. from -# Chester: cat ~/.android/adbkey.pub > /tmp/jtube-adbkeys/chester.pub -"$ADB" -s "emulator-$PORT" root > /dev/null 2>&1 || true -for key_file in "$HOME/.android/adbkey.pub" /tmp/jtube-adbkeys/*.pub; do - [ -f "$key_file" ] || continue - "$ADB" -s "emulator-$PORT" push "$key_file" "/data/misc/adb/adb_keys" > /dev/null 2>&1 || \ - "$ADB" -s "emulator-$PORT" shell "cat > /data/misc/adb/adb_keys" < "$key_file" || { - echo "could not whitelist $key_file"; - } -done -"$ADB" -s "emulator-$PORT" shell "setprop service.adb.tcp.port 5601; stop adbd; start adbd" > /dev/null 2>&1 || true - # --- the bridge: advertise adbd on the LAN as :5556 --------------------------- if ! ss -tln 2>/dev/null | grep -q ":5556"; then if command -v socat > /dev/null; then