android-test: headless emulator default for GUI smokes

Default EMULATOR_HEADLESS=1 (-no-window, QT offscreen) while guest GLES
still runs via SwiftShader. Auto-start AVD when no adb device; WINDOWED=1
for a host window on laptops.
This commit is contained in:
Hernâni Marques 2026-07-17 21:29:52 +02:00
parent 4b729663b3
commit 0c3019822f
No known key found for this signature in database
8 changed files with 320 additions and 59 deletions

View file

@ -156,12 +156,12 @@ STACK=stage ./android-test/run-android-pay-smoke.sh
**User-local emulator (no apt root if SDK already under `$HOME/Android/Sdk`):** **User-local emulator (no apt root if SDK already under `$HOME/Android/Sdk`):**
```bash ```bash
export ANDROID_HOME=$HOME/Android/Sdk # Default = headless (no host window); guest GLES via SwiftShader.
export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH # Prefer the helper — same flags smokes use:
# AVD e.g. TalerWallet34 (google_apis x86_64) ./android-test/start-android-emulator.sh --wait
emulator -avd TalerWallet34 -no-window -gpu swiftshader_indirect -memory 1536 &
adb wait-for-device
./android-test/run-android-pay-smoke.sh ./android-test/run-android-pay-smoke.sh
# Or let smoke auto-start the AVD (AUTO_START_EMULATOR=1).
# Windowed laptop: WINDOWED=1 ./android-test/start-android-emulator.sh --wait
``` ```
**Limits (clementine 2026-07-17):** host RAM ~3.7GiB → emulator often ANRs System UI; **Limits (clementine 2026-07-17):** host RAM ~3.7GiB → emulator often ANRs System UI;

View file

@ -120,6 +120,39 @@ Automation can run on hosts that support **Android only**, **GUI tooling only**
| `AUTO_ANDROID` | `0` / `1` | Run Android wallet automation (adb, APK, emulator/device) | | `AUTO_ANDROID` | `0` / `1` | Run Android wallet automation (adb, APK, emulator/device) |
| `AUTO_GUI` | `0` / `1` | Drive **graphical** UI (uiautomator taps, multi-round). If `0`, deep-link/CLI-only install smoke. | | `AUTO_GUI` | `0` / `1` | Drive **graphical** UI (uiautomator taps, multi-round). If `0`, deep-link/CLI-only install smoke. |
| `AUTO_PLATFORM` | `auto` / `linux` / `macos` / `ios` | Host family (optional override) | | `AUTO_PLATFORM` | `auto` / `linux` / `macos` / `ios` | Host family (optional override) |
| `EMULATOR_HEADLESS` | **`1` (default)** / `0` | No host window (`-no-window`). Server/CI-safe. |
| `WINDOWED` | `0` / `1` | Shortcut: `WINDOWED=1` ⇒ show emulator window (`EMULATOR_HEADLESS=0`) |
| `EMULATOR_GPU` | default **`swiftshader_indirect`** | Guest GLES still runs headless (layout/taps/screenshots). Override `host` only if windowed + real GPU. |
| `AUTO_START_EMULATOR` | **`1` (default)** / `0` | If no adb device, start AVD via `start-android-emulator.sh --wait` |
---
## Headless mode (default)
**Default for all GUI / pay smokes:** no window on the host. Opening an emulator
GUI on servers (or headless CI) is wrong; these scripts therefore start the AVD
with **`-no-window`** and still keep **graphics logic** in the guest:
| Piece | Headless default |
|-------|------------------|
| Host window | **off** (`-no-window`, `QT_QPA_PLATFORM=offscreen`, no `DISPLAY`) |
| Guest GLES | **on** via **SwiftShader** (`-gpu swiftshader_indirect`) |
| UI automation | **on**`uiautomator` dump/tap + `screencap` (no X11 needed) |
| Audio / boot anim | off (`-no-audio -no-boot-anim`) |
```bash
# explicit (same as default)
./start-android-emulator.sh --wait
# or let smoke auto-start headless AVD when no phone is plugged in
STACK=goa ./run-android-gui-smoke.sh
# laptop: show the emulator window
WINDOWED=1 ./start-android-emulator.sh --wait
# or: EMULATOR_HEADLESS=0 EMULATOR_GPU=host ./start-android-emulator.sh --wait
```
Shared code: `lib_android_env.sh` + `start-android-emulator.sh`.
**Semantics:** **Semantics:**
@ -203,6 +236,8 @@ Scripts **today** implement Android only; they should honour:
| `run-android-gui-smoke.sh` | Multi-round GUI drive | | `run-android-gui-smoke.sh` | Multi-round GUI drive |
| `run-goa-gui-chain.sh` | Multi mint/withdraw/pay (gui-workflows port) | | `run-goa-gui-chain.sh` | Multi mint/withdraw/pay (gui-workflows port) |
| `run-android-build-and-smoke.sh` | Build inference fix branch + smoke (`GUI=1` optional) | | `run-android-build-and-smoke.sh` | Build inference fix branch + smoke (`GUI=1` optional) |
| `start-android-emulator.sh` | Start AVD (**headless default**, SwiftShader) |
| `lib_android_env.sh` | Flags, PATH, `android_ensure_device` |
| `lib_ui.py` | Dump/tap/ANR helpers | | `lib_ui.py` | Dump/tap/ANR helpers |
--- ---
@ -233,6 +268,7 @@ Scripts **today** implement Android only; they should honour:
- [x] Minimalvariante: was geht / was nicht (Beschränkungen) - [x] Minimalvariante: was geht / was nicht (Beschränkungen)
- [x] Two stacks (goa / stage) - [x] Two stacks (goa / stage)
- [x] Flags design (`AUTO_ANDROID` / `AUTO_GUI` / platform defaults) - [x] Flags design (`AUTO_ANDROID` / `AUTO_GUI` / platform defaults)
- [x] **Headless emulator default** (`EMULATOR_HEADLESS=1`, SwiftShader)
- [x] Linux default = Android suite only - [x] Linux default = Android suite only
- [x] Inference-branch policy - [x] Inference-branch policy
- [ ] Wrapper enforces flags in every entry script (incremental) - [ ] Wrapper enforces flags in every entry script (incremental)

