aboutsummaryrefslogtreecommitdiff
path: root/test-rig/e2e.sh
diff options
context:
space:
mode:
Diffstat (limited to 'test-rig/e2e.sh')
-rwxr-xr-xtest-rig/e2e.sh929
1 files changed, 795 insertions, 134 deletions
diff --git a/test-rig/e2e.sh b/test-rig/e2e.sh
index ae2a1a8..4eaf239 100755
--- a/test-rig/e2e.sh
+++ b/test-rig/e2e.sh
@@ -1,172 +1,833 @@
#!/bin/sh
-# Single-emulator self-mirror E2E.
+# Two-emulator release E2E.
#
-# Boot an AOSP API-35 emulator, root it, install the app, let it
-# generate its keypair, copy that pubkey into /data/misc/adb/adb_keys
-# so the app can connect to its own host's adbd over TCP, launch
-# Mirror against 127.0.0.1:5555, wait for the first video frame in
-# logcat, screencap, assert the frame is non-uniform.
+# The target exposes Android's real Wireless debugging pairing dialog. The
+# source release APK reads that six-digit code through its ordinary UI, pairs
+# through Conscrypt/SPAKE2, mirrors a deterministic moving pattern through the
+# production SurfaceView, reconnects after scrcpy is killed, and reconnects
+# again after adbd rotates its process-scoped TLS certificate.
#
-# Runs inside the test image; orchestrated by ../test.
+# Timings are overridable for slower or faster hosts:
+# E2E_BOOT_DEADLINE, E2E_SETTLE_DEADLINE, E2E_UI_DEADLINE,
+# E2E_ROTATE_DEADLINE, E2E_RESIZE_DEADLINE.
set -eu
ROOT=/work
-# Debug applicationId (note the .debug suffix). Activity classes live in
-# the invalid.lena.scrcpy namespace, which carries no suffix.
-PKG=invalid.lena.scrcpy.debug
-AVD=scrcpy-test
-SERIAL=emulator-5554
-APK=$ROOT/app/build/outputs/apk/debug/app-debug.apk
+SOURCE_AVD=scrcpy-source
+TARGET_AVD=scrcpy-target
+SOURCE_SERIAL=emulator-5554
+TARGET_SERIAL=emulator-5556
+SOURCE_PKG=invalid.lena.scrcpy
+TARGET_PKG=invalid.lena.scrcpy.debug
+SOURCE_APK=$ROOT/app/build/outputs/apk/release/app-release.apk
+TARGET_APK=$ROOT/app/build/outputs/apk/debug/app-debug.apk
TMPDIR=$ROOT/.tools/e2e
+CONNECT_FORWARD=37000
+PAIR_FORWARD=37001
+SYSTEM_IMAGE=system-images/android-36/default/x86_64/
+# Do not reuse or kill a developer's host ADB server. A reused server may
+# have a different key and leave a headless emulator unauthorized.
+export ADB_SERVER_SOCKET=tcp:localhost:5038
+export ANDROID_ADB_SERVER_ADDRESS=localhost
+export ANDROID_ADB_SERVER_PORT=5038
+
mkdir -p "$TMPDIR"
log() { printf 'e2e: %s\n' "$*" >&2; }
-# ---- 0. server jar ----
-[ -f "$ROOT/app/src/main/assets/scrcpy-server.jar" ] || "$ROOT/scripts/update-server"
+deadline() {
+ dl_name=$1
+ dl_value=$2
+ case "$dl_value" in
+ ''|0|*[!0-9]*)
+ log "$dl_name must be a positive integer"
+ exit 2
+ ;;
+ esac
+ printf '%s\n' "$dl_value"
+}
-# ---- 1. build APK ----
-log 'gradle :app:assembleDebug'
-"$ROOT/gradlew" --no-daemon -q :app:assembleDebug
+E2E_BOOT_DEADLINE=$(deadline E2E_BOOT_DEADLINE "${E2E_BOOT_DEADLINE:-300}")
+E2E_SETTLE_DEADLINE=$(deadline E2E_SETTLE_DEADLINE "${E2E_SETTLE_DEADLINE:-240}")
+E2E_UI_DEADLINE=$(deadline E2E_UI_DEADLINE "${E2E_UI_DEADLINE:-120}")
+E2E_ROTATE_DEADLINE=$(deadline E2E_ROTATE_DEADLINE "${E2E_ROTATE_DEADLINE:-45}")
+E2E_RESIZE_DEADLINE=$(deadline E2E_RESIZE_DEADLINE "${E2E_RESIZE_DEADLINE:-60}")
-# ---- 2. AVD + boot ----
-if ! avdmanager list avd | grep -q "Name: $AVD$"; then
- log "creating avd $AVD"
- echo no | avdmanager create avd -n "$AVD" -k 'system-images;android-35;default;x86_64' -d pixel >/dev/null
+free_kb=$(df -Pk "$ROOT/.tools" | awk 'NR == 2 { print $4 }')
+case "$free_kb" in
+ ''|*[!0-9]*) log "cannot determine free space under $ROOT/.tools"; exit 1 ;;
+esac
+if [ "$free_kb" -lt 1048576 ]; then
+ log "need at least 1 GiB free under $ROOT/.tools; have $(( free_kb / 1024 )) MiB"
+ exit 1
fi
-# Clean stale lock files from a previously aborted run; otherwise the
-# emulator refuses to start ("Running multiple emulators with the same AVD").
-AVD_DIR="$ANDROID_AVD_HOME/$AVD.avd"
-rm -f "$AVD_DIR"/multiinstance.lock "$AVD_DIR"/hardware-qemu.ini.lock 2>/dev/null || true
-rm -rf "$HOME/.android/avd/running" 2>/dev/null || true
+stop_emulator() {
+ adb -s "$1" emu kill >/dev/null 2>&1 || true
+}
-if ! pgrep -f "emulator.*-avd $AVD" >/dev/null; then
- log 'booting emulator (no window, no snapshot)'
- emulator -avd "$AVD" -no-window -no-audio -no-snapshot -gpu swiftshader_indirect \
- -no-boot-anim -accel on >"$TMPDIR/emulator.log" 2>&1 &
-fi
-
-log 'wait-for-device'
-adb -s "$SERIAL" wait-for-device
-log 'wait for boot'
-until [ "$(adb -s "$SERIAL" shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do
- sleep 2
-done
+cleanup() {
+ stop_emulator "$SOURCE_SERIAL"
+ stop_emulator "$TARGET_SERIAL"
+ adb kill-server >/dev/null 2>&1 || true
+}
+trap cleanup EXIT INT TERM
-# ---- 3. root (emulator adbd already listens on TCP 5555) ----
-log 'adb root'
-adb -s "$SERIAL" root
-adb -s "$SERIAL" wait-for-device
+wait_boot() {
+ wb_serial=$1
+ wb_pid=${2:-}
+ wb_log=${3:-}
+ # A single API-36 emulator takes about 90 s to boot on an idle
+ # 12-core host, and this tier boots two at once. 120 s left no margin:
+ # on a loaded machine the second one misses it, and even a successful
+ # run was observed finishing at 112 s. Overridable so a fast CI can
+ # tighten it back up.
+ wb_deadline=$(( $(date +%s) + E2E_BOOT_DEADLINE ))
+ while [ "$(date +%s)" -lt "$wb_deadline" ]; do
+ if [ -n "$wb_pid" ] && ! kill -0 "$wb_pid" 2>/dev/null; then
+ if wait "$wb_pid"; then wb_rc=0; else wb_rc=$?; fi
+ log "$wb_serial emulator exited during boot (rc=$wb_rc)"
+ if [ -n "$wb_log" ] && [ -f "$wb_log" ]; then
+ tail -40 "$wb_log" >&2
+ fi
+ exit 1
+ fi
+ if [ "$(adb -s "$wb_serial" get-state 2>/dev/null || true)" = device ]; then
+ wb_done=$(adb -s "$wb_serial" shell getprop sys.boot_completed 2>/dev/null \
+ | tr -d '\r' || true)
+ [ "$wb_done" = 1 ] && return
+ fi
+ sleep 2
+ done
+ log "$wb_serial did not finish booting"
+ exit 1
+}
-# ---- 4. install ----
-log "install $APK"
-adb -s "$SERIAL" install -r "$APK" >/dev/null
+# sys.boot_completed fires while the system is still finishing first-boot
+# work, and this rig passes -wipe-data on every run, so every boot is a
+# full first boot. uiautomator needs an idle window and will not get one
+# until that settles: a dump attempted too early fails for a minute and
+# then gives up. Wait for a focused window before driving any UI.
+wait_ui_ready() {
+ wu_serial=$1
+ wu_deadline=$(( $(date +%s) + E2E_SETTLE_DEADLINE ))
+ while [ "$(date +%s)" -lt "$wu_deadline" ]; do
+ if adb -s "$wu_serial" shell dumpsys window 2>/dev/null \
+ | grep -q 'mCurrentFocus=Window'; then
+ return
+ fi
+ sleep 2
+ done
+ log "$wu_serial never reported a focused window; the system never settled"
+ adb -s "$wu_serial" shell dumpsys window 2>/dev/null | head -20 >&2 || true
+ exit 1
+}
-# ---- 5. let the app generate its keypair ----
-log "launching Main once so Adb.java writes filesDir/adbkey"
-adb -s "$SERIAL" shell am start -W -n "$PKG/invalid.lena.scrcpy.Main" >/dev/null
-# Adb.java writes the keypair inside its constructor, before setContentView
-sleep 1
+wait_wifi() {
+ ww_deadline=$(( $(date +%s) + 90 ))
+ while [ "$(date +%s)" -lt "$ww_deadline" ]; do
+ if adb -s "$TARGET_SERIAL" shell ip -4 addr show wlan0 2>/dev/null \
+ | grep -q ' inet '; then
+ return
+ fi
+ sleep 2
+ done
+ log 'target Wi-Fi never received an address'
+ exit 1
+}
-# ---- 6. extract pubkey and authorise ----
-log 'reading filesDir/adbkey via run-as'
-adb -s "$SERIAL" shell "run-as $PKG cat files/adbkey" > "$TMPDIR/adbkey.der"
-if [ ! -s "$TMPDIR/adbkey.der" ]; then
- log 'adbkey was empty - did Adb.java run?'
- adb -s "$SERIAL" logcat -d -t 200 -s scrcpy-android >&2 || true
+refresh_ui() {
+ ru_serial=$1
+ ru_file=$2
+ ru_tmp=$ru_file.tmp
+ ru_error=$ru_file.error
+ # uiautomator refuses to dump while the window is still animating or
+ # settling, which is exactly the state a freshly booted emulator is in
+ # when the first dump is taken. Animations are already off by here;
+ # this is the remaining margin.
+ ru_deadline=$(( $(date +%s) + E2E_UI_DEADLINE ))
+ ru_n=0
+ while [ "$(date +%s)" -lt "$ru_deadline" ]; do
+ ru_n=$(( ru_n + 1 ))
+ # --compressed drops non-interesting nodes. It succeeds on some
+ # hierarchies where the full dump gives up, and everything this
+ # rig looks for (resource-id, text, bounds) survives compression.
+ if [ $(( ru_n % 2 )) -eq 0 ]; then
+ ru_mode=--compressed
+ else
+ ru_mode=
+ fi
+ if adb -s "$ru_serial" shell uiautomator dump $ru_mode /sdcard/scrcpy-e2e-ui.xml \
+ >"$ru_error" 2>&1 \
+ && adb -s "$ru_serial" exec-out cat /sdcard/scrcpy-e2e-ui.xml \
+ > "$ru_tmp" 2>>"$ru_error" \
+ && grep -q '<hierarchy' "$ru_tmp"; then
+ mv "$ru_tmp" "$ru_file"
+ rm -f "$ru_error"
+ return
+ fi
+ # uiautomator waits for an idle window and gives up if one never
+ # comes. A dozing screen or a stuck animation both cause that, so
+ # nudge the device awake between attempts rather than just waiting.
+ if [ $(( ru_n % 5 )) -eq 0 ]; then
+ adb -s "$ru_serial" shell input keyevent KEYCODE_WAKEUP >/dev/null 2>&1 || true
+ adb -s "$ru_serial" shell wm dismiss-keyguard >/dev/null 2>&1 || true
+ fi
+ sleep 1
+ done
+ log "uiautomator never produced a hierarchy for $ru_serial after $ru_n attempts"
+ adb -s "$ru_serial" shell dumpsys window 2>/dev/null \
+ | grep -E 'mCurrentFocus|mFocusedApp' >&2 || true
+ [ ! -s "$ru_error" ] || tail -20 "$ru_error" >&2
+ rm -f "$ru_tmp" "$ru_error"
+ log "could not capture UI hierarchy from $ru_serial"
exit 1
-fi
+}
+
+find_node() {
+ fn_file=$1
+ fn_needle=$2
+ tr '>' '\n' < "$fn_file" | awk -v needle="$fn_needle" \
+ 'index($0, needle) { print; exit }'
+}
+
+node_attr() {
+ na_node=$1
+ na_attr=$2
+ printf '%s\n' "$na_node" \
+ | sed -n "s/.* ${na_attr}=\"\([^\"]*\)\".*/\1/p"
+}
-log 'computing android pubkey blob'
-java "$ROOT/test-rig/Keygen.java" "$TMPDIR/adbkey.der" > "$TMPDIR/adb_keys.line"
-
-log 'appending to /data/misc/adb/adb_keys'
-adb -s "$SERIAL" push "$TMPDIR/adb_keys.line" /sdcard/adb_keys.line >/dev/null
-adb -s "$SERIAL" shell "cat /sdcard/adb_keys.line >> /data/misc/adb/adb_keys \
- && chown system:shell /data/misc/adb/adb_keys \
- && chmod 640 /data/misc/adb/adb_keys \
- && rm /sdcard/adb_keys.line"
-
-# adbd rereads /data/misc/adb/adb_keys on every AUTH challenge,
-# so no daemon restart is necessary. Restarting it would put the
-# host's adb client into 'offline' state and force a reconnect dance.
-
-# ---- 7. trigger Mirror ----
-log 'starting Mirror -> 127.0.0.1:5555'
-adb -s "$SERIAL" logcat -c
-adb -s "$SERIAL" shell am start -n "$PKG/invalid.lena.scrcpy.Mirror" \
- --es host 127.0.0.1 --ei port 5555 >/dev/null
-
-# ---- 8. wait for first frame, fail on session give-up ----
-# Server's openAbstract budget (Server.OPEN_DEADLINE_MS) must stay
-# shorter than this deadline; otherwise our retries finish before
-# we've given the wire a chance.
-deadline=$(( $(date +%s) + ${E2E_DEADLINE:-60} ))
-ok=
-while [ "$(date +%s)" -lt "$deadline" ]; do
- line=$(adb -s "$SERIAL" logcat -d -s scrcpy-android | tail -200)
- if printf '%s' "$line" | grep -q 'video frame n=1'; then
- ok=1; break
+tap_node() {
+ tn_serial=$1
+ tn_node=$2
+ tn_bounds=$(node_attr "$tn_node" bounds)
+ tn_coords=$(printf '%s\n' "$tn_bounds" | sed -n \
+ 's/^\[\([0-9][0-9]*\),\([0-9][0-9]*\)\]\[\([0-9][0-9]*\),\([0-9][0-9]*\)\]$/\1 \2 \3 \4/p')
+ [ -n "$tn_coords" ] || {
+ log "could not parse UI bounds: $tn_bounds"
+ exit 1
+ }
+ # Split the validated numeric tuple into four shell arguments.
+ # shellcheck disable=SC2086
+ set -- $tn_coords
+ adb -s "$tn_serial" shell input tap $(( ($1 + $3) / 2 )) $(( ($2 + $4) / 2 ))
+}
+
+tap_resource() {
+ tr_serial=$1
+ tr_file=$2
+ tr_id=$3
+ tr_node=$(find_node "$tr_file" "resource-id=\"$tr_id\"")
+ [ -n "$tr_node" ] || {
+ log "resource $tr_id is not visible"
+ exit 1
+ }
+ tap_node "$tr_serial" "$tr_node"
+}
+
+input_resource() {
+ ir_serial=$1
+ ir_file=$2
+ ir_id=$3
+ ir_text=$4
+ tap_resource "$ir_serial" "$ir_file" "$ir_id"
+ adb -s "$ir_serial" shell input text "$ir_text"
+ sleep 1
+ refresh_ui "$ir_serial" "$ir_file"
+}
+
+dismiss_fullscreen_prompt() {
+ df_serial=$1
+ df_file=$2
+ sleep 1
+ refresh_ui "$df_serial" "$df_file"
+ df_button=$(find_node "$df_file" 'text="Got it"')
+ if [ -n "$df_button" ]; then
+ log "dismissing first-use full-screen prompt on $df_serial"
+ tap_node "$df_serial" "$df_button"
+ sleep 1
fi
- if printf '%s' "$line" | grep -q 'session: gave up'; then
- log 'session gave up; logcat:'
- printf '%s\n' "$line" | tail -50 >&2
+}
+
+start_pattern() {
+ sp_file=$1
+ adb -s "$TARGET_SERIAL" shell am force-stop "$TARGET_PKG"
+ if ! sp_start=$(adb -s "$TARGET_SERIAL" shell am start -W -n \
+ "$TARGET_PKG/invalid.lena.scrcpy.Pattern" 2>&1); then
+ log 'target Pattern activity failed to start'
+ printf '%s\n' "$sp_start" >&2
exit 1
fi
+ sp_deadline=$(( $(date +%s) + 15 ))
+ while [ "$(date +%s)" -lt "$sp_deadline" ]; do
+ if adb -s "$TARGET_SERIAL" shell dumpsys activity activities 2>/dev/null \
+ | grep -q 'ResumedActivity.*invalid.lena.scrcpy.Pattern'; then
+ dismiss_fullscreen_prompt "$TARGET_SERIAL" "$sp_file"
+ return
+ fi
+ sleep 1
+ done
+ log 'target Pattern activity did not become resumed'
+ printf '%s\n' "$sp_start" >&2
+ adb -s "$TARGET_SERIAL" shell dumpsys activity activities 2>/dev/null \
+ | grep -E 'ResumedActivity|invalid\.lena\.scrcpy\.Pattern' >&2 || true
+ adb -s "$TARGET_SERIAL" logcat -d -b crash 2>/dev/null | tail -80 >&2 || true
+ exit 1
+}
+
+reopen_source_from_notification() {
+ rn_file=$1
+ rn_node=
+ rn_deadline=$(( $(date +%s) + 15 ))
+ while [ "$(date +%s)" -lt "$rn_deadline" ]; do
+ adb -s "$SOURCE_SERIAL" shell cmd statusbar expand-notifications
+ sleep 1
+ refresh_ui "$SOURCE_SERIAL" "$rn_file"
+ rn_node=$(find_node "$rn_file" 'text="Mirroring active"')
+ [ -n "$rn_node" ] && break
+ done
+ [ -n "$rn_node" ] || {
+ log 'foreground-session notification was not visible'
+ cat "$rn_file" >&2
+ exit 1
+ }
+ tap_node "$SOURCE_SERIAL" "$rn_node"
+}
+
+open_wireless_page() {
+ ow_file=$1
+ ow_started=
+ ow_deadline=$(( $(date +%s) + 15 ))
+ while [ "$(date +%s)" -lt "$ow_deadline" ]; do
+ if adb -s "$TARGET_SERIAL" shell am start -W -a \
+ android.settings.APPLICATION_DEVELOPMENT_SETTINGS >/dev/null 2>&1; then
+ ow_started=1
+ break
+ fi
+ sleep 1
+ done
+ [ -n "$ow_started" ] || {
+ log 'Developer options did not open'
+ exit 1
+ }
+ ow_search=
+ ow_deadline=$(( $(date +%s) + 30 ))
+ while [ "$(date +%s)" -lt "$ow_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$ow_file"
+ [ -n "$(find_node "$ow_file" 'text="Use wireless debugging"')" ] && return
+ ow_search=$(find_node "$ow_file" 'content-desc="Search settings"')
+ [ -n "$ow_search" ] && break
+ sleep 1
+ done
+ [ -n "$ow_search" ] || {
+ log 'Settings search button did not appear'
+ cat "$ow_file" >&2
+ exit 1
+ }
+ tap_node "$TARGET_SERIAL" "$ow_search"
sleep 1
-done
-if [ -z "$ok" ]; then
- log 'timed out waiting for video frame n=1'
- log "full app log: $TMPDIR/logcat.scrcpy"
- adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.scrcpy" 2>&1 || true
- log "full system log: $TMPDIR/logcat.full"
- adb -s "$SERIAL" logcat -d > "$TMPDIR/logcat.full" 2>&1 || true
- log 'last 80 scrcpy-android lines:'
- tail -80 "$TMPDIR/logcat.scrcpy" >&2 || true
+ adb -s "$TARGET_SERIAL" shell input text 'Wireless%sdebugging'
+
+ ow_result=
+ ow_deadline=$(( $(date +%s) + 60 ))
+ while [ "$(date +%s)" -lt "$ow_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$ow_file"
+ # Search returns more than one exact title. Either result opens a
+ # containing Settings page with the matching preference visible.
+ ow_result=$(tr '>' '\n' < "$ow_file" | awk \
+ 'index($0, "text=\"Wireless debugging\"") \
+ && index($0, "resource-id=\"android:id/title\"") { print; exit }')
+ [ -n "$ow_result" ] && break
+ sleep 1
+ done
+ [ -n "$ow_result" ] || {
+ log 'Wireless debugging did not appear in Settings search'
+ cat "$ow_file" >&2
+ exit 1
+ }
+ tap_node "$TARGET_SERIAL" "$ow_result"
+
+ ow_deadline=$(( $(date +%s) + 30 ))
+ while [ "$(date +%s)" -lt "$ow_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$ow_file"
+ [ -n "$(find_node "$ow_file" 'text="Use wireless debugging"')" ] && return
+ ow_pref=$(tr '>' '\n' < "$ow_file" | awk \
+ 'index($0, "text=\"Wireless debugging\"") \
+ && index($0, "resource-id=\"android:id/title\"") { print; exit }')
+ [ -z "$ow_pref" ] || tap_node "$TARGET_SERIAL" "$ow_pref"
+ sleep 1
+ done
+ log 'Wireless debugging page did not open from Settings search'
+ cat "$ow_file" >&2
+ exit 1
+}
+
+enable_wireless() {
+ ew_file=$1
+ refresh_ui "$TARGET_SERIAL" "$ew_file"
+ ew_switch=$(find_node "$ew_file" 'text="Use wireless debugging"')
+ [ -n "$ew_switch" ] || {
+ log 'Wireless debugging switch was not visible on target'
+ cat "$ew_file" >&2
+ exit 1
+ }
+
+ ew_toggle=$(find_node "$ew_file" 'resource-id="android:id/switch_widget"')
+ if [ "$(node_attr "$ew_toggle" checked)" != true ]; then
+ tap_node "$TARGET_SERIAL" "$ew_switch"
+ sleep 1
+ refresh_ui "$TARGET_SERIAL" "$ew_file"
+ ew_allow=$(find_node "$ew_file" 'resource-id="android:id/button1"')
+ if [ -n "$ew_allow" ]; then
+ tap_node "$TARGET_SERIAL" "$ew_allow"
+ fi
+ fi
+
+ ew_enabled=
+ ew_deadline=$(( $(date +%s) + 15 ))
+ while [ "$(date +%s)" -lt "$ew_deadline" ]; do
+ ew_enabled=$(adb -s "$TARGET_SERIAL" shell settings get global adb_wifi_enabled \
+ 2>/dev/null | tr -d '\r' || true)
+ [ "$ew_enabled" = 1 ] && return
+ sleep 1
+ done
+ log "Wireless debugging confirmation did not enable it (state=$ew_enabled)"
+ refresh_ui "$TARGET_SERIAL" "$ew_file"
+ cat "$ew_file" >&2
+ exit 1
+}
+
+source_log() {
+ adb -s "$SOURCE_SERIAL" logcat -d -s scrcpy-android 2>/dev/null || true
+}
+
+target_log() {
+ adb -s "$TARGET_SERIAL" logcat -d -s scrcpy-android 2>/dev/null || true
+}
+
+fail_on_runtime_error() {
+ fo_log=$(source_log)
+ if printf '%s\n' "$fo_log" | grep -q 'session: gave up'; then
+ log 'session gave up:'
+ printf '%s\n' "$fo_log" | tail -80 >&2
+ exit 1
+ fi
+ if adb -s "$SOURCE_SERIAL" logcat -d -b crash 2>/dev/null \
+ | grep -q "$SOURCE_PKG"; then
+ log 'release app crashed:'
+ adb -s "$SOURCE_SERIAL" logcat -d -b crash 2>/dev/null | tail -80 >&2
+ exit 1
+ fi
+ if adb -s "$SOURCE_SERIAL" logcat -d 2>/dev/null \
+ | grep -q "ANR in $SOURCE_PKG"; then
+ log 'release app ANR detected'
+ exit 1
+ fi
+}
+
+# Rotating the target only proves something if the target's display
+# actually turns. Without this the next wait cannot tell a rig that failed
+# to rotate from a client that ignored the new geometry, and the failure
+# reads as a client bug either way.
+target_display() {
+ adb -s "$TARGET_SERIAL" shell dumpsys window displays 2>/dev/null \
+ | grep -o 'cur=[0-9]*x[0-9]*' | head -1 | tr -d '\r'
+}
+
+# Compare against what the display was, not against a literal geometry:
+# the target's size depends on its AVD profile, and asserting a hardcoded
+# 1920x1080 failed on a device that rotates to 1080x606.
+wait_display_change() {
+ wd_before=$1
+ wd_deadline=$(( $(date +%s) + E2E_ROTATE_DEADLINE ))
+ wd_cur=
+ while [ "$(date +%s)" -lt "$wd_deadline" ]; do
+ wd_cur=$(target_display)
+ if [ -n "$wd_cur" ] && [ "$wd_cur" != "$wd_before" ]; then
+ log "target display $wd_before -> $wd_cur"
+ return
+ fi
+ sleep 1
+ done
+ log "target display never moved from ${wd_before:-unknown}"
+ log 'the target did not rotate, so the client was never asked to resize'
+ exit 1
+}
+
+wait_log() {
+ wl_pattern=$1
+ wl_seconds=$2
+ wl_deadline=$(( $(date +%s) + wl_seconds ))
+ while [ "$(date +%s)" -lt "$wl_deadline" ]; do
+ fail_on_runtime_error
+ source_log | grep -q "$wl_pattern" && return
+ sleep 1
+ done
+ log "timed out waiting for log pattern: $wl_pattern"
+ source_log | tail -100 >&2
+ exit 1
+}
+
+wait_target_log() {
+ wt_pattern=$1
+ wt_seconds=$2
+ wt_deadline=$(( $(date +%s) + wt_seconds ))
+ while [ "$(date +%s)" -lt "$wt_deadline" ]; do
+ fail_on_runtime_error
+ target_log | grep -q "$wt_pattern" && return
+ sleep 1
+ done
+ log "timed out waiting for target log pattern: $wt_pattern"
+ target_log | tail -100 >&2
+ log 'source log at target assertion failure:'
+ source_log | tail -100 >&2
exit 1
+}
+
+wait_connect_port() {
+ wc_file=$1
+ wp_deadline=$(( $(date +%s) + 60 ))
+ while [ "$(date +%s)" -lt "$wp_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$wc_file"
+ wp_addr=$(tr '>' '\n' < "$wc_file" | sed -n \
+ 's/.* text="\([0-9][0-9.]*:[0-9][0-9]*\)".*/\1/p' | sed -n '1p')
+ wp_port=${wp_addr##*:}
+ case "$wp_port" in
+ ''|*[!0-9]*) ;;
+ *) [ "$wp_port" -gt 0 ] && [ "$wp_port" -le 65535 ] \
+ && { printf '%s\n' "$wp_port"; return; } ;;
+ esac
+ # In landscape the IP row starts below the visible fold. These
+ # coordinates are inside the Settings list in both orientations.
+ adb -s "$TARGET_SERIAL" shell input swipe 900 950 900 500 250
+ sleep 1
+ done
+ log 'target never displayed a Wireless debugging connect port'
+ wc_state=$(adb -s "$TARGET_SERIAL" shell settings get global adb_wifi_enabled \
+ 2>/dev/null | tr -d '\r' || true)
+ log "adb_wifi_enabled=$wc_state"
+ cat "$wc_file" >&2
+ exit 1
+}
+
+wait_tls_stopped() {
+ ws_deadline=$(( $(date +%s) + 30 ))
+ while [ "$(date +%s)" -lt "$ws_deadline" ]; do
+ ws_port=$(adb -s "$TARGET_SERIAL" shell getprop persist.adb.tls_server.enable \
+ 2>/dev/null | tr -d '\r' || true)
+ case "$ws_port" in
+ ''|0) return ;;
+ esac
+ sleep 1
+ done
+ log 'target Wireless debugging TLS port did not stop'
+ exit 1
+}
+
+# ---- build the exact artifacts under test ----
+
+[ -f "$ROOT/app/src/main/assets/scrcpy-server.jar" ] || "$ROOT/scripts/update-server"
+log 'building debug target fixture and minified release source'
+"$ROOT/gradlew" --no-daemon -q :app:assembleDebug
+KS=$ROOT/.tools/e2e-release.p12
+if [ ! -f "$KS" ]; then
+ keytool -genkey -noprompt -keystore "$KS" -storetype PKCS12 \
+ -storepass changeit -keypass changeit -alias e2e \
+ -keyalg RSA -keysize 2048 -validity 36500 \
+ -dname 'CN=scrcpy-android e2e, O=local, C=US' >/dev/null
fi
-log 'video frame n=1 - proceeding to screencap'
+KEYSTORE_PATH=$KS KEYSTORE_PASS=changeit KEY_ALIAS=e2e KEY_PASS=changeit \
+ "$ROOT/scripts/build-apk" >/dev/null
-# Give the decoder a moment to actually render to the surface.
-sleep 1
+# ---- boot two isolated devices ----
-# ---- 9. screencap + pixhash ----
-adb -s "$SERIAL" exec-out screencap -p > "$TMPDIR/shot.png"
-log "screencap $(wc -c <"$TMPDIR/shot.png") bytes"
-java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/shot.png" --min 5
-
-# ---- 9b. mid-session disconnect -> auto-reconnect ----
-# Kill the scrcpy server on the target out from under the app. The video
-# socket dies; Session.run() must detect it, rerun the bring-up ladder,
-# and a fresh VideoStream must log 'video frame n=1' again. Clear logcat
-# first so the only frame we can match is the post-reconnect one.
-log 'forcing a mid-session disconnect (pkill scrcpy server on target)'
-adb -s "$SERIAL" logcat -c
-adb -s "$SERIAL" shell 'pkill -f com.genymobile.scrcpy.Server' || true
-
-rdeadline=$(( $(date +%s) + ${E2E_RECONNECT_DEADLINE:-60} ))
-relink=
-while [ "$(date +%s)" -lt "$rdeadline" ]; do
- if adb -s "$SERIAL" logcat -d -s scrcpy-android | grep -q 'video frame n=1'; then
- relink=1; break
+ensure_avd() {
+ ea_name=$1
+ ea_config=$ANDROID_AVD_HOME/$ea_name.avd/config.ini
+ if avdmanager list avd | grep -q "Name: $ea_name$"; then
+ if [ -f "$ea_config" ] \
+ && grep -Eq "^image\.sysdir\.[0-9]+ *= *$SYSTEM_IMAGE$" "$ea_config"; then
+ return
+ fi
+ log "recreating stale AVD $ea_name"
+ avdmanager delete avd -n "$ea_name" >/dev/null
fi
+ log "creating AVD $ea_name"
+ echo no | avdmanager create avd -n "$ea_name" \
+ -k 'system-images;android-36;default;x86_64' -d pixel >/dev/null
+}
+
+for e2_avd in "$SOURCE_AVD" "$TARGET_AVD"; do
+ ensure_avd "$e2_avd"
+done
+
+stop_emulator "$SOURCE_SERIAL"
+stop_emulator "$TARGET_SERIAL"
+adb start-server >/dev/null
+sleep 2
+
+for e2_avd in "$SOURCE_AVD" "$TARGET_AVD"; do
+ e2_dir=$ANDROID_AVD_HOME/$e2_avd.avd
+ rm -f "$e2_dir/multiinstance.lock" "$e2_dir/hardware-qemu.ini.lock" \
+ "$e2_dir/snapshot.lock.lock" 2>/dev/null || true
+done
+
+log 'booting source on 5554'
+emulator -avd "$SOURCE_AVD" -port 5554 -no-window -no-audio -no-snapshot \
+ -wipe-data -memory 1536 -no-metrics -gpu swiftshader \
+ -no-boot-anim -accel on \
+ >"$TMPDIR/source-emulator.log" 2>&1 &
+SOURCE_PID=$!
+wait_boot "$SOURCE_SERIAL" "$SOURCE_PID" "$TMPDIR/source-emulator.log"
+wait_ui_ready "$SOURCE_SERIAL"
+
+log 'booting target on 5556'
+emulator -avd "$TARGET_AVD" -port 5556 -no-window -no-audio -no-snapshot \
+ -wipe-data -memory 1536 -no-metrics -gpu swiftshader \
+ -no-boot-anim -accel on \
+ >"$TMPDIR/target-emulator.log" 2>&1 &
+TARGET_PID=$!
+wait_boot "$TARGET_SERIAL" "$TARGET_PID" "$TMPDIR/target-emulator.log"
+wait_ui_ready "$TARGET_SERIAL"
+wait_wifi
+sleep 10
+
+# AOSP may show a one-off first-boot System UI timeout while software graphics
+# settles. Waiting is safe here, before any app or product assertion exists.
+for e2_serial in "$SOURCE_SERIAL" "$TARGET_SERIAL"; do
+ e2_ui=$TMPDIR/boot-${e2_serial}.xml
+ refresh_ui "$e2_serial" "$e2_ui"
+ e2_wait=$(find_node "$e2_ui" 'resource-id="android:id/aerr_wait"')
+ if [ -n "$e2_wait" ]; then
+ tap_node "$e2_serial" "$e2_wait"
+ sleep 5
+ fi
+done
+
+for e2_serial in "$SOURCE_SERIAL" "$TARGET_SERIAL"; do
+ adb -s "$e2_serial" shell settings put global window_animation_scale 0
+ adb -s "$e2_serial" shell settings put global transition_animation_scale 0
+ adb -s "$e2_serial" shell settings put global animator_duration_scale 0
+ adb -s "$e2_serial" shell settings put secure \
+ immersive_mode_confirmations confirmed
+done
+
+# ---- install clean source and target apps ----
+
+adb -s "$SOURCE_SERIAL" uninstall "$SOURCE_PKG" >/dev/null 2>&1 || true
+adb -s "$SOURCE_SERIAL" uninstall "$TARGET_PKG" >/dev/null 2>&1 || true
+adb -s "$TARGET_SERIAL" uninstall "$TARGET_PKG" >/dev/null 2>&1 || true
+adb -s "$SOURCE_SERIAL" install "$SOURCE_APK" >/dev/null
+adb -s "$SOURCE_SERIAL" install "$TARGET_APK" >/dev/null
+adb -s "$TARGET_SERIAL" install "$TARGET_APK" >/dev/null
+adb -s "$SOURCE_SERIAL" shell pm grant "$SOURCE_PKG" \
+ android.permission.POST_NOTIFICATIONS >/dev/null
+
+# ---- open the target's real pairing-code dialog ----
+
+log 'enabling Developer options and opening Wireless debugging'
+adb -s "$TARGET_SERIAL" shell settings put global development_settings_enabled 1
+
+TARGET_UI=$TMPDIR/target-ui.xml
+open_wireless_page "$TARGET_UI"
+enable_wireless "$TARGET_UI"
+CONNECT_PORT=$(wait_connect_port "$TARGET_UI")
+
+pair_pref=
+pair_deadline=$(( $(date +%s) + 30 ))
+while [ "$(date +%s)" -lt "$pair_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$TARGET_UI"
+ pair_pref=$(find_node "$TARGET_UI" 'text="Pair device with pairing code"')
+ [ -n "$pair_pref" ] && break
sleep 1
done
-if [ -z "$relink" ]; then
- log 'timed out waiting for auto-reconnect (no fresh video frame n=1)'
- adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.reconnect" 2>&1 || true
- log "full app log: $TMPDIR/logcat.reconnect"
- tail -80 "$TMPDIR/logcat.reconnect" >&2 || true
+[ -n "$pair_pref" ] || {
+ log 'pairing-code preference was not visible on target'
+ cat "$TARGET_UI" >&2
exit 1
+}
+tap_node "$TARGET_SERIAL" "$pair_pref"
+
+PAIR_CODE=
+PAIR_ADDR=
+code_deadline=$(( $(date +%s) + 30 ))
+while [ "$(date +%s)" -lt "$code_deadline" ]; do
+ refresh_ui "$TARGET_SERIAL" "$TARGET_UI"
+ code_node=$(find_node "$TARGET_UI" 'resource-id="com.android.settings:id/pairing_code"')
+ addr_node=$(find_node "$TARGET_UI" 'resource-id="com.android.settings:id/ip_addr"')
+ PAIR_CODE=$(node_attr "$code_node" text)
+ PAIR_ADDR=$(node_attr "$addr_node" text)
+ [ "${#PAIR_CODE}" -eq 6 ] && [ -n "$PAIR_ADDR" ] && break
+ sleep 1
+done
+case "$PAIR_CODE" in
+ [0-9][0-9][0-9][0-9][0-9][0-9]) ;;
+ *) log "bad pairing code from Settings: $PAIR_CODE"; exit 1 ;;
+esac
+PAIR_PORT=${PAIR_ADDR##*:}
+case "$PAIR_PORT" in
+ ''|*[!0-9]*) log "bad pairing address from Settings: $PAIR_ADDR"; exit 1 ;;
+esac
+log "target connect=$CONNECT_PORT pair=$PAIR_PORT"
+
+adb -s "$TARGET_SERIAL" emu redir del tcp:$CONNECT_FORWARD >/dev/null 2>&1 || true
+adb -s "$TARGET_SERIAL" emu redir del tcp:$PAIR_FORWARD >/dev/null 2>&1 || true
+adb -s "$TARGET_SERIAL" emu redir add "tcp:$CONNECT_FORWARD:$CONNECT_PORT" >/dev/null
+adb -s "$TARGET_SERIAL" emu redir add "tcp:$PAIR_FORWARD:$PAIR_PORT" >/dev/null
+
+# ---- pair through the release app's ordinary form ----
+
+log 'entering pairing data in the source release app'
+adb -s "$SOURCE_SERIAL" logcat -c
+adb -s "$SOURCE_SERIAL" shell am start -W -n "$SOURCE_PKG/.Main" >/dev/null
+SOURCE_UI=$TMPDIR/source-ui.xml
+button_deadline=$(( $(date +%s) + 90 ))
+while [ "$(date +%s)" -lt "$button_deadline" ]; do
+ refresh_ui "$SOURCE_SERIAL" "$SOURCE_UI"
+ button_node=$(find_node "$SOURCE_UI" "resource-id=\"$SOURCE_PKG:id/pair\"")
+ [ "$(node_attr "$button_node" enabled)" = true ] && break
+ sleep 1
+done
+[ "$(node_attr "$button_node" enabled)" = true ] || {
+ log 'source pairing button never became enabled'
+ source_log | tail -80 >&2
+ exit 1
+}
+
+input_resource "$SOURCE_SERIAL" "$SOURCE_UI" \
+ "$SOURCE_PKG:id/device_address" "10.0.2.2:$CONNECT_FORWARD"
+input_resource "$SOURCE_SERIAL" "$SOURCE_UI" \
+ "$SOURCE_PKG:id/pair_port" "$PAIR_FORWARD"
+input_resource "$SOURCE_SERIAL" "$SOURCE_UI" \
+ "$SOURCE_PKG:id/pair_code" "$PAIR_CODE"
+tap_resource "$SOURCE_SERIAL" "$SOURCE_UI" "$SOURCE_PKG:id/pair"
+wait_log 'pair ok host=10.0.2.2' 45
+
+# Put a known moving image on the target before opening the saved row.
+start_pattern "$TARGET_UI"
+adb -s "$SOURCE_SERIAL" shell input keyevent KEYCODE_BACK >/dev/null
+sleep 1
+refresh_ui "$SOURCE_SERIAL" "$SOURCE_UI"
+row_node=$(find_node "$SOURCE_UI" "resource-id=\"$SOURCE_PKG:id/device_label\"")
+if [ -z "$row_node" ]; then
+ adb -s "$SOURCE_SERIAL" shell input swipe 500 1500 500 500 300
+ refresh_ui "$SOURCE_SERIAL" "$SOURCE_UI"
+ row_node=$(find_node "$SOURCE_UI" "resource-id=\"$SOURCE_PKG:id/device_label\"")
fi
-log 'auto-reconnect ok - fresh video frame n=1 after link loss'
+[ -n "$row_node" ] || {
+ log 'paired row did not appear in source UI'
+ cat "$SOURCE_UI" >&2
+ exit 1
+}
+[ "$(node_attr "$row_node" text)" = "10.0.2.2:$CONNECT_FORWARD" ] || {
+ log 'saved-device row contains the wrong endpoint'
+ cat "$SOURCE_UI" >&2
+ exit 1
+}
+tap_node "$SOURCE_SERIAL" "$row_node"
+
+# ---- require TLS, Opus playback, and actual rendered output ----
+
+wait_log 'adb connect ok' 90
+wait_log 'audio meta codec=opus' 30
+wait_log 'audio sink: opus configured' 30
+wait_log 'audio sink: playback started' 30
+wait_log 'video sink: rendered frame n=1' 30
+dismiss_fullscreen_prompt "$SOURCE_SERIAL" "$SOURCE_UI"
+
+sleep 1
+adb -s "$SOURCE_SERIAL" exec-out screencap -p > "$TMPDIR/paired.png"
+java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/paired.png" --pattern
+
+log 'forwarding touch, key, and target clipboard through the control socket'
+adb -s "$SOURCE_SERIAL" logcat -c
+adb -s "$TARGET_SERIAL" logcat -c
+adb -s "$SOURCE_SERIAL" shell input tap 500 1000
+wait_target_log 'pattern: touch up' 15
+wait_target_log 'pattern: clipboard set' 15
+wait_log 'clipboard from target: 10 chars' 15
+adb -s "$SOURCE_SERIAL" shell input keyevent KEYCODE_A
+wait_target_log 'pattern: key up code=29' 15
+
+# Copy text in another source app while Mirror is backgrounded. Android 10+
+# only lets the focused app read the clipboard, so Mirror must read and send
+# it explicitly after regaining focus. Backgrounding also destroys the
+# SurfaceView Surface; resume must rebuild the decoder from cached codec config.
+log 'forwarding source clipboard and requiring Surface recreation'
+adb -s "$SOURCE_SERIAL" logcat -c
+adb -s "$TARGET_SERIAL" logcat -c
+if ! source_pattern_start=$(adb -s "$SOURCE_SERIAL" shell am start -W -n \
+ "$TARGET_PKG/invalid.lena.scrcpy.Pattern" \
+ --es clipboard_text source-e2e 2>&1); then
+ log 'source clipboard fixture failed to start'
+ printf '%s\n' "$source_pattern_start" >&2
+ exit 1
+fi
+wait_log 'mirror: surface destroyed' 30
+dismiss_fullscreen_prompt "$SOURCE_SERIAL" "$SOURCE_UI"
+adb -s "$SOURCE_SERIAL" shell input tap 500 1000
+wait_log 'pattern: clipboard set' 15
+reopen_source_from_notification "$SOURCE_UI"
+wait_log 'mirror: surface created' 30
+wait_log 'clipboard to target: 10 chars' 30
+wait_target_log 'pattern: clipboard from source' 30
+wait_log 'video sink: rendered frame n=1' 45
+adb -s "$SOURCE_SERIAL" exec-out screencap -p > "$TMPDIR/resumed.png"
+java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/resumed.png" --pattern
+sleep 2
+if source_log | grep -q 'session: link lost'; then
+ log 'session dropped after Surface recreation'
+ source_log | tail -100 >&2
+ exit 1
+fi
+
+# A target resize rebuilds MediaCodec. The new decoder must receive the
+# cached codec-config packet that was sent only once at stream startup.
+log 'rotating target and requiring a resized rendered stream'
+adb -s "$SOURCE_SERIAL" logcat -c
+ROTATE_BEFORE=$(target_display)
+log "target display before rotation: ${ROTATE_BEFORE:-unknown}"
+adb -s "$TARGET_SERIAL" shell wm user-rotation lock 1
+wait_display_change "$ROTATE_BEFORE"
+wait_log 'video resize' "$E2E_RESIZE_DEADLINE"
+wait_log 'video sink: rendered frame n=1' 45
+adb -s "$SOURCE_SERIAL" exec-out screencap -p > "$TMPDIR/rotated.png"
+java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/rotated.png" --pattern
+adb -s "$TARGET_SERIAL" logcat -c
+adb -s "$SOURCE_SERIAL" shell input tap 500 1000
+wait_target_log 'pattern: touch up' 15
+
+# ---- kill only scrcpy and require a fresh generation ----
+
+log 'killing target scrcpy server and requiring automatic reconnect'
+adb -s "$SOURCE_SERIAL" logcat -c
+adb -s "$TARGET_SERIAL" shell 'pkill -f com.genymobile.scrcpy.Server' || true
+wait_log 'session: link lost' 45
+wait_log 'spawn server ver=' 60
+wait_log 'video sink: rendered frame n=1' 45
+adb -s "$SOURCE_SERIAL" exec-out screencap -p > "$TMPDIR/reconnected.png"
+java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/reconnected.png" --pattern
-# ---- 10. cleanup ----
-log "full app log: $TMPDIR/logcat.scrcpy"
-adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.scrcpy" 2>&1 || true
-log 'adb emu kill'
-adb -s "$SERIAL" emu kill || true
+# ---- restart adbd and require reconnect with its new ephemeral certificate ----
-log 'e2e: pass'
+log 'restarting target adbd to change its process-scoped TLS identity'
+adb -s "$SOURCE_SERIAL" logcat -c
+adb -s "$TARGET_SERIAL" root >/dev/null 2>&1 || true
+wait_boot "$TARGET_SERIAL"
+adb -s "$TARGET_SERIAL" shell settings put global adb_wifi_enabled 0
+wait_tls_stopped
+open_wireless_page "$TARGET_UI"
+enable_wireless "$TARGET_UI"
+NEW_CONNECT_PORT=$(wait_connect_port "$TARGET_UI")
+adb -s "$TARGET_SERIAL" emu redir del tcp:$CONNECT_FORWARD >/dev/null 2>&1 || true
+adb -s "$TARGET_SERIAL" emu redir add "tcp:$CONNECT_FORWARD:$NEW_CONNECT_PORT" >/dev/null
+start_pattern "$TARGET_UI"
+wait_log 'session: link lost' 90
+wait_log 'adb connect ok' 90
+wait_log 'video sink: rendered frame n=1' 60
+adb -s "$SOURCE_SERIAL" exec-out screencap -p > "$TMPDIR/adbd-restarted.png"
+java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/adbd-restarted.png" --pattern
+source_log > "$TMPDIR/logcat.scrcpy"
+log 'pairing, TLS, Opus, input, two-way clipboard, resize, render, and reconnect: pass'