agentsclimarketplace

Android emulator harness

Skill simiancraft/simiancraft-skills/skills/android-emulator-harness

Bring up an Android app in a headless emulator on Linux/WSL and drive it for automated integration testing, the Android analog of Playwright for web. Boots an AVD under KVM, installs an APK (standalone or dev-client+Metro), drives the UI with Maestro (resilient tap-by-label/wait/assert), and gates on logcat + screenshots that come back for inspection. Use for ANY Android/Expo/React-Native project (not just one app) when the task is "run the app on an emulator", "drive the Android UI", "smoke-test a screen", "reproduce a tap-and-crash", "automate an Android flow", or "set up Android UI testing". Project-agnostic base; layer a domain skill on top for specialized inputs (see android-emulator-mask-testing for camera/segmentation). Validated on Linux/WSL with KVM, Maestro 2.x, and JDK 17.From its SKILL.md

Install
npx -y skills add simiancraft/simiancraft-skills --skill android-emulator-harness

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 8 commands, including `sudo gpasswd -a $USER kvm` and 7 more.
  • fetches URLsInstructs the agent to fetch 1 URL, including https://get.maestro.mobile.dev.

SKILL.md

11.4 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it

Android Emulator Harness (headless, WSL/Linux, Maestro-driven)

Specializations (e.g. camera/mask) sit ON TOP of this kernel and override only the parts they must.

Runtime/package manager. Examples use bun/bunx; substitute your own runner (npm/npx, pnpm/pnpm dlx, or yarn) wherever they appear. The Android tooling itself (adb, emulator, Maestro, the JDK) is unaffected.

Environment gotchas (verify once per machine)

  • KVM group. x86/x86_64 emulation needs /dev/kvm; the user must be in the kvm group: sudo gpasswd -a $USER kvm (needs a real terminal for the password). No relogin if you launch under sg kvm -c "...". Symptom if missing: x86_64 emulation currently requires hardware acceleration!.
  • JDK 17 must be the active java for sdkmanager/avdmanager/Maestro. Maestro 2.x aborts on Java 8 with ERROR: Java 17 or higher is required; it reads the java on PATH, so exporting JAVA_HOME alone is not enough; prepend it to PATH. Symptom: bare ~/.maestro/bin/maestro --version fails until you do.
  • EAS builds/artifacts. If eas isn't on PATH, install eas-cli and use it directly, or use the Expo MCP mcp__expo-mcp__* tools if your host provides them.
  • GPU. Under WSL there is usually no GPU passthrough (/dev/dri absent); on native Linux you may have one. Either way the default software GLES is fine for UI/logic; only GPU-compute workloads (e.g. MediaPipe) need the swangle override, which the specialized camera skill covers.
  • Resolve binaries explicitly and put JDK 17 on PATH; don't trust a stale PATH emulator or the system java. This one block gets you to an operating state:
    # ANDROID_HOME varies by OS: Linux (Android Studio default) $HOME/Android/Sdk, macOS $HOME/Library/Android/sdk
    export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
    # JDK 17 home is OS/distro-specific; point this at wherever your JDK 17 lives.
    #   Debian/Ubuntu: /usr/lib/jvm/java-17-openjdk-amd64   macOS: $(/usr/libexec/java_home -v 17)
    export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
    export PATH="$JAVA_HOME/bin:$PATH"          # so Maestro's wrapper sees Java 17
    SDK="$ANDROID_HOME"; EMU="$SDK/emulator/emulator"; ADB="$SDK/platform-tools/adb"
    # modern SDKs install to cmdline-tools/latest/bin; older/hand-installed to cmdline-tools/tools/bin
    CLT="$(ls -d "$SDK"/cmdline-tools/latest/bin 2>/dev/null || ls -d "$SDK"/cmdline-tools/*/bin 2>/dev/null | head -1)"
    SDKMGR="$CLT/sdkmanager"; AVDMGR="$CLT/avdmanager"
    MAESTRO="$HOME/.maestro/bin/maestro"
    # preflight: java -version → 17; $ADB version; $MAESTRO --version → 2.x
    

1. Create an AVD (one-time)

Default to x86_64 (faster under KVM, widest native-lib coverage). Use 32-bit x86 ONLY when a required native lib lacks an x86_64 variant (see android-emulator-mask-testing).