View file

@ -59,5 +59,16 @@ Canonical notes (minimal variant, **flags**, Linux defaults):
| `AUTO_ANDROID` | `1` | Run Android adb/APK automation | | `AUTO_ANDROID` | `1` | Run Android adb/APK automation |
| `AUTO_GUI` | `1` | Graphical uiautomator drive (else deep-link only) | | `AUTO_GUI` | `1` | Graphical uiautomator drive (else deep-link only) |
| `AUTO_PLATFORM` | `auto``linux` | Host family; macOS can later pair with iOS out of tree | | `AUTO_PLATFORM` | `auto``linux` | Host family; macOS can later pair with iOS out of tree |
| **`EMULATOR_HEADLESS`** | **`1`** | **No host window** (`-no-window`). Server-safe. |
| `EMULATOR_GPU` | `swiftshader_indirect` | Guest GLES still active for UI/taps/screenshots |
| `AUTO_START_EMULATOR` | `1` | Auto-start headless AVD if no adb device |
| `WINDOWED` | `0` | `WINDOWED=1` → show emulator window |
```bash
# headless AVD only (default path for servers)
./start-android-emulator.sh --wait
# windowed (laptops)
WINDOWED=1 ./start-android-emulator.sh --wait
```
See also parent [GIT-AUTOMATION-NOTES.md](../GIT-AUTOMATION-NOTES.md). See also parent [GIT-AUTOMATION-NOTES.md](../GIT-AUTOMATION-NOTES.md).

View file

@ -0,0 +1,153 @@
# shellcheck shell=bash
# Shared Android env for GUI / adb smokes.
# Source: . "$ROOT/lib_android_env.sh"
#
# Defaults favour **servers / CI**: no host window (headless emulator), while the
# guest still runs GLES via SwiftShader so UI layout / uiautomator work.
#
# See GUI-AUTOMATION-NOTES.md → Headless mode
# --- SDK / PATH ---------------------------------------------------------------
export ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
for d in \
"$ANDROID_HOME/platform-tools" \
"$ANDROID_HOME/emulator" \
"$ANDROID_HOME/cmdline-tools/latest/bin"
do
[ -d "$d" ] && case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH" ;; esac
done
export PATH
# AVD path (some hosts keep AVDs under ~/.config/.android)
if [ -z "${ANDROID_AVD_HOME:-}" ]; then
if [ -d "$HOME/.android/avd" ]; then
export ANDROID_AVD_HOME="$HOME/.android/avd"
elif [ -d "$HOME/.config/.android/avd" ]; then
export ANDROID_AVD_HOME="$HOME/.config/.android/avd"
# emulator also looks at ~/.android/avd — soft-link if missing
if [ ! -e "$HOME/.android/avd" ]; then
mkdir -p "$HOME/.android"
ln -sfn "$HOME/.config/.android/avd" "$HOME/.android/avd" 2>/dev/null || true
fi
fi
fi
# --- Platform / feature flags -------------------------------------------------
os=$(uname -s)
case "${AUTO_PLATFORM:-auto}" in
auto)
case "$os" in
Linux) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
Darwin) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
esac
;;
linux|macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
esac
# Headless = default (no X11/Wayland window on host). GUI *logic* still runs in
# the guest via software GPU. Set EMULATOR_HEADLESS=0 or WINDOWED=1 for a window.
: "${EMULATOR_HEADLESS:=1}"
if [ "${WINDOWED:-0}" = "1" ]; then
EMULATOR_HEADLESS=0
fi
export EMULATOR_HEADLESS
# Soft GLES for headless; windowed may override to host/auto if set by user.
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
: "${EMULATOR_GPU:=swiftshader_indirect}"
else
: "${EMULATOR_GPU:=swiftshader_indirect}" # still safe default; host GPU: EMULATOR_GPU=host
fi
export EMULATOR_GPU
: "${EMULATOR_AVD:=TalerWallet34}"
: "${EMULATOR_MEMORY:=1536}"
: "${EMULATOR_CORES:=2}"
# If no adb device: start headless AVD (0 = require phone/manual emulator)
: "${AUTO_START_EMULATOR:=1}"
: "${EMULATOR_BOOT_TIMEOUT:=180}"
: "${EMULATOR_LOG:=/tmp/taler-android-emulator.log}"
android_adb_serial() {
adb devices 2>/dev/null | awk '/\tdevice$/{print $1; exit}'
}
android_has_device() {
[ -n "$(android_adb_serial)" ]
}
# Build emulator argv for current headless/windowed mode.
# Prints args; caller exec/nohup.
android_emulator_args() {
local avd="${1:-$EMULATOR_AVD}"
local args=( -avd "$avd" -gpu "$EMULATOR_GPU" -no-audio -no-boot-anim )
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
args+=( -no-window )
# Avoid Qt trying to open a display on servers
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-offscreen}"
fi
if [ -n "${EMULATOR_MEMORY:-}" ]; then
args+=( -memory "$EMULATOR_MEMORY" )
fi
if [ -n "${EMULATOR_CORES:-}" ]; then
args+=( -cores "$EMULATOR_CORES" )
fi
# extra user flags
# shellcheck disable=SC2206
if [ -n "${EMULATOR_EXTRA_ARGS:-}" ]; then
# intentional word-split for extra flags
args+=( ${EMULATOR_EXTRA_ARGS} )
fi
printf '%s\n' "${args[@]}"
}
# Ensure an adb "device" is online. Optionally start headless emulator.
# Sets SERIAL / ANDROID_SERIAL on success. Returns 0 ok, 3 no device.
android_ensure_device() {
local serial
serial="${SERIAL:-${ANDROID_SERIAL:-}}"
if [ -z "$serial" ]; then
serial=$(android_adb_serial || true)
fi
if [ -n "$serial" ]; then
SERIAL="$serial"
export ANDROID_SERIAL="$serial"
return 0
fi
if [ "${AUTO_START_EMULATOR}" != "1" ]; then
echo "no adb device (AUTO_START_EMULATOR=0 — plug phone or start AVD manually)" >&2
return 3
fi
if ! command -v emulator >/dev/null 2>&1; then
echo "no adb device and emulator binary missing under $ANDROID_HOME" >&2
return 3
fi
echo "no adb device → starting AVD ${EMULATOR_AVD} (headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU})"
local starter
starter="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/start-android-emulator.sh"
if [ ! -x "$starter" ]; then
echo "missing $starter" >&2
return 3
fi
EMULATOR_HEADLESS="$EMULATOR_HEADLESS" EMULATOR_GPU="$EMULATOR_GPU" \
EMULATOR_AVD="$EMULATOR_AVD" EMULATOR_MEMORY="$EMULATOR_MEMORY" \
EMULATOR_CORES="$EMULATOR_CORES" EMULATOR_LOG="$EMULATOR_LOG" \
EMULATOR_BOOT_TIMEOUT="$EMULATOR_BOOT_TIMEOUT" \
"$starter" --wait || return 3
serial=$(android_adb_serial || true)
if [ -z "$serial" ]; then
echo "emulator started but no adb device yet — see $EMULATOR_LOG" >&2
return 3
fi
SERIAL="$serial"
export ANDROID_SERIAL="$serial"
echo "device: $SERIAL (headless=${EMULATOR_HEADLESS})"
return 0
}

