#!/usr/bin/env bash # android-ui [args] # # dump list the on-screen text and content-descriptions # tap tap the element whose content-desc or text matches, exactly once # shot screenshot, but only after confirming is foreground # foreground print the foreground package and exit non-zero if it isn't # # Exists because a blind `adb shell input tap X Y` during this project landed on a completely # different app's settings screen, and the screenshot taken straight afterwards was reported as # the app under test. Coordinates are guesses about a layout; element descriptions are what the # app actually published. Every command here verifies the foreground package first, so a tap can # never silently drive something else. set -uo pipefail ADB=${ADB:-/opt/android-sdk/platform-tools/adb} LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" && pwd)" die() { echo "android-ui: $1" >&2; exit 2; } [ -x "$ADB" ] || die "adb not found at $ADB (set ADB=...)" [ -x "$LIB/uinodes.py" ] || die "missing $LIB/uinodes.py" [ $# -ge 2 ] || die "usage: android-ui [args]" PKG="$1"; CMD="$2"; shift 2 foreground_pkg() { "$ADB" shell dumpsys activity activities 2>/dev/null \ | grep -oE 'topResumedActivity=ActivityRecord\{[^}]*' \ | head -1 \ | grep -oE '[a-zA-Z0-9_.]+/[a-zA-Z0-9_.]+' \ | head -1 \ | cut -d/ -f1 } require_foreground() { local actual actual=$(foreground_pkg) if [ "$actual" != "$PKG" ]; then echo "android-ui: foreground is '${actual:-unknown}', expected '$PKG'" >&2 echo " Nothing was done. Bring the app forward first; a tap here would have driven" >&2 echo " whatever else is on screen." >&2 exit 1 fi } dump_xml() { "$ADB" shell uiautomator dump /sdcard/android-ui.xml >/dev/null 2>&1 \ || die "uiautomator dump failed" "$ADB" shell cat /sdcard/android-ui.xml 2>/dev/null } case "$CMD" in foreground) actual=$(foreground_pkg) echo "foreground: ${actual:-unknown}" echo "expected : $PKG" [ "$actual" = "$PKG" ] && { echo "result : MATCH"; exit 0; } echo "result : MISMATCH"; exit 1 ;; dump) require_foreground dump_xml | "$LIB/uinodes.py" list ;; tap) [ $# -ge 1 ] || die "tap needs a label" require_foreground TARGET="$1" COORDS=$(dump_xml | "$LIB/uinodes.py" find "$TARGET") case "$COORDS" in NONE) echo "android-ui: nothing on screen is labelled '$TARGET'" >&2 echo " Run: android-ui $PKG dump" >&2 exit 1 ;; AMBIGUOUS*) # Tapping the first of several is how you act on the wrong row. echo "android-ui: '$TARGET' matches ${COORDS#AMBIGUOUS } places; refusing to guess" >&2 echo " Run: android-ui $PKG dump" >&2 exit 1 ;; esac "$ADB" shell input tap $COORDS >/dev/null 2>&1 || die "tap failed" echo "tapped '$TARGET' at ($COORDS)" ;; shot) [ $# -ge 1 ] || die "shot needs an output path" require_foreground "$ADB" exec-out screencap -p > "$1" || die "screencap failed" SIZE=$(stat -c%s "$1" 2>/dev/null || echo 0) [ "$SIZE" -gt 1000 ] || die "screenshot is $SIZE bytes; capture failed" echo "captured $1 ($SIZE bytes) with $PKG in the foreground" ;; *) die "unknown command '$CMD'" ;; esac