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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
|
#!/bin/sh
# Regenerate the F-Droid listing screenshots.
#
# Boots a pixel_6 AVD on the same AOSP API-36 image the e2e tier uses -
# natively 1080x2400, the geometry the tracked PNGs already have - and
# screencaps Main and Settings into
# fastlane/metadata/android/en-US/images/phoneScreenshots/.
#
# The AVD is its own (scrcpy-shots, not scrcpy-test) so a screenshot run
# never perturbs the device state ./test e2e asserts against. Panel size
# comes from the device profile rather than `wm size`: overriding
# geometry on a booted software-GPU emulator ANRs SystemUI, and the ANR
# dialog lands in the capture.
#
# Runs inside the test image; orchestrated by ../test.
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-shots
SERIAL=emulator-5554
APK=$ROOT/app/build/outputs/apk/debug/app-debug.apk
SHOTS=$ROOT/fastlane/metadata/android/en-US/images/phoneScreenshots
TMPDIR=$ROOT/.tools/shots
STAGE=$TMPDIR/output
WIDTH=1080
HEIGHT=2400
SYSTEM_IMAGE=system-images/android-36/default/x86_64/
# Isolate emulator authorization from any ADB server running on the host.
export ADB_SERVER_SOCKET=tcp:localhost:5039
export ANDROID_ADB_SERVER_ADDRESS=localhost
export ANDROID_ADB_SERVER_PORT=5039
mkdir -p "$TMPDIR" "$STAGE" "$SHOTS"
log() { printf 'screenshots: %s\n' "$*" >&2; }
cleanup() {
adb -s "$SERIAL" emu kill >/dev/null 2>&1 || true
adb kill-server >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
wait_boot() {
# A clean API-36 emulator commonly needs more than two minutes on a
# loaded software-rendering host. Match the proven e2e boot margin.
wb_deadline=$(( $(date +%s) + 300 ))
while [ "$(date +%s)" -lt "$wb_deadline" ]; do
if [ "$(adb -s "$SERIAL" get-state 2>/dev/null || true)" = device ]; then
wb_done=$(adb -s "$SERIAL" shell getprop sys.boot_completed \
2>/dev/null | tr -d '\r' || true)
[ "$wb_done" = 1 ] && return
fi
sleep 2
done
log "$SERIAL did not finish booting"
exit 1
}
dump_ui() {
du_file=$1
du_tmp=$du_file.tmp
du_deadline=$(( $(date +%s) + 15 ))
while [ "$(date +%s)" -lt "$du_deadline" ]; do
if adb -s "$SERIAL" shell uiautomator dump /sdcard/scrcpy-shots-ui.xml \
>/dev/null 2>&1 \
&& adb -s "$SERIAL" exec-out cat /sdcard/scrcpy-shots-ui.xml \
> "$du_tmp" 2>/dev/null \
&& grep -q '<hierarchy' "$du_tmp"; then
mv "$du_tmp" "$du_file"
return
fi
sleep 1
done
rm -f "$du_tmp"
log 'could not capture UI hierarchy'
exit 1
}
dismiss_systemui_anr() {
da_ui=$TMPDIR/systemui-anr.xml
dump_ui "$da_ui"
da_node=$(tr '>' '\n' < "$da_ui" | awk \
'index($0, "resource-id=\"android:id/aerr_wait\"") { print; exit }')
[ -n "$da_node" ] || return 0
da_bounds=$(printf '%s\n' "$da_node" \
| sed -n 's/.* bounds="\([^\"]*\)".*/\1/p')
da_coords=$(printf '%s\n' "$da_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 "$da_coords" ] || return 0
# Split the validated numeric tuple into four shell arguments.
# shellcheck disable=SC2086
set -- $da_coords
log 'waiting for first-boot System UI ANR to recover'
adb -s "$SERIAL" shell input tap $(( ($1 + $3) / 2 )) $(( ($2 + $4) / 2 ))
sleep 5
}
# ---- 0. server jar ----
[ -f "$ROOT/app/src/main/assets/scrcpy-server.jar" ] || "$ROOT/scripts/update-server"
# ---- 1. build APK ----
log 'gradle :app:assembleDebug'
"$ROOT/gradlew" --no-daemon -q :app:assembleDebug
# ---- 2. AVD + boot ----
AVD_DIR="$ANDROID_AVD_HOME/$AVD.avd"
if avdmanager list avd | grep -q "Name: $AVD$"; then
if [ ! -f "$AVD_DIR/config.ini" ] \
|| ! grep -Eq "^image\.sysdir\.[0-9]+ *= *$SYSTEM_IMAGE$" \
"$AVD_DIR/config.ini"; then
log "recreating stale avd $AVD"
avdmanager delete avd -n "$AVD" >/dev/null
fi
fi
if ! avdmanager list avd | grep -q "Name: $AVD$"; then
log "creating avd $AVD (pixel_6, ${WIDTH}x${HEIGHT})"
echo no | avdmanager create avd -n "$AVD" -k 'system-images;android-36;default;x86_64' -d pixel_6 >/dev/null
fi
# Clean stale lock files from a previously aborted run; snapshot.lock.lock
# and read-snapshot.txt in particular make the next boot die with
# "a snapshot operation is pending and timeout has expired".
rm -f "$AVD_DIR"/multiinstance.lock "$AVD_DIR"/hardware-qemu.ini.lock \
"$AVD_DIR"/snapshot.lock.lock "$AVD_DIR"/read-snapshot.txt 2>/dev/null || true
rm -rf "$ANDROID_USER_HOME/avd/running" 2>/dev/null || true
adb start-server >/dev/null
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 \
-no-boot-anim -accel on >"$TMPDIR/emulator.log" 2>&1 &
fi
wait_boot
# ---- 3. install ----
log "install $APK"
adb -s "$SERIAL" install -r "$APK" >/dev/null
# Screenshot 1 is the empty-list state; a re-run must not inherit the
# devices.json a previous run left behind.
adb -s "$SERIAL" shell pm clear "$PKG" >/dev/null
# ---- 4. capture ----
# Start from the exported launcher activity. Settings is deliberately not
# exported, so open it through the same button a user taps.
open_settings() {
os_ui=$TMPDIR/settings-button.xml
dump_ui "$os_ui"
os_node=$(tr '>' '\n' < "$os_ui" | awk -v id="$PKG:id/settings" \
'index($0, "resource-id=\"" id "\"") { print; exit }')
[ -n "$os_node" ] || {
log 'settings button was not visible'
cat "$os_ui" >&2
exit 1
}
os_bounds=$(printf '%s\n' "$os_node" \
| sed -n 's/.* bounds="\([^\"]*\)".*/\1/p')
os_coords=$(printf '%s\n' "$os_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 "$os_coords" ] || {
log "could not parse settings-button bounds: $os_bounds"
exit 1
}
# Split the validated numeric tuple into four shell arguments.
# shellcheck disable=SC2086
set -- $os_coords
os_x=$(( ($1 + $3) / 2 ))
os_y=$(( ($2 + $4) / 2 ))
os_deadline=$(( $(date +%s) + 15 ))
while [ "$(date +%s)" -lt "$os_deadline" ]; do
if adb -s "$SERIAL" shell input tap "$os_x" "$os_y" 2>/dev/null; then
return
fi
sleep 1
done
log 'could not tap the settings button'
exit 1
}
shot() {
activity=$1
out=$2
log "capture $activity -> $out"
case "$activity" in
Main)
adb -s "$SERIAL" shell am force-stop "$PKG"
if ! start_output=$(adb -s "$SERIAL" shell am start -W -n \
"$PKG/invalid.lena.scrcpy.Main" 2>&1); then
log 'launcher activity failed to start'
printf '%s\n' "$start_output" >&2
exit 1
fi ;;
SettingsActivity)
open_settings ;;
*)
log "unsupported screenshot activity: $activity"
exit 1 ;;
esac
sleep 4
dismiss_systemui_anr
# Anything else on top - a system dialog, an ANR, a failed launch -
# means the shot is not the screen we asked for. Fail, do not ship it.
focus_line=$(adb -s "$SERIAL" shell dumpsys window \
| sed -n '/mCurrentFocus=/ { p; q; }')
focus=$(printf '%s\n' "$focus_line" \
| sed -n 's/.* \([^ ]*\/[^ }]*\).*/\1/p')
case "$focus" in
"$PKG/invalid.lena.scrcpy.$activity") ;;
*)
log "focused window is '$focus', want $PKG/invalid.lena.scrcpy.$activity"
log "$focus_line"
adb -s "$SERIAL" logcat -d -b crash 2>/dev/null | tail -80 >&2 || true
exit 1 ;;
esac
staged=$STAGE/$out
adb -s "$SERIAL" exec-out screencap -p > "$staged"
[ -s "$staged" ] || { log "$out is empty"; exit 1; }
# Reject anything that is not the panel we expect: a wrong-sized PNG
# means the device profile changed and the shot is unusable.
got=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height -of csv=p=0:s=x "$staged")
[ "$got" = "${WIDTH}x${HEIGHT}" ] || { log "$out is $got, want ${WIDTH}x${HEIGHT}"; exit 1; }
}
shot Main 1.png
shot SettingsActivity 2.png
install -m 0644 "$STAGE/1.png" "$SHOTS/1.png"
install -m 0644 "$STAGE/2.png" "$SHOTS/2.png"
log "wrote $SHOTS/1.png $SHOTS/2.png"
log 'done'
|