summaryrefslogtreecommitdiff
path: root/bin/android-ui
blob: 25257f13663f781d9f3d2004c0af60513d97ccca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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