summaryrefslogtreecommitdiff
path: root/bin/android-ui
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-09-21 19:17:27 +0000
committerClaude <noreply@anthropic.com>2026-09-21 19:17:27 +0000
commit8f94e3caa0b1b477bacbbbd8954f2cea743a9337 (patch)
tree75bcbfdb34235dbdad029a63f695b03fa3cd3df0 /bin/android-ui
parentb51ba30f1c77bcadfbf96b2eb7b4917c20faa7b0 (diff)
Add gradle-report, android-ui and publish-apk
Three more wrappers, each from a failure that actually happened while building Listen rather than an imagined one. gradle-report: detekt rejects an invalid config property before analysing anything, exits non-zero, and leaves the previous run's XML in place. That file was read as current findings three times in one session; the tell was line numbers that never moved after a refactor. It now names the rejected property and refuses any report older than the run. Both real shapes of the message are covered -- a misspelled property and structurally invalid YAML say different things. android-ui: a blind `input tap 400 300` landed in FeeDreader's alarm settings, and the screenshot taken immediately after was reported as Listen. Every command verifies the foreground package before acting, and taps resolve by label rather than coordinate. A label matching several elements refuses rather than taking the first -- in a list where every row has the same Play button, the first match is rarely the one meant. The node parsing is a separate file on purpose. The first version inlined it as python3 -c inside a bash case block and the nested quoting mangled the regexes silently: the dump reported one element and a tap landed somewhere else entirely. Shell-quoted parsers are their own trap. publish-apk: publishing is where a silent mistake costs most -- a stale published APK once sat for eight weeks and installing it threw a Room downgrade. It checks the host, proves the file is a real APK rather than an HTML error page, byte-compares what landed against what was tested, and confirms the server serves those same bytes. Test suite is 25 cases, still weighted toward the negative ones: a wrapper that only ever reports PASS is worse than no wrapper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'bin/android-ui')
-rwxr-xr-xbin/android-ui98
1 files changed, 98 insertions, 0 deletions
diff --git a/bin/android-ui b/bin/android-ui
new file mode 100755
index 0000000..25257f1
--- /dev/null
+++ b/bin/android-ui
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+# android-ui <package> <command> [args]
+#
+# dump list the on-screen text and content-descriptions
+# tap <description> tap the element whose content-desc or text matches, exactly once
+# shot <file> screenshot, but only after confirming <package> is foreground
+# foreground print the foreground package and exit non-zero if it isn't <package>
+#
+# 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 <package> <dump|tap|shot|foreground> [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