View file

@ -12,14 +12,9 @@
# #
set -euo pipefail set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib_android_env.sh
. "$ROOT/lib_android_env.sh"
# Platform flags (GUI-AUTOMATION-NOTES.md). Linux → Android only.
os=$(uname -s)
case "${AUTO_PLATFORM:-auto}" in
auto) case "$os" in Linux|Darwin) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; *) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;; esac ;;
linux|macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
esac
[ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; } [ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; }
if [ "${AUTO_GUI}" != "1" ]; then if [ "${AUTO_GUI}" != "1" ]; then
echo "AUTO_GUI=0 → deep-link smoke only" echo "AUTO_GUI=0 → deep-link smoke only"
@ -46,17 +41,12 @@ STACK="${STACK:-auto}"
SERIAL="${SERIAL:-}" SERIAL="${SERIAL:-}"
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
if ! command -v adb >/dev/null 2>&1; then
[ -x "$HOME/Android/Sdk/platform-tools/adb" ] && export PATH="$HOME/Android/Sdk/platform-tools:$PATH"
fi
command -v adb >/dev/null || { echo "adb missing" >&2; exit 2; } command -v adb >/dev/null || { echo "adb missing" >&2; exit 2; }
if [ -z "$SERIAL" ]; then # Headless default: no host window; guest GLES via SwiftShader (uiautomator).
SERIAL=$(adb devices | awk '/\tdevice$/{print $1; exit}') android_ensure_device || { echo "no adb device (try ./start-android-emulator.sh --wait)" >&2; exit 3; }
fi
[ -n "$SERIAL" ] || { echo "no adb device" >&2; exit 3; }
ADB=(adb -s "$SERIAL") ADB=(adb -s "$SERIAL")
echo "device: $SERIAL mode: GUI (uiautomator)" echo "device: $SERIAL mode: GUI (uiautomator) headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}"
case "$STACK" in case "$STACK" in
auto) auto)

View file

