|
| 1 | +# shellcheck shell=bash |
| 2 | +# Shared helpers for the Play declaration screencast recorders. |
| 3 | +# Sourced by each declaration folder's record.sh. The sourcing script must set |
| 4 | +# OUTDIR (and may set SERIAL/PKG/SIZE/NOREC) BEFORE sourcing this file. |
| 5 | +set -uo pipefail |
| 6 | + |
| 7 | +SERIAL="${SERIAL:-31071JEHN17531}" # Pixel 7a (lynx) |
| 8 | +PKG="${PKG:-eu.darken.amply}" |
| 9 | +SIZE="${SIZE:-720x1600}" # 1080x2400 scaled 1:1 in aspect |
| 10 | +NOREC="${NOREC:-0}" |
| 11 | +ADB=(adb -s "$SERIAL") |
| 12 | +LIBDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 13 | +UIX="$OUTDIR/ui.xml"; RAW="$OUTDIR/raw.mp4"; CAPS="$OUTDIR/captions.txt" |
| 14 | +mkdir -p "$OUTDIR"; : > "$CAPS" |
| 15 | + |
| 16 | +# ---- ui helpers ------------------------------------------------------------- |
| 17 | +# uiautomator intermittently fails with "could not get idle state" during |
| 18 | +# animations (worse under screenrecord load) and then writes nothing, leaving a |
| 19 | +# stale dump. Retry, and only overwrite $UIX with a dump that has nodes. |
| 20 | +dump() { |
| 21 | + local out |
| 22 | + for _ in 1 2 3 4; do |
| 23 | + if "${ADB[@]}" shell uiautomator dump /sdcard/__ui.xml 2>&1 | grep -q "dumped to"; then |
| 24 | + out="$("${ADB[@]}" exec-out cat /sdcard/__ui.xml)" |
| 25 | + if [ -n "$out" ] && printf '%s' "$out" | grep -q "<node"; then |
| 26 | + printf '%s' "$out" > "$UIX"; return 0 |
| 27 | + fi |
| 28 | + fi |
| 29 | + sleep 0.4 |
| 30 | + done |
| 31 | + [ -s "$UIX" ] && return 1 |
| 32 | + "${ADB[@]}" exec-out cat /sdcard/__ui.xml > "$UIX" 2>/dev/null || true |
| 33 | + return 1 |
| 34 | +} |
| 35 | +_find() { python3 "$LIBDIR/find_node.py" "$@" 2>/dev/null; } |
| 36 | + |
| 37 | +tap() { # tap <needle> [idx] [-c] (-c = substring match) |
| 38 | + local needle="$1" idx="${2:-0}" flag="${3:-}" xy |
| 39 | + for _ in $(seq 1 12); do |
| 40 | + dump |
| 41 | + if [ "$flag" = "-c" ]; then xy=$(_find -c "$UIX" "$needle" "$idx"); else xy=$(_find "$UIX" "$needle" "$idx"); fi |
| 42 | + if [ -n "$xy" ]; then "${ADB[@]}" shell input tap $xy; return 0; fi |
| 43 | + sleep 0.5 |
| 44 | + done |
| 45 | + echo " ! tap not found: '$needle'" >&2; return 1 |
| 46 | +} |
| 47 | +have() { # have <needle> [-c] -> 0 if present on screen |
| 48 | + local needle="$1" flag="${2:-}" |
| 49 | + dump |
| 50 | + if [ "$flag" = "-c" ]; then _find -c "$UIX" "$needle" >/dev/null; else _find "$UIX" "$needle" >/dev/null; fi |
| 51 | +} |
| 52 | +swipe_up() { "${ADB[@]}" shell input swipe 540 1900 540 900 500; } # scroll down a list |
| 53 | +swipe_down() { "${ADB[@]}" shell input swipe 540 900 540 1900 500; } |
| 54 | +back() { "${ADB[@]}" shell input keyevent BACK; } |
| 55 | +pause() { sleep "${1:-1.6}"; } |
| 56 | +shade_open() { "${ADB[@]}" shell cmd statusbar expand-notifications; } |
| 57 | +shade_close() { "${ADB[@]}" shell cmd statusbar collapse; } |
| 58 | +# Captions may contain a literal "\n" to force a line break; postprocess.sh expands it. |
| 59 | +# Keep each line under ~34 characters or it overflows the 720px frame. |
| 60 | +# NOTE: the caption text goes through the environment, not `awk -v`. awk expands |
| 61 | +# escape sequences in -v assignments, which would turn the literal "\n" into a real |
| 62 | +# newline and break the one-caption-per-line format postprocess.sh parses. |
| 63 | +cap() { |
| 64 | + local now; now=$(date +%s.%N) |
| 65 | + CAP_TEXT="$1" awk -v a="$now" -v b="$REC_T0" 'BEGIN{printf "%.2f|%s\n", a-b, ENVIRON["CAP_TEXT"]}' >> "$CAPS" |
| 66 | +} |
| 67 | + |
| 68 | +# ---- privacy hygiene for the recording -------------------------------------- |
| 69 | +# The finished video is uploaded to Google. Anything in the notification shade or |
| 70 | +# the quick-settings strip is in frame: other apps' notifications, the Wi-Fi SSID, |
| 71 | +# the user's location and language. Clear and mute it for the take, restore after. |
| 72 | +notif_clear() { |
| 73 | + shade_open; pause 1.2 |
| 74 | + tap "Clear all" >/dev/null 2>&1 || true |
| 75 | + pause 1 |
| 76 | + shade_close; pause 1 |
| 77 | +} |
| 78 | +dnd() { "${ADB[@]}" shell cmd notification set_dnd "$1" >/dev/null 2>&1; } # priority|off |
| 79 | +wifi() { "${ADB[@]}" shell svc wifi "$1" >/dev/null 2>&1; } # disable|enable |
| 80 | + |
| 81 | +# ---- battery simulation ----------------------------------------------------- |
| 82 | +# Only the battery *readings* are driven here — the service, the restore decision |
| 83 | +# and the settings write are the real app on real hardware. The current recording |
| 84 | +# uses unplug/reset only (pulling the cable of the device running the screen |
| 85 | +# capture is not an option); the level/status helpers exist for takes that need to |
| 86 | +# demonstrate the 100% stop condition instead. |
| 87 | +# BATTERY_STATUS_CHARGING=2, BATTERY_STATUS_FULL=5. |
| 88 | +bat_level() { "${ADB[@]}" shell dumpsys battery set level "$1" >/dev/null; } |
| 89 | +bat_status() { "${ADB[@]}" shell dumpsys battery set status "$1" >/dev/null; } |
| 90 | +bat_plug() { "${ADB[@]}" shell dumpsys battery set usb 1 >/dev/null; } |
| 91 | +bat_unplug() { "${ADB[@]}" shell dumpsys battery unplug >/dev/null; } |
| 92 | +bat_reset() { "${ADB[@]}" shell dumpsys battery reset >/dev/null; } |
| 93 | + |
| 94 | +# ---- charge policy readback (Pixel) ----------------------------------------- |
| 95 | +# charge_optimization_mode: 1 = the 80% limit, 0 = unrestricted/off. |
| 96 | +policy_mode() { "${ADB[@]}" shell settings get secure charge_optimization_mode | tr -d '\r'; } |
| 97 | + |
| 98 | +# ---- app helpers ------------------------------------------------------------ |
| 99 | +app_launch() { "${ADB[@]}" shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; } |
| 100 | +app_stop() { "${ADB[@]}" shell am force-stop "$PKG"; } |
| 101 | +screen_wake() { |
| 102 | + "${ADB[@]}" shell input keyevent KEYCODE_WAKEUP |
| 103 | + "${ADB[@]}" shell wm dismiss-keyguard >/dev/null 2>&1 |
| 104 | +} |
| 105 | + |
| 106 | +# ---- recording control ------------------------------------------------------ |
| 107 | +rec_start() { |
| 108 | + if [ "$NOREC" = "0" ]; then |
| 109 | + echo "Recording → $RAW" |
| 110 | + "${ADB[@]}" shell rm -f /sdcard/__demo.mp4 |
| 111 | + "${ADB[@]}" shell screenrecord --size "$SIZE" --bit-rate 8000000 --time-limit 180 /sdcard/__demo.mp4 & |
| 112 | + REC_CLIENT=$! |
| 113 | + sleep 1.2 |
| 114 | + else |
| 115 | + echo "NOREC=1 → validating taps without recording" |
| 116 | + fi |
| 117 | + REC_T0=$(date +%s.%N) |
| 118 | +} |
| 119 | +rec_stop() { |
| 120 | + if [ "$NOREC" = "0" ]; then |
| 121 | + "${ADB[@]}" shell pkill -INT screenrecord 2>/dev/null || true |
| 122 | + wait "$REC_CLIENT" 2>/dev/null || true |
| 123 | + sleep 1 |
| 124 | + "${ADB[@]}" pull /sdcard/__demo.mp4 "$RAW" >/dev/null && echo "Saved $RAW" |
| 125 | + fi |
| 126 | + echo "Captions:"; cat "$CAPS" |
| 127 | +} |
0 commit comments