#!/usr/bin/env bash # # Answers MIUI's "Install via USB" dialog while installs are happening on the device. # # ./tools/benchmark-watch-install.sh # ten minutes, poll every 2 s # ./tools/benchmark-watch-install.sh --interval 5 # poll less often when idle # ./tools/benchmark-watch-install.sh --burst 30 # keep watching fast for 30 s after a dialog # ./tools/benchmark-watch-install.sh --dry-run # report, never tap # # Run this for *every* install, not just the first: a benchmark run installs the test APK and then, per # test, the app under test. Measured the hard way — the first version of this script spent three to four # seconds reading one dialog, and the next dialog opened and closed inside that window. # # How it answers a dialog: # # 1. If the Install button's position for this dialog was measured before (and saved), tap it — but # only after re-checking, immediately beforehand, that an installer window is still focused. That # check is what stops a tap from landing on the home screen once the dialog closes. # 2. Otherwise read the screen (`uiautomator dump`), find the button by label — refusing anything that # means "no" — verify the installer is still focused, then tap its own bounds and save the position # for the dialogs that cannot be read in time. # 3. If no Install button can be read and no position is known, tap nothing and say so. # # After every dialog the script watches in "burst" mode (every 0.4 s instead of the idle interval), # because installs arrive in pairs and the second one used to slip through while the first was read. # # Measured on the Xiaomi phone (Android 12, MIUI 14): an adb-started install raises the dialog after # about 1.7 s with a ten second deny countdown, so there is time for one dump and one tap; the same # dialog is FLAG_SECURE, so a screencap of it shows whatever is behind it. # No `-e`: this is a watcher that has to survive for the whole run, and a single failed adb call must not # end it. It ended silently once before, in the middle of a benchmark run, which is why it looked like it # had "stopped running" — a helper ending in `| head -1` takes a SIGPIPE when `head` exits first, and # `pipefail` plus `-e` turned that into an exit. set -uo pipefail seconds=600 interval=2 burst=30 wait=10 dry_run=0 while [ $# -gt 0 ]; do case "$1" in --seconds) seconds=$2; shift 2 ;; --interval) interval=$2; shift 2 ;; --burst) burst=$2; shift 2 ;; --wait) wait=$2; shift 2 ;; --dry-run) dry_run=1; shift ;; -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "usage: $0 [--seconds N] [--interval N] [--burst N] [--wait N] [--dry-run]" >&2; exit 2 ;; esac done command -v adb >/dev/null || { echo "benchmark-watch-install: adb is not on PATH" >&2; exit 1; } command -v python3 >/dev/null || { echo "benchmark-watch-install: python3 is needed to read the UI dump" >&2; exit 1; } device_dump=/sdcard/.benchmark-watch.xml local_dump=$(mktemp) trap 'rm -f "$local_dump"' EXIT answered_token="" tapped_token="" last_tap_at=-100 opened_windows=0 taps=0 misses=0 focused_window() { adb shell dumpsys window 2>/dev/null | grep -m1 'mCurrentFocus' || true } # Only the installer's own windows count; anything else is left alone. is_installer() { case "$1" in *securitycenter*install*|*InstallApp*|*packageinstaller*) return 0 ;; *) return 1 ;; esac } # The window token, so the same dialog is not answered twice while it closes. window_token() { sed -n 's/.*mCurrentFocus=Window{\([0-9a-f]*\).*/\1/p' <<<"$1" | head -1 || true } # The activity that owns the focused window. Dialogs each get their own window token, so the token # cannot be used to tell "the dialog I read is still up" from "another installer dialog replaced it" — # which is exactly what happens when a run installs twice in quick succession. The activity name is # stable across both, and it is still specific enough that a tap can only ever land on an installer. installer_activity() { sed -n 's/.*u0 \([^ }]*\).*/\1/p' <<<"$1" | head -1 || true } # Prints "