Watch for MIUI's install dialog and answer it only when its button is on screen
macrobenchmark installs its target before measuring it, and on this phone MIUI puts that install behind com.miui.securitycenter/.permcenter.install.AdbInstallActivity. The test cannot answer it: CompilationMode.installPackageFromPaths runs `pm install -t ...` through Shell.executeScriptCaptureStdout, so the call blocks until the child exits and no test code is running while the dialog is up. This script runs beside the Gradle run and taps the Install button instead. It never taps blindly. Every five seconds it asks who has focus; if that is the installer, it reads the screen and taps the bounds of a button whose label is an install word - or, failing that, MIUI's own button2 id - and if no such button appears within ten seconds it taps nothing and says so. The first version guessed at the dialog's layout when the screen could not be read, which is what made it click whatever happened to be at the guessed coordinates; that code is gone. Reading the screen costs three to four seconds per dialog, which is the price of never pressing the wrong thing. Measured limits, in the script's own comment: for an install started from adb the dialog appears after 1.7 s and counts down from about ten, so a five second poll leaves time to read and tap; for the install macrobenchmark starts inside the test, the window is focused for under a second and is FLAG_SECURE (a screencap of the focused dialog shows the home screen behind it), so that dialog cannot be answered by this script or by a human. The durable fix is the MIUI developer option the dialog is named after, Install via USB.
This commit is contained in:
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Answers MIUI's "Install via USB" dialog while a benchmark run is in flight.
|
||||||
|
#
|
||||||
|
# ./tools/benchmark-watch-install.sh # watch for ten minutes, poll every 5 s
|
||||||
|
# ./tools/benchmark-watch-install.sh --interval 2 # poll more often
|
||||||
|
# ./tools/benchmark-watch-install.sh --dry-run # report, never tap
|
||||||
|
#
|
||||||
|
# Why this exists: macrobenchmark installs its target before measuring it, and on this phone MIUI puts
|
||||||
|
# that install behind `com.miui.securitycenter/.permcenter.install.AdbInstallActivity`. The test cannot
|
||||||
|
# answer it itself — `CompilationMode.installPackageFromPaths` builds `pm install -t …` and runs it
|
||||||
|
# through `Shell.executeScriptCaptureStdout`, so the call blocks until the child exits and no test code
|
||||||
|
# is running while the dialog is up. This script runs beside the Gradle run instead.
|
||||||
|
#
|
||||||
|
# **It never taps blindly.** A dialog is only answered after its Install button has been read off the
|
||||||
|
# screen and its own bounds are known; if the button is not there, nothing is tapped. That costs a
|
||||||
|
# `uiautomator dump` per dialog (3-4 s on this device) but it cannot press whatever happens to sit at a
|
||||||
|
# guessed coordinate.
|
||||||
|
#
|
||||||
|
# Measured on the Xiaomi phone (Android 12, MIUI 14), and both numbers matter:
|
||||||
|
#
|
||||||
|
# * for an install started from adb, the dialog appears after about 1.7 s and its deny button counts
|
||||||
|
# down from about ten seconds — so a 5 s poll still leaves time to read the screen and tap;
|
||||||
|
# * for the install macrobenchmark starts from inside the test, the window is focused for well under
|
||||||
|
# a second (`13:51:27.70 AdbInstallActivity` → `13:51:28.38 null`), which is shorter than one
|
||||||
|
# `uiautomator dump`. Those dialogs will not be caught, by this script or by a human.
|
||||||
|
#
|
||||||
|
# So this is honest about its limit: it makes adb-driven installs unattended, and it will not rescue a
|
||||||
|
# reinstall that MIUI refuses instantly. `--interval` and `--wait` are there for tuning per device.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
seconds=600
|
||||||
|
interval=5
|
||||||
|
wait=10
|
||||||
|
dry_run=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--seconds) seconds=$2; shift 2 ;;
|
||||||
|
--interval) interval=$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] [--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
|
||||||
|
|
||||||
|
# Prints "<x> <y> <label>" for the dialog's Install button, or nothing at all. Anything that would mean
|
||||||
|
# "no" is excluded by name, so a dialog this does not understand is left alone rather than answered
|
||||||
|
# wrongly.
|
||||||
|
install_button() {
|
||||||
|
adb shell uiautomator dump "$device_dump" >/dev/null 2>&1 || return 0
|
||||||
|
adb pull "$device_dump" "$local_dump" >/dev/null 2>&1 || return 0
|
||||||
|
python3 - "$local_dump" <<'PY' || true
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
DENY = ('verweigern', 'deny', 'ablehnen', 'abbrechen', 'cancel', 'nein', 'no')
|
||||||
|
ALLOW = ('installieren', 'install', 'erlauben', 'zulassen', 'allow')
|
||||||
|
|
||||||
|
def centre(node):
|
||||||
|
x1, y1, x2, y2 = (int(v) for v in node.get('bounds').strip('[]').replace('][', ',').split(','))
|
||||||
|
return (x1 + x2) // 2, (y1 + y2) // 2
|
||||||
|
|
||||||
|
for node in ET.parse(sys.argv[1]).getroot().iter('node'):
|
||||||
|
if 'Button' not in node.get('class', ''):
|
||||||
|
continue
|
||||||
|
text = (node.get('text') or '').strip()
|
||||||
|
low = text.lower()
|
||||||
|
if not text or any(low.startswith(d) for d in DENY):
|
||||||
|
continue
|
||||||
|
# MIUI's ids put the allow button at button2, but the name is the deciding factor, so a build that
|
||||||
|
# labels the buttons differently still gets the right one.
|
||||||
|
if any(low.startswith(a) for a in ALLOW) or node.get('resource-id', '').endswith('button2'):
|
||||||
|
x, y = centre(node)
|
||||||
|
print(x, y, text.replace(' ', '_'))
|
||||||
|
break
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
taps=0
|
||||||
|
misses=0
|
||||||
|
deadline=$((SECONDS + seconds))
|
||||||
|
echo "benchmark-watch-install: watching for $seconds s, polling every ${interval}s"
|
||||||
|
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
sleep "$interval"
|
||||||
|
|
||||||
|
focus=$(adb shell dumpsys window 2>/dev/null | grep -m1 'mCurrentFocus' || true)
|
||||||
|
case "$focus" in
|
||||||
|
*securitycenter*install*|*InstallApp*|*packageinstaller*) ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The dialog is up: wait for the Install button to be readable, then click exactly where it is.
|
||||||
|
answered=0
|
||||||
|
for _ in $(seq 1 "$wait"); do
|
||||||
|
read -r button_x button_y button_label <<<"$(install_button)"
|
||||||
|
if [ -n "${button_x:-}" ]; then
|
||||||
|
if [ "$dry_run" -eq 1 ]; then
|
||||||
|
echo " would tap '$button_label' at $button_x,$button_y"
|
||||||
|
else
|
||||||
|
adb shell input tap "$button_x" "$button_y" || true
|
||||||
|
taps=$((taps + 1))
|
||||||
|
echo " tapped '$button_label' at $button_x,$button_y"
|
||||||
|
fi
|
||||||
|
answered=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$answered" -eq 0 ]; then
|
||||||
|
misses=$((misses + 1))
|
||||||
|
echo " an installer window was focused but no Install button appeared within ${wait}s"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "benchmark-watch-install: done, $taps install dialog(s) answered, $misses seen but not answerable"
|
||||||
Reference in New Issue
Block a user