@ -20,15 +20,9 @@
# #
set -euo pipefail set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib_android_env.sh
. "$ROOT/lib_android_env.sh"
# Platform flags (see GUI-AUTOMATION-NOTES.md). Linux default: Android only.
os=$(uname -s)
case "${AUTO_PLATFORM:-auto}" in
auto) case "$os" in Linux) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; Darwin) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; *) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;; esac ;;
linux) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
esac
if [ "${AUTO_ANDROID}" != "1" ]; then if [ "${AUTO_ANDROID}" != "1" ]; then
echo "skipped: AUTO_ANDROID=${AUTO_ANDROID} (platform flags — see GUI-AUTOMATION-NOTES.md)" echo "skipped: AUTO_ANDROID=${AUTO_ANDROID} (platform flags — see GUI-AUTOMATION-NOTES.md)"
exit 0 exit 0
@ -46,28 +40,23 @@ OUT_DIR="${OUT_DIR:-$ROOT/out}"
mkdir -p "$APK_DIR" "$OUT_DIR" mkdir -p "$APK_DIR" "$OUT_DIR"
if ! command -v adb >/dev/null 2>&1; then if ! command -v adb >/dev/null 2>&1; then
if [ -x "$HOME/Android/Sdk/platform-tools/adb" ]; then echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2
export PATH="$HOME/Android/Sdk/platform-tools:$PATH" exit 2
else
echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2
exit 2
fi
fi fi
if [ -z "$SERIAL" ]; then # Default: headless AVD (no host window). WINDOWED=1 / EMULATOR_HEADLESS=0 for UI.
SERIAL=$(adb devices | awk '/\tdevice$/{print $1; exit}') if ! android_ensure_device; then
fi cat >&2 <<EOF
if [ -z "$SERIAL" ]; then No adb device. Options:
cat >&2 <<'EOF'
No adb device. Options (no root for user-local emulator if /dev/kvm is usable):
1) Plug phone with USB debugging 1) Plug phone with USB debugging
2) Start AVD: $HOME/Android/Sdk/emulator/emulator -avd TalerWallet34 -no-window 2) Headless AVD (default): $ROOT/start-android-emulator.sh --wait
3) Then re-run this script 3) Windowed AVD: WINDOWED=1 $ROOT/start-android-emulator.sh --wait
4) AUTO_START_EMULATOR=0 disables auto-start from this smoke
EOF EOF
exit 3 exit 3
fi fi
ADB=(adb -s "$SERIAL") ADB=(adb -s "$SERIAL")
echo "device: $SERIAL" echo "device: $SERIAL emulator_headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}"
# Resolve stack endpoints # Resolve stack endpoints
case "$STACK" in case "$STACK" in

View file

@ -37,19 +37,12 @@ step() { echo; echo "======== $* ========"; }
info() { echo " $*"; } info() { echo " $*"; }
pause() { sleep "${1:-${PAUSE:-0.8}}"; } pause() { sleep "${1:-${PAUSE:-0.8}}"; }
# Platform flags — Linux default Android+GUI (see GUI-AUTOMATION-NOTES.md) # Platform flags + headless emulator defaults (see GUI-AUTOMATION-NOTES.md)
os=$(uname -s) # shellcheck source=lib_android_env.sh
case "${AUTO_PLATFORM:-auto}" in . "$ROOT/lib_android_env.sh"
auto) case "$os" in Linux|Darwin) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;; *) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;; esac ;;
linux|macos) : "${AUTO_ANDROID:=1}"; : "${AUTO_GUI:=1}" ;;
*) : "${AUTO_ANDROID:=0}"; : "${AUTO_GUI:=0}" ;;
esac
[ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; } [ "${AUTO_ANDROID}" = "1" ] || { echo "skipped: AUTO_ANDROID=${AUTO_ANDROID}"; exit 0; }
[ "${AUTO_GUI}" = "1" ] || { echo "skipped: AUTO_GUI=${AUTO_GUI} (chain is GUI)"; exit 0; } [ "${AUTO_GUI}" = "1" ] || { echo "skipped: AUTO_GUI=${AUTO_GUI} (chain is GUI)"; exit 0; }
if ! command -v adb >/dev/null 2>&1; then
[ -x "$HOME/Android/Sdk/platform-tools/adb" ] && export PATH="$HOME/Android/Sdk/platform-tools:$PATH"
fi
command -v adb >/dev/null || die "adb not found" command -v adb >/dev/null || die "adb not found"
command -v python3 >/dev/null || die "python3 required" command -v python3 >/dev/null || die "python3 required"
@ -112,12 +105,11 @@ if [[ ! -f "${EXP_PW_FILE:-}" && "$STACK" = "stage" ]]; then
fi fi
[[ -n "${EXP_PW_FILE:-}" && -f "$EXP_PW_FILE" ]] || die "set EXP_PW_FILE (explorer password)" [[ -n "${EXP_PW_FILE:-}" && -f "$EXP_PW_FILE" ]] || die "set EXP_PW_FILE (explorer password)"
if [[ -z "${ANDROID_SERIAL:-}" ]]; then # Headless AVD by default (no host window). WINDOWED=1 for a visible emulator.
n=$(adb devices | awk 'NR>1 && $2=="device"{c++} END{print c+0}') SERIAL="${SERIAL:-${ANDROID_SERIAL:-}}"
[[ "$n" -ge 1 ]] || die "no adb device (start emulator or plug phone)" android_ensure_device || die "no adb device (./start-android-emulator.sh --wait)"
export ANDROID_SERIAL=$(adb devices | awk 'NR>1 && $2=="device"{print $1; exit}') export ANDROID_SERIAL="$SERIAL"
fi info "ANDROID_SERIAL=$ANDROID_SERIAL STACK=$STACK PKG=$PKG SHOTDIR=$SHOTDIR headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU"
info "ANDROID_SERIAL=$ANDROID_SERIAL STACK=$STACK PKG=$PKG SHOTDIR=$SHOTDIR"
ADB=(adb -s "$ANDROID_SERIAL") ADB=(adb -s "$ANDROID_SERIAL")
adb_sh() { "${ADB[@]}" shell "$@"; } adb_sh() { "${ADB[@]}" shell "$@"; }

