feat: record the FGS video on the real device, and fix what the take exposed
The declaration's video exists now: store-assets/fgs-demo/foreground-service-demo.mp4, recorded on
the test device (Android 12 / MIUI, 2a2ae895) with INSTALL=0 WARMUP=0, and then checked frame by
frame rather than assumed - the intro at 0 s, the scan starting ("Comparing file signatures...
91/102", 8 s), the shade held open for ~6 s with the live notification ("Finding duplicate photos
/ Looking for visually similar photos... / CANCEL", 13-18 s) while the scan kept running behind
it, and the finished results at the end. Hosting it and pasting the link into the declaration is
what remains.
The script grew two flags, both because an earlier attempt got it wrong:
- WARMUP=0 skips the warm-up and keeps the *cold* scan on camera. A warm scan of this library
takes 5.4 s - less than the notification shade takes to open - so the first phone take filmed an
empty shade, convincingly. The warm-up only ever existed for the emulator's FGS deadline
problem, which a real device does not have.
- INSTALL=0 records with the build already on the device, because MIUI does not allow adb
installs: the command hangs on two on-device prompts (Google Play Protect, then MIUI's own
Installieren) and auto-cancels as INSTALL_FAILED_USER_RESTRICTED if they go unanswered.
Also found by running this against a phone instead of an AVD:
- the trigger is chosen by mode - the intro's "Start scan" when the warm-up is skipped, the
results screen's top-bar "Scan" when it is not - and the lookup result is checked rather than
assumed,
- pacing is a variable now, tightened in the WARMUP=0 path, because the phone's cold scan is only
~20 s and the shade has to be open while it runs,
- the library check counts the demo folder rather than the whole device library: on a phone the
rest of the library is the owner's own photos and is supposed to be there,
- the warm-up became a function and does not run at all under WARMUP=0, and a stray reference to
its `attempt` variable (fatal under `set -u`) is gone.
Step 5 of the checklist now carries the recorded recipe, both flags with the reasons, and two
device facts that were invisible until a phone was attached: MIUI fails every synthetic tap with
INJECT_EVENTS until USB debugging is enabled, and toggling that resets the USB connection onto a
new /dev/bus/usb node. The emulator verdict is compressed to what it is - the platform kills the
app on every scan start there (am_crash ForegroundServiceDidNotStartInTimeException, no am_kill),
with QEMU's -icount clock dilation noted as the one thing that ever worked, and only once.
This commit is contained in:
+119
-72
@@ -145,7 +145,14 @@ checkpoint() { # <label> — one PNG per milestone, so the video can be verified
|
||||
# --------------------------------------------------------------- device and app setup
|
||||
|
||||
say "Device $serial"
|
||||
"${adb[@]}" install -r "$apk" | tail -1
|
||||
if [ "${INSTALL:-1}" = "1" ]; then
|
||||
"${adb[@]}" install -r "$apk" | tail -1
|
||||
else
|
||||
# MIUI refuses shell-initiated installs unless "Install via USB" is on, and answers one of them
|
||||
# with two dialogs (Play Protect, then its own installer) that a script cannot see. So the APK
|
||||
# comes from the device itself: INSTALL=0 records with whatever build is already installed.
|
||||
printf ' install skipped (INSTALL=0); recording with the build already on the device\n'
|
||||
fi
|
||||
# The video is about the foreground service, not about a permission prompt, so grant photo access
|
||||
# up front - the "Photo access needed" banner would otherwise be the first thing a reviewer sees.
|
||||
"${adb[@]}" shell pm grant "$pkg" android.permission.READ_EXTERNAL_STORAGE 2>/dev/null || true
|
||||
@@ -161,14 +168,15 @@ sleep 2
|
||||
"${adb[@]}" shell content call --uri content://media --method scan_file --arg /sdcard/DCIM/foto-dup-demo >/dev/null
|
||||
sleep 2
|
||||
local_files="$(find "$repo/demo-photos/foto-dup-demo" -name '*.jpg' | wc -l)"
|
||||
# `content query` prints "No result found." rather than nothing when the table is empty, so count
|
||||
# the rows instead of the lines of output.
|
||||
indexed="$("${adb[@]}" shell content query --uri content://media/external/images/media --projection _id 2>/dev/null \
|
||||
| grep -c '^Row:' || true)"
|
||||
# Count the demo folder only, not the whole library: on a real phone the rest of the library is the
|
||||
# owner's own photos and is meant to be there. What matters is that the demo group out-ranks it,
|
||||
# which is what tools/make-demo-photos.py's --target-mb is for.
|
||||
indexed="$("${adb[@]}" shell content query --uri content://media/external/images/media --projection relative_path 2>/dev/null \
|
||||
| grep -c 'foto-dup-demo' || true)"
|
||||
indexed="${indexed:-0}"
|
||||
printf ' %s images indexed, %s in the generated library\n' "$indexed" "$local_files"
|
||||
if [ "$indexed" -ne "$local_files" ]; then
|
||||
echo "warn: device library does not match the generated one - the results screen will not match the other captures" >&2
|
||||
printf ' %s demo photos indexed, %s in the generated library\n' "$indexed" "$local_files"
|
||||
if [ "$indexed" -lt "$local_files" ]; then
|
||||
echo "warn: fewer demo photos are indexed than were generated - the results capture may not show the demo group first" >&2
|
||||
fi
|
||||
|
||||
say "Clearing stale notifications, so the shade shows only this app"
|
||||
@@ -196,69 +204,103 @@ sleep 2
|
||||
# process has been alive for a while (classes loaded, JIT warm) and the emulator is not still doing
|
||||
# first-boot work. So warm the app up first, and only start recording once a real scan has been
|
||||
# confirmed - by the notification, for the reason given at fgs_started().
|
||||
if [ "${SETTLE_SECONDS:-150}" != "0" ]; then
|
||||
say "Letting the emulator settle (first-boot work competes for the same CPU)"
|
||||
sleep "${SETTLE_SECONDS:-150}"
|
||||
fi
|
||||
warm_up() {
|
||||
if [ "${SETTLE_SECONDS:-150}" != "0" ]; then
|
||||
say "Letting the emulator settle (first-boot work competes for the same CPU)"
|
||||
sleep "${SETTLE_SECONDS:-150}"
|
||||
fi
|
||||
|
||||
say "Warming the app up (a cold process misses the FGS deadline - see the note in this script)"
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 4 ]; do
|
||||
attempt=$(( attempt + 1 ))
|
||||
say "Warming the app up (a cold process misses the FGS deadline - see the note in this script)"
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 4 ]; do
|
||||
attempt=$(( attempt + 1 ))
|
||||
"${adb[@]}" shell am force-stop "$pkg"
|
||||
"${adb[@]}" shell am start -n "$activity" >/dev/null
|
||||
# Time for the process to load its classes before it is asked to start a service in under five
|
||||
# seconds. This is the difference between the runs that worked and the ones that crashed.
|
||||
sleep "${WARM_SECONDS:-75}"
|
||||
if ! start="$(wait_for 'Start scan' 120)"; then
|
||||
echo "error: the empty state never appeared - right APK installed? photo access granted?" >&2
|
||||
exit 3
|
||||
fi
|
||||
"${adb[@]}" shell input tap $start
|
||||
started=no
|
||||
# dumpsys only while the service starts: every `uiautomator dump` spawns a JVM on the device and
|
||||
# competes for exactly the CPU that the five-second deadline is measured against.
|
||||
for tap_try in 1 2 3; do
|
||||
for _ in $(seq 1 12); do
|
||||
if fgs_started; then started=yes; break; fi
|
||||
sleep 3
|
||||
done
|
||||
if [ "$started" = yes ]; then break; fi
|
||||
printf ' tap %s did not start the service' "$tap_try"
|
||||
# On a slowed emulator the window can still be settling even though the accessibility tree
|
||||
# already lists the button, so the tap goes nowhere. Tapping again is far cheaper than the
|
||||
# cold restart the outer loop would otherwise resort to.
|
||||
if start="$(wait_for 'Start scan' 60)"; then
|
||||
printf ' - tapping again\n'
|
||||
"${adb[@]}" shell input tap $start
|
||||
else
|
||||
printf ' - the intro is gone, so restarting instead\n'
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$started" = yes ]; then
|
||||
printf ' attempt %s: the service started (notification posted)\n' "$attempt"
|
||||
break
|
||||
fi
|
||||
printf ' attempt %s: the service never started\n' "$attempt"
|
||||
"${adb[@]}" logcat -b events -d -t 400 2>/dev/null | grep -m1 'am_crash' || true
|
||||
done
|
||||
if [ "$started" != yes ]; then
|
||||
echo "error: no scan could be started on this emulator - record the video on a real device" >&2
|
||||
exit 4
|
||||
fi
|
||||
}
|
||||
|
||||
# A real device needs none of that warm-up, and skipping it is what keeps the *cold* first scan -
|
||||
# ~20 s over 5,385 photos - as the one on camera. That is not a nicety: a warm scan on this phone
|
||||
# finishes in about five seconds, which is less than the notification shade takes to render, so the
|
||||
# take would film an empty shade. Hence WARMUP=0 for a real device, and the trigger becomes the
|
||||
# intro's own button rather than the results screen's "Scan".
|
||||
trigger_text='Scan'
|
||||
trigger_exact=1
|
||||
# Pacing: the emulator needs a long, dump-free window after the tap because the FGS deadline is the
|
||||
# thing at risk there. A real device does not - the notification is up within a second - and it needs
|
||||
# the opposite, because its cold scan is only ~20 s and the shade has to be open *while it runs*.
|
||||
post_tap_wait=8
|
||||
leave_wait=3
|
||||
shade_wait=6
|
||||
if [ "${WARMUP:-1}" = "0" ]; then
|
||||
say "Warm-up skipped (WARMUP=0): the cold first scan is the one being recorded"
|
||||
post_tap_wait=2
|
||||
leave_wait=1
|
||||
shade_wait=5
|
||||
"${adb[@]}" shell am force-stop "$pkg"
|
||||
"${adb[@]}" shell am start -n "$activity" >/dev/null
|
||||
# Time for the process to load its classes before it is asked to start a service in under five
|
||||
# seconds. This is the difference between the runs that worked and the ones that crashed.
|
||||
sleep "${WARM_SECONDS:-75}"
|
||||
if ! start="$(wait_for 'Start scan' 120)"; then
|
||||
if ! wait_for 'Start scan' 120 >/dev/null; then
|
||||
echo "error: the empty state never appeared - right APK installed? photo access granted?" >&2
|
||||
exit 3
|
||||
fi
|
||||
"${adb[@]}" shell input tap $start
|
||||
started=no
|
||||
# dumpsys only while the service starts: every `uiautomator dump` spawns a JVM on the device and
|
||||
# competes for exactly the CPU that the five-second deadline is measured against.
|
||||
for tap_try in 1 2 3; do
|
||||
for _ in $(seq 1 12); do
|
||||
if fgs_started; then started=yes; break; fi
|
||||
sleep 3
|
||||
done
|
||||
if [ "$started" = yes ]; then break; fi
|
||||
printf ' tap %s did not start the service' "$tap_try"
|
||||
# On a slowed emulator the window can still be settling even though the accessibility tree
|
||||
# already lists the button, so the tap goes nowhere. Tapping again is far cheaper than the
|
||||
# cold restart the outer loop would otherwise resort to.
|
||||
if start="$(wait_for 'Start scan' 60)"; then
|
||||
printf ' - tapping again\n'
|
||||
"${adb[@]}" shell input tap $start
|
||||
else
|
||||
printf ' - the intro is gone, so restarting instead\n'
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$started" = yes ]; then
|
||||
printf ' attempt %s: the service started (notification posted)\n' "$attempt"
|
||||
break
|
||||
fi
|
||||
printf ' attempt %s: the service never started\n' "$attempt"
|
||||
"${adb[@]}" logcat -b events -d -t 400 2>/dev/null | grep -m1 'am_crash' || true
|
||||
done
|
||||
if [ "$started" != yes ]; then
|
||||
echo "error: no scan could be started on this emulator - record the video on a real device" >&2
|
||||
exit 4
|
||||
trigger_text='Start scan'
|
||||
trigger_exact=0
|
||||
else
|
||||
warm_up
|
||||
fi
|
||||
|
||||
# Let the warm-up scan run to *completion* instead of cancelling it: cancelling does not lead back to
|
||||
# the intro, and restarting the app to get there throws away exactly the warmth that keeps the
|
||||
# deadline met - which is how the first recordings ended up with the platform killing the app on
|
||||
# camera. So the recorded scan is triggered the way a returning user triggers one: the top bar's
|
||||
# "Scan", on the results screen.
|
||||
say "Letting the warm-up scan finish"
|
||||
if ! wait_for 'duplicate group' 300 >/dev/null; then
|
||||
wait_for 'No duplicates found' 20 >/dev/null \
|
||||
|| echo "warn: the warm-up scan never reached a result screen" >&2
|
||||
if [ "${WARMUP:-1}" != "0" ]; then
|
||||
# Let the warm-up scan run to *completion* instead of cancelling it: cancelling does not lead
|
||||
# back to the intro, and restarting the app to get there throws away exactly the warmth that
|
||||
# keeps the deadline met - which is how the first recordings ended up with the platform killing
|
||||
# the app on camera. So the recorded scan is triggered the way a returning user triggers one:
|
||||
# the top bar's "Scan", on the results screen.
|
||||
say "Letting the warm-up scan finish"
|
||||
if ! wait_for 'duplicate group' 300 >/dev/null; then
|
||||
wait_for 'No duplicates found' 20 >/dev/null \
|
||||
|| echo "warn: the warm-up scan never reached a result screen" >&2
|
||||
fi
|
||||
checkpoint 0-warmup-results
|
||||
fi
|
||||
checkpoint 0-warmup-results
|
||||
|
||||
say "Recording (screenrecord, at most ${recording_seconds}s)"
|
||||
"${adb[@]}" shell rm -f "$remote"
|
||||
@@ -268,30 +310,35 @@ say "Recording (screenrecord, at most ${recording_seconds}s)"
|
||||
recorder_pid=$!
|
||||
sleep 3
|
||||
|
||||
# The trigger. This is the step the declaration video exists to show. Exact match, because the
|
||||
# results card's "Scanned in 39.7 s" also contains "Scan".
|
||||
if ! scan_button="$(wait_for_exact 'Scan' 30)"; then
|
||||
echo "error: no Scan button on screen to trigger the scan with" >&2
|
||||
# The trigger. This is the step the declaration video exists to show.
|
||||
if [ "$trigger_exact" = 1 ]; then
|
||||
# Exact match, because the results card's "Scanned in 5.4 s" also contains "Scan".
|
||||
scan_button="$(wait_for_exact "$trigger_text" 30)" || true
|
||||
else
|
||||
scan_button="$(wait_for "$trigger_text" 30)" || true
|
||||
fi
|
||||
if [ -z "$scan_button" ]; then
|
||||
echo "error: no '$trigger_text' button on screen to trigger the scan with" >&2
|
||||
exit 3
|
||||
fi
|
||||
printf ' Scan (top bar) at %s\n' "$scan_button"
|
||||
printf ' %s at %s\n' "$trigger_text" "$scan_button"
|
||||
"${adb[@]}" shell input tap $scan_button
|
||||
sleep 8 # deliberately dump-free: this is the deadline window
|
||||
sleep "$post_tap_wait" # deliberately dump-free: this is the deadline window
|
||||
if ! fgs_started; then
|
||||
printf ' the service did not start; the events buffer said:\n'
|
||||
"${adb[@]}" logcat -b events -d -t 400 2>/dev/null | grep -m1 'am_crash' || true
|
||||
echo "warn: no scan running - the video will not show the service" >&2
|
||||
else
|
||||
checkpoint 1-scanning
|
||||
printf ' the service started (notification posted) on a process warmed by attempt %s\n' "$attempt"
|
||||
printf ' the service started (notification posted)\n'
|
||||
fi
|
||||
|
||||
# Away from the app: from here on the service is what keeps the scan alive.
|
||||
sleep 3
|
||||
sleep "$leave_wait"
|
||||
"${adb[@]}" shell input keyevent KEYCODE_HOME
|
||||
sleep 3
|
||||
sleep 2
|
||||
"${adb[@]}" shell cmd statusbar expand-notifications
|
||||
sleep 6
|
||||
sleep "$shade_wait"
|
||||
checkpoint 2-notification
|
||||
printf ' notifications posted by the app: %s\n' "$(notifications_from_app)"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user