yes | "$SDKMGR" "system-images;android-34;google_apis;x86_64" "platforms;android-34"
# device profile must exist in this SDK's catalog; pixel_3 is safe on older SDKs, pixel_6 on newer
echo no | "$AVDMGR" create avd -n harness -k "system-images;android-34;google_apis;x86_64" -d pixel_6 --force

2. Boot headless under KVM

sg kvm -c "nohup $EMU -avd harness \
  -no-window -no-audio -no-boot-anim -no-snapshot \
  -gpu swiftshader_indirect \
  -accel on -port 5554 > /tmp/emulator.log 2>&1 &"
$ADB wait-for-device
for i in $(seq 1 48); do
  [ "$($ADB shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] && break; sleep 5
done
$ADB shell settings put global window_animation_scale 0   # determinism: kill animations
$ADB shell settings put global transition_animation_scale 0
$ADB shell settings put global animator_duration_scale 0

Snapshot the warmed device once for fast restarts: launch without -no-snapshot, let it boot, then future runs reuse the snapshot instead of cold-booting.

3. Acquire + launch the app

APKs come from: a local ./android/gradlew assembleDebug|Release, or an EAS artifact (eas build:run -p android --latest downloads AND installs to the running emulator; eas build:download --build-id <id> fetches only; the Expo MCP mcp__expo-mcp__build_list works too). Inspect a build's git commit (eas build:list --json) BEFORE diagnosing; a stale binary vs fresh JS is a common false bug.

AAPT2="$(ls "$SDK"/build-tools/*/aapt2 2>/dev/null | sort -V | tail -1)"   # newest installed build-tools
PKG=$("$AAPT2" dump badging app.apk | sed -n "s/package: name='\([^']*\)'.*/\1/p")
$ADB install -r -g app.apk            # -g grants runtime perms (CAMERA, etc.) up front
$ADB shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1

Launch-type fork (check the landing activity): $ADB shell dumpsys activity activities | grep topResumedActivity

  • .MainActivity → standalone, JS embedded. Done.
  • …DevLauncherActivity → an Expo dev build; needs Metro. Start it (bunx expo start in the app dir), adb reverse tcp:8081 tcp:8081, then open the dev-client launch URL. The URL is your app's own custom scheme with the expo-development-client host (not literally expo-development-client://, and not a bare Metro URL): adb shell am start -a android.intent.action.VIEW -d "<your-app-scheme>://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081" (with adb reverse in place use localhost; without it, url-encode http://10.0.2.2:8081). Prefer a preview/standalone build for unattended runs to avoid this entirely.

The dev menu. Open the React Native / Expo developer menu with adb shell input keyevent 82 (or Cmd+M / Ctrl+M); it exposes the performance monitor, the element inspector, and Open DevTools. The menu and React Native DevTools are identical to iOS; see expo-developer-tools for what each item and panel does and how to read it.

Auth/login. Many apps gate the first screen behind login. The first flow must authenticate from env or an out-of-repo secrets file (NEVER hardcode). Pass secrets to Maestro with --env KEY=VALUE (or an env file) and read them in the flow as ${KEY}; drive the login like any other screen and keep creds outside the repo.

4. Drive the UI with Maestro (preferred)

Maestro ($MAESTRO, i.e. ~/.maestro/bin/maestro) is the resilient driver: selects by text/id, waits for elements, retries, screenshots; it is app-agnostic and tests the final bundled binary (no Detox/Appium npm shim inside the app). Install once: curl -Ls "https://get.maestro.mobile.dev" | bash.

Selector best practice for React Native: testID. A testID prop on a component is what Maestro matches via id:, and it's stable across copy changes, localization, and re-layout, unlike visible text. Add testID="preview" in the app, select with { id: "preview" }. Visible-text taps (tapOn: "Dark Office") are fine for quick smoke flows but brittle as a contract. If you can edit the app, prefer testID.

A flow is YAML (appId header, then steps):

# flows/smoke.yaml
appId: com.example.app
---
- launchApp
- assertVisible: "Sign in"          # or your first screen
- tapOn: { id: "preview-toggle" }    # by testID, stable
- tapOn: "Dark Office"               # by visible label, quick but brittle
- assertVisible: { id: "preview" }   # waits, retries
- takeScreenshot: dark-office

Run it (Maestro auto-targets the connected adb device):

$MAESTRO test flows/smoke.yaml --format junit --output /tmp/maestro-report.xml
ls ~/.maestro/tests/*/   # screenshots land here

Finding selectors: $MAESTRO hierarchy prints the live view tree (text + resolved ids); use it to discover what to tap before writing the flow. $MAESTRO studio is the interactive picker. Both beat guessing.

Driving a slider (Android SeekBar). RN sliders back onto a native android.widget.SeekBar, which tapOn selects but can't set a value. Three steps:

  1. Find the track. Parse the SeekBar's own "bounds" from $MAESTRO hierarchy (the "bounds" that immediately precedes "class" : "android.widget.SeekBar"). Tapping the slider's label y can miss the track and just scroll the parent; use the widget's own bounds, not the label's.
  2. Drive the track-center y. adb shell input swipe <x_from> <y> <x_to> <y> 500 (or input tap <x> <y>), with x = x0 + value*(x1-x0) across the track bounds.
  3. Verify. Read the value text back and confirm it changed; re-fetch y after any scroll (it drifts). If a long vertical swipe lands as a tap on a selectable control, re-check that earlier selections survived.

Expo Go caveat: you cannot launchApp a custom appId in Expo Go; use openLink: exp://10.0.2.2:8081 instead (the host-loopback alias from inside the emulator; 127.0.0.1 only reaches host Metro after adb reverse tcp:8081 tcp:8081). A preview/standalone or dev build takes plain launchApp.

Last-resort fallback when Maestro genuinely can't see a custom-rendered (e.g. GL) element: uiautomator dump + parse bounds + adb shell input tap <cx> <cy>. Brittle; use only when hierarchy shows nothing tappable.

5. Assert

  • HARD (gate the run), logcat: clear before the action (adb logcat -c), act, then adb logcat -d | grep -iE "FATAL EXCEPTION|ANR in|refcount < 1|UnsatisfiedLink|<your app's error tags>" must be empty. Also assert expected init lines ARE present.
  • SOFT (agent eyeballs), screenshot: adb exec-out screencap -p > shot.png, then Read it. Don't pixel-diff animated/GPU content; judge structurally + by eye.
  • Keep a per-check artifact bundle (screenshot + logcat slice + pass/fail).

What the emulator CAN and CANNOT validate

  • CAN: no-crash / no-ANR, navigation + layout + UI wiring, form/login flows, effect/feature toggles, deterministic logic.
  • CANNOT (well): real performance/FPS (software GLES under KVM is not representative), real camera/mic/sensor fidelity, GPU-compute features without the specialized GPU override, true network/real-time quality. Those stay device passes.

Teardown

$ADB -s emulator-5554 emu kill

Specializations that layer on this base

  • android-emulator-mask-testing: get a real person in the camera so MediaPipe/ML Kit segmentation runs (32-bit x86 + -gpu swangle_indirect + imagefile: camera feed). Overrides the AVD image, the boot/camera command, and adds mask assertions.
  • (future) audio/voice (LiveKit): mic injection + real-time connectivity; its own empirical gotcha-hunt, same shape.

Source-of-truth priority

Verified local command output on the INSTALLED tooling (-help-*, dumpsys SurfaceFlinger, logcat, screenshots) OUTRANKS docs, which describe whatever version Google currently ships. Re-check on each new machine.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most e2e browser skills give in ~3.1k tokens

Counted across 499 of the 513 authors here whose files we hold, read 2026-09-06

  • Capture screenshots, videos, and traces on failurein 32 of 499, across 23 files
  • Close the browser when donein 22 of 499
  • Interact with elements using snapshot refsin 21 of 499, across 20 files
  • Wait for specific network responses instead of fixed timeoutsin 20 of 499, across 10 files
  • Keep tests independent with no shared statein 19 of 499, across 17 files
  • Use Page Object Model classes to encapsulate page interactionsin 19 of 499, across 9 files
  • Locate elements with data-testid attributesin 19 of 499, across 10 files
  • Quarantine flaky tests with fixme or skipin 17 of 499, across 7 files
  • Upload test artifacts after every CI runin 17 of 499, across 8 files
  • Wait on conditions instead of using fixed sleepsin 17 of 499, across 13 files
  • Clean up test data after each testin 17 of 499, across 16 files
  • Test user-visible behavior, not implementation detailsin 16 of 499, across 10 files

Said here and by no other author read

  • Put JDK 17 first on PATH
  • Resolve SDK binaries by explicit path
  • Default the AVD to x86_64
  • Boot the emulator headless under KVM
  • Wait for boot completion before testing
  • Zero the animation scales for determinism

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.