View file

@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Start the Taler Android AVD. **Default = headless** (no host window).
# Guest GLES still runs via SwiftShader so uiautomator / screenshots work.
#
# Usage:
# ./start-android-emulator.sh # headless, background
# ./start-android-emulator.sh --wait # headless + wait for boot
# ./start-android-emulator.sh --windowed # show emulator window
# EMULATOR_HEADLESS=0 ./start-android-emulator.sh
# EMULATOR_GPU=host WINDOWED=1 ./start-android-emulator.sh
#
# Env: EMULATOR_AVD EMULATOR_MEMORY EMULATOR_CORES EMULATOR_GPU
# EMULATOR_HEADLESS EMULATOR_LOG EMULATOR_BOOT_TIMEOUT EMULATOR_EXTRA_ARGS
#
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib_android_env.sh
. "$ROOT/lib_android_env.sh"
WAIT=0
for a in "$@"; do
case "$a" in
--wait) WAIT=1 ;;
--windowed| --gui) EMULATOR_HEADLESS=0; export EMULATOR_HEADLESS ;;
--headless) EMULATOR_HEADLESS=1; export EMULATOR_HEADLESS ;;
-h|--help)
sed -n '2,18p' "$0"
exit 0
;;
esac
done
command -v emulator >/dev/null || {
echo "emulator not found (ANDROID_HOME=$ANDROID_HOME)" >&2
exit 2
}
command -v adb >/dev/null || {
echo "adb not found" >&2
exit 2
}
# Already booted?
if serial=$(android_adb_serial); [ -n "$serial" ]; then
boot=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
if [ "$boot" = "1" ]; then
echo "already running: $serial (boot_completed=1) — headless default still applies to *new* starts"
exit 0
fi
fi
# Avoid pkill patterns that match this wrapper's argv (self-kill).
mapfile -t EMU_ARGS < <(android_emulator_args "$EMULATOR_AVD")
echo "starting: emulator ${EMU_ARGS[*]}"
echo " headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU log=$EMULATOR_LOG"
if [ "${EMULATOR_HEADLESS}" = "1" ]; then
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-offscreen}"
# Do not require DISPLAY
unset DISPLAY || true
fi
mkdir -p "$(dirname "$EMULATOR_LOG")"
# Background; do not use pkill -f emulator from callers matching full cmdline
nohup emulator "${EMU_ARGS[@]}" >"$EMULATOR_LOG" 2>&1 &
echo "emulator pid=$! log=$EMULATOR_LOG"
if [ "$WAIT" != "1" ]; then
echo "not waiting (pass --wait for boot). adb devices when ready."
exit 0
fi
echo "waiting for boot (timeout ${EMULATOR_BOOT_TIMEOUT}s)…"
deadline=$((SECONDS + EMULATOR_BOOT_TIMEOUT))
serial=""
while (( SECONDS < deadline )); do
adb wait-for-device 2>/dev/null || true
serial=$(android_adb_serial || true)
if [ -n "$serial" ]; then
boot=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
if [ "$boot" = "1" ]; then
echo "BOOT_OK serial=$serial headless=$EMULATOR_HEADLESS gpu=$EMULATOR_GPU"
exit 0
fi
fi
sleep 2
done
echo "boot timeout — last log:" >&2
tail -30 "$EMULATOR_LOG" >&2 || true
exit 3