openclaw/scripts/android-screenshots.sh
Josh Avant cd2724e432
feat(release): automate iOS and Android store releases (#158807)
Add manually triggered iOS Store Release and Android Store Release workflows with the same local release commands. Derive store versions and OpenAI release notes at build time, retain recovery artifacts, and avoid generated metadata commits.

Pin queued releases to their dispatch commit, qualify native iOS behavior before signing, and improve Android screenshots and interrupted artifact recovery.

Validation includes focused release/planner tests, workflow and type checks, native iOS qualification, Android screenshot capture, and historical release-note exercises. CI failure attribution and remaining hosted release verification are documented in the PR.

Closes #158806
2026-09-26 06:28:29 -05:00

777 lines
25 KiB
Bash

#!/usr/bin/env bash
# Bash 5.3+ can deadlock writing heredoc pipes on macOS before the reader starts.
if [[ ${OSTYPE:-} == darwin* && $BASH != /bin/bash ]] && ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3))); then
exec /bin/bash "$0" "$@"
fi
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/android-screenshots.sh [--form-factor all|phone|wear] [--device <adb-serial>] [--avd <name>] [--locale en-US] [--skip-build] [--skip-install] [--keep-emulator] [--dry-run]
Builds and installs the phone and Wear OS debug apps on matching emulators,
launches production screens with deterministic local fixture state, and writes
Google Play screenshots under:
apps/android/fastlane/metadata/android/<locale>/images/phoneScreenshots/
apps/android/fastlane/metadata/android/<locale>/images/wearScreenshots/
Capture evidence is saved under:
.artifacts/android-screenshots/latest/
By default, the script captures both form factors using retained Pixel 2 and
Wear OS Large Round AVDs. Use --form-factor with --avd or --device to capture
one form factor on an explicitly selected emulator.
EOF
}
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ANDROID_DIR="${ROOT_DIR}/apps/android"
DEFAULT_SCREENSHOT_AVD="OpenClaw_Screenshots_API36"
DEFAULT_SCREENSHOT_DEVICE_PROFILE="pixel_2"
DEFAULT_WEAR_SCREENSHOT_AVD="OpenClaw_Wear_Screenshots_API34"
DEFAULT_WEAR_SCREENSHOT_DEVICE_PROFILE="wearos_large_round"
case "$(uname -m)" in
arm64|aarch64) DEFAULT_SCREENSHOT_ABI="arm64-v8a" ;;
*) DEFAULT_SCREENSHOT_ABI="x86_64" ;;
esac
DEFAULT_SCREENSHOT_SYSTEM_IMAGE="system-images;android-36;google_apis;${DEFAULT_SCREENSHOT_ABI}"
DEFAULT_WEAR_SCREENSHOT_SYSTEM_IMAGE="system-images;android-34;android-wear;${DEFAULT_SCREENSHOT_ABI}"
FORM_FACTOR="${ANDROID_SCREENSHOT_FORM_FACTOR:-all}"
LOCALE="en-US"
DEVICE="${ANDROID_SCREENSHOT_DEVICE:-}"
AVD="${ANDROID_SCREENSHOT_AVD:-$DEFAULT_SCREENSHOT_AVD}"
SCREENSHOT_DEVICE_PROFILE="${ANDROID_SCREENSHOT_DEVICE_PROFILE:-$DEFAULT_SCREENSHOT_DEVICE_PROFILE}"
SCREENSHOT_SYSTEM_IMAGE="${ANDROID_SCREENSHOT_SYSTEM_IMAGE:-$DEFAULT_SCREENSHOT_SYSTEM_IMAGE}"
KEEP_EMULATOR="${ANDROID_SCREENSHOT_KEEP_EMULATOR:-0}"
SKIP_BUILD=0
SKIP_INSTALL=0
DRY_RUN=0
SCENES=(home chat settings gateway voice-wake)
OUTPUT_TYPE="phoneScreenshots"
GRADLE_ASSEMBLE_TASK=":app:assemblePlayDebug"
APP_PACKAGE="ai.openclaw.app.debug"
ACTIVITY_COMPONENT="$APP_PACKAGE/ai.openclaw.app.MainActivity"
EMULATOR_PID=""
EMULATOR_LOG=""
STARTED_EMULATOR=0
ARTIFACT_ROOT="${ROOT_DIR}/.artifacts/android-screenshots/latest"
ARTIFACT_DIR="${ARTIFACT_ROOT}/phone"
SCREENSHOT_SIZE="${ANDROID_SCREENSHOT_SIZE:-1440x2560}"
DISPLAY_OVERRIDDEN=0
ORIGINAL_WM_SIZE=""
ORIGINAL_WM_DENSITY=""
SCREENSHOT_DENSITY=""
TIMEZONE_OVERRIDDEN=0
ORIGINAL_AUTO_TIME_ZONE=""
ORIGINAL_TIME_ZONE=""
DEVICE_EXPLICIT=0
AVD_EXPLICIT=0
while [[ $# -gt 0 ]]; do
case "$1" in
--)
shift
;;
--form-factor)
FORM_FACTOR="${2:-}"
shift 2
;;
--device)
DEVICE="${2:-}"
DEVICE_EXPLICIT=1
shift 2
;;
--avd)
AVD="${2:-}"
AVD_EXPLICIT=1
shift 2
;;
--locale)
LOCALE="${2:-}"
shift 2
;;
--skip-build)
SKIP_BUILD=1
shift
;;
--skip-install)
SKIP_INSTALL=1
shift
;;
--keep-emulator)
KEEP_EMULATOR=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
case "$FORM_FACTOR" in
all)
if [[ "$DEVICE_EXPLICIT" == "1" || "$AVD_EXPLICIT" == "1" ]]; then
echo "--device and --avd require --form-factor phone or --form-factor wear." >&2
exit 1
fi
if [[ "$KEEP_EMULATOR" == "1" ]]; then
echo "--keep-emulator requires --form-factor phone or --form-factor wear." >&2
exit 1
fi
child_args=(--locale "$LOCALE")
[[ "$SKIP_BUILD" == "1" ]] && child_args+=(--skip-build)
[[ "$SKIP_INSTALL" == "1" ]] && child_args+=(--skip-install)
[[ "$DRY_RUN" == "1" ]] && child_args+=(--dry-run)
bash "$0" --form-factor phone "${child_args[@]}"
bash "$0" --form-factor wear "${child_args[@]}"
exit 0
;;
phone)
;;
wear)
if [[ "$DEVICE_EXPLICIT" != "1" ]]; then
DEVICE="${ANDROID_WEAR_SCREENSHOT_DEVICE:-}"
fi
if [[ "$AVD_EXPLICIT" != "1" ]]; then
AVD="${ANDROID_WEAR_SCREENSHOT_AVD:-$DEFAULT_WEAR_SCREENSHOT_AVD}"
fi
SCREENSHOT_DEVICE_PROFILE="${ANDROID_WEAR_SCREENSHOT_DEVICE_PROFILE:-$DEFAULT_WEAR_SCREENSHOT_DEVICE_PROFILE}"
SCREENSHOT_SYSTEM_IMAGE="${ANDROID_WEAR_SCREENSHOT_SYSTEM_IMAGE:-$DEFAULT_WEAR_SCREENSHOT_SYSTEM_IMAGE}"
SCREENSHOT_SIZE="${ANDROID_WEAR_SCREENSHOT_SIZE:-454x454}"
SCENES=(chat voice controls)
OUTPUT_TYPE="wearScreenshots"
GRADLE_ASSEMBLE_TASK=":wear:assembleDebug"
ACTIVITY_COMPONENT="$APP_PACKAGE/ai.openclaw.wear.MainActivity"
ARTIFACT_DIR="${ARTIFACT_ROOT}/wear"
;;
*)
echo "Invalid Android screenshot form factor: ${FORM_FACTOR}" >&2
echo "Use all, phone, or wear." >&2
exit 1
;;
esac
validate_locale() {
local locale="$1"
if [[ "$locale" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
return
fi
echo "Invalid Android screenshot locale: ${locale}" >&2
echo "Use a locale tag like en-US or pt-BR; path separators and dot segments are not allowed." >&2
exit 1
}
validate_locale "$LOCALE"
if [[ ! "$SCREENSHOT_SIZE" =~ ^[0-9]+x[0-9]+$ ]]; then
echo "Invalid Android screenshot size: ${SCREENSHOT_SIZE}" >&2
echo "Use a pixel size like 1080x1920." >&2
exit 1
fi
SCREENSHOT_WIDTH=$((10#${SCREENSHOT_SIZE%x*}))
SCREENSHOT_HEIGHT=$((10#${SCREENSHOT_SIZE#*x}))
SCREENSHOT_MIN_DIMENSION=$((SCREENSHOT_WIDTH < SCREENSHOT_HEIGHT ? SCREENSHOT_WIDTH : SCREENSHOT_HEIGHT))
SCREENSHOT_MAX_DIMENSION=$((SCREENSHOT_WIDTH > SCREENSHOT_HEIGHT ? SCREENSHOT_WIDTH : SCREENSHOT_HEIGHT))
if (( SCREENSHOT_MIN_DIMENSION < 320 || SCREENSHOT_MAX_DIMENSION > 3840 || SCREENSHOT_MAX_DIMENSION > SCREENSHOT_MIN_DIMENSION * 2 )); then
echo "Android screenshot size ${SCREENSHOT_SIZE} does not meet Google Play dimension and aspect-ratio limits." >&2
exit 1
fi
cleanup_started_emulator() {
local stopped=0
if [[ "$STARTED_EMULATOR" != "1" || "$KEEP_EMULATOR" == "1" ]]; then
return
fi
if [[ -n "${ADB_BIN:-}" && -n "${ADB_SERIAL:-}" ]]; then
if "$ADB_BIN" -s "$ADB_SERIAL" emu kill >/dev/null 2>&1; then
stopped=1
local deadline=$((SECONDS + 30))
while (( SECONDS < deadline )); do
if ! "$ADB_BIN" devices | awk 'NR > 1 && $2 == "device" { print $1 }' | grep -Fxq "$ADB_SERIAL"; then
break
fi
sleep 1
done
fi
fi
if [[ "$stopped" != "1" && -n "$EMULATOR_PID" ]]; then
kill "$EMULATOR_PID" >/dev/null 2>&1 || true
fi
}
restore_device_display() {
if [[ "$DISPLAY_OVERRIDDEN" != "1" || -z "${ADB_BIN:-}" || -z "${ADB_SERIAL:-}" ]]; then
return
fi
if [[ -n "$ORIGINAL_WM_SIZE" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell wm size "$ORIGINAL_WM_SIZE" >/dev/null 2>&1 || true
else
"$ADB_BIN" -s "$ADB_SERIAL" shell wm size reset >/dev/null 2>&1 || true
fi
if [[ -n "$ORIGINAL_WM_DENSITY" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell wm density "$ORIGINAL_WM_DENSITY" >/dev/null 2>&1 || true
else
"$ADB_BIN" -s "$ADB_SERIAL" shell wm density reset >/dev/null 2>&1 || true
fi
}
restore_device_timezone() {
if [[ "$TIMEZONE_OVERRIDDEN" != "1" || -z "${ADB_BIN:-}" || -z "${ADB_SERIAL:-}" ]]; then
return
fi
if [[ -n "$ORIGINAL_TIME_ZONE" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell cmd alarm set-timezone "$ORIGINAL_TIME_ZONE" >/dev/null 2>&1 || true
fi
if [[ "$ORIGINAL_AUTO_TIME_ZONE" == "true" || "$ORIGINAL_AUTO_TIME_ZONE" == "false" ]]; then
"$ADB_BIN" -s "$ADB_SERIAL" shell cmd time_zone_detector set_auto_detection_enabled "$ORIGINAL_AUTO_TIME_ZONE" >/dev/null 2>&1 || true
fi
}
cleanup() {
local status=$?
set +e
if [[ "$DRY_RUN" != "1" && -d "$ARTIFACT_DIR" ]]; then
{
printf 'exit_status=%s\n' "$status"
printf 'form_factor=%s\n' "$FORM_FACTOR"
printf 'avd=%s\n' "$AVD"
printf 'serial=%s\n' "${ADB_SERIAL:-}"
if [[ -n "$EMULATOR_PID" ]]; then
ps -p "$EMULATOR_PID" -o pid=,ppid=,stat=,etime=,command=
fi
} >"$ARTIFACT_DIR/process-status.txt" 2>&1
fi
restore_device_display
restore_device_timezone
cleanup_started_emulator
return "$status"
}
trap cleanup EXIT
adb_bin() {
if [[ -n "${ADB:-}" ]]; then
printf '%s\n' "$ADB"
return
fi
if command -v adb >/dev/null 2>&1; then
command -v adb
return
fi
for sdk_root in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "$HOME/Library/Android/sdk"; do
if [[ -n "$sdk_root" && -x "$sdk_root/platform-tools/adb" ]]; then
printf '%s\n' "$sdk_root/platform-tools/adb"
return
fi
done
echo "adb not found. Install Android platform-tools or set ADB." >&2
return 127
}
emulator_bin() {
if [[ -n "${ANDROID_EMULATOR:-}" ]]; then
printf '%s\n' "$ANDROID_EMULATOR"
return
fi
if command -v emulator >/dev/null 2>&1; then
command -v emulator
return
fi
for sdk_root in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "$HOME/Library/Android/sdk"; do
if [[ -n "$sdk_root" && -x "$sdk_root/emulator/emulator" ]]; then
printf '%s\n' "$sdk_root/emulator/emulator"
return
fi
done
echo "Android emulator binary not found. Install the Android emulator or set ANDROID_EMULATOR." >&2
return 127
}
avdmanager_bin() {
if [[ -n "${AVDMANAGER:-}" ]]; then
printf '%s\n' "$AVDMANAGER"
return
fi
for sdk_root in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "$HOME/Library/Android/sdk"; do
for relative_path in cmdline-tools/latest/bin/avdmanager cmdline-tools/bin/avdmanager tools/bin/avdmanager; do
if [[ -n "$sdk_root" && -x "$sdk_root/$relative_path" ]]; then
printf '%s\n' "$sdk_root/$relative_path"
return
fi
done
done
if command -v avdmanager >/dev/null 2>&1; then
command -v avdmanager
return
fi
echo "avdmanager not found. Install Android SDK command-line tools or set AVDMANAGER." >&2
return 127
}
ensure_screenshot_avd() {
local avd="$1"
local emulator
local avdmanager
emulator="$(emulator_bin)"
if "$emulator" -list-avds | grep -Fxq "$avd"; then
return
fi
avdmanager="$(avdmanager_bin)"
echo "Creating no-cutout screenshot AVD '${avd}' from device profile '${SCREENSHOT_DEVICE_PROFILE}'." >&2
if ! printf 'no\n' | "$avdmanager" create avd \
--force \
--name "$avd" \
--package "$SCREENSHOT_SYSTEM_IMAGE" \
--device "$SCREENSHOT_DEVICE_PROFILE"; then
echo "Could not create Android screenshot AVD '${avd}'." >&2
echo "Install SDK package '${SCREENSHOT_SYSTEM_IMAGE}' or set ANDROID_SCREENSHOT_SYSTEM_IMAGE." >&2
return 1
fi
}
connected_devices() {
local adb="$1"
"$adb" devices | awk 'NR > 1 && $2 == "device" { print $1 }'
}
device_count() {
local devices="$1"
printf '%s\n' "$devices" | sed '/^$/d' | wc -l | tr -d ' '
}
running_avd_name() {
local adb="$1"
local serial="$2"
"$adb" -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | sed -n '1p'
}
wait_for_single_device() {
local adb="$1"
local timeout_seconds="${ANDROID_SCREENSHOT_EMULATOR_TIMEOUT_SECONDS:-180}"
local deadline=$((SECONDS + timeout_seconds))
local devices
local count
while (( SECONDS < deadline )); do
devices="$(connected_devices "$adb")"
count="$(device_count "$devices")"
if [[ "$count" == "1" ]]; then
printf '%s\n' "$devices"
return
fi
sleep 2
done
echo "Timed out waiting for exactly one Android emulator device." >&2
"$adb" devices -l >&2 || true
return 1
}
wait_for_boot_completed() {
local adb="$1"
local serial="$2"
local timeout_seconds="${ANDROID_SCREENSHOT_EMULATOR_TIMEOUT_SECONDS:-180}"
local deadline=$((SECONDS + timeout_seconds))
local boot_completed
"$adb" -s "$serial" wait-for-device
while (( SECONDS < deadline )); do
boot_completed="$("$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)"
if [[ "$boot_completed" == "1" ]]; then
"$adb" -s "$serial" shell input keyevent 82 >/dev/null 2>&1 || true
return
fi
sleep 2
done
echo "Timed out waiting for Android emulator boot completion on ${serial}." >&2
return 1
}
wait_for_explicit_device() {
local adb="$1"
local serial="$2"
local timeout_seconds="${ANDROID_SCREENSHOT_DEVICE_TIMEOUT_SECONDS:-30}"
local deadline=$((SECONDS + timeout_seconds))
local state
while (( SECONDS < deadline )); do
state="$("$adb" devices | awk -v serial="$serial" '$1 == serial { print $2 }')"
if [[ "$state" == "device" ]]; then
return
fi
sleep 2
done
if [[ -n "$state" ]]; then
echo "Android device '${serial}' did not become usable within ${timeout_seconds}s; current adb state is '${state}'." >&2
else
echo "Android device '${serial}' was not found within ${timeout_seconds}s." >&2
fi
"$adb" devices -l >&2 || true
return 1
}
stabilize_device_for_screenshots() {
local adb="$1"
local serial="$2"
ORIGINAL_AUTO_TIME_ZONE="$("$adb" -s "$serial" shell cmd time_zone_detector is_auto_detection_enabled 2>/dev/null | tr -d '\r')"
ORIGINAL_TIME_ZONE="$("$adb" -s "$serial" shell getprop persist.sys.timezone 2>/dev/null | tr -d '\r')"
if [[ "$ORIGINAL_AUTO_TIME_ZONE" != "true" && "$ORIGINAL_AUTO_TIME_ZONE" != "false" ]]; then
echo "Could not determine emulator automatic timezone setting." >&2
return 1
fi
if [[ -z "$ORIGINAL_TIME_ZONE" ]]; then
echo "Could not determine emulator timezone." >&2
return 1
fi
# Arm cleanup before the first mutation; restoring an unchanged snapshot is safe.
TIMEZONE_OVERRIDDEN=1
# Seeded chat timestamps use the device timezone, so disable detection before
# pinning UTC to avoid inheriting the runner's locale.
"$adb" -s "$serial" shell cmd time_zone_detector set_auto_detection_enabled false >/dev/null
"$adb" -s "$serial" shell cmd alarm set-timezone UTC >/dev/null
"$adb" -s "$serial" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put global stay_on_while_plugged_in 3 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell svc power stayon true >/dev/null 2>&1 || true
"$adb" -s "$serial" shell input keyevent 224 >/dev/null 2>&1 || true
"$adb" -s "$serial" shell wm dismiss-keyguard >/dev/null 2>&1 || true
"$adb" -s "$serial" shell settings put system font_scale 1.0 >/dev/null 2>&1 || true
}
require_emulator_device() {
local adb="$1"
local serial="$2"
local qemu
qemu="$("$adb" -s "$serial" shell getprop ro.kernel.qemu 2>/dev/null | tr -d '\r')"
if [[ "$qemu" == "1" ]]; then
return
fi
echo "Android screenshot capture requires an emulator; '${serial}' is not an emulator." >&2
echo "Pass --avd <name> or --device <emulator-serial>." >&2
return 1
}
configure_screenshot_display() {
local adb="$1"
local serial="$2"
local current_size
local current_density
local effective_size
local effective_density
local effective_width
current_size="$("$adb" -s "$serial" shell wm size 2>/dev/null | tr -d '\r')"
current_density="$("$adb" -s "$serial" shell wm density 2>/dev/null | tr -d '\r')"
ORIGINAL_WM_SIZE="$(printf '%s\n' "$current_size" | sed -n 's/^Override size: //p')"
ORIGINAL_WM_DENSITY="$(printf '%s\n' "$current_density" | sed -n 's/^Override density: //p')"
effective_size="${ORIGINAL_WM_SIZE:-$(printf '%s\n' "$current_size" | sed -n 's/^Physical size: //p')}"
effective_density="${ORIGINAL_WM_DENSITY:-$(printf '%s\n' "$current_density" | sed -n 's/^Physical density: //p')}"
if [[ ! "$effective_size" =~ ^[0-9]+x[0-9]+$ || ! "$effective_density" =~ ^[0-9]+$ ]]; then
echo "Could not determine emulator display size and density." >&2
return 1
fi
effective_width=$((10#${effective_size%x*}))
SCREENSHOT_DENSITY=$(((10#$effective_density * SCREENSHOT_WIDTH + effective_width / 2) / effective_width))
DISPLAY_OVERRIDDEN=1
"$adb" -s "$serial" shell wm size "$SCREENSHOT_SIZE" >/dev/null
"$adb" -s "$serial" shell wm density "$SCREENSHOT_DENSITY" >/dev/null
echo "Android screenshot density: ${SCREENSHOT_DENSITY} dpi"
}
boot_emulator() {
local adb="$1"
local avd="$2"
local emulator
local emulator_args
local extra_args
local serial
ensure_screenshot_avd "$avd"
emulator="$(emulator_bin)"
EMULATOR_LOG="$ARTIFACT_DIR/emulator.log"
echo "No connected Android device found. Booting AVD '${avd}'." >&2
emulator_args=(-avd "$avd" -no-window -no-audio -no-boot-anim)
if [[ -n "${ANDROID_SCREENSHOT_EMULATOR_ARGS:-}" ]]; then
read -r -a extra_args <<<"$ANDROID_SCREENSHOT_EMULATOR_ARGS"
emulator_args+=(${extra_args[@]+"${extra_args[@]}"})
fi
printf '%q ' "$emulator" "${emulator_args[@]}" >"$ARTIFACT_DIR/emulator-args.txt"
printf '\n' >>"$ARTIFACT_DIR/emulator-args.txt"
"$emulator" "${emulator_args[@]}" >"$EMULATOR_LOG" 2>&1 &
EMULATOR_PID="$!"
STARTED_EMULATOR=1
serial="$(wait_for_single_device "$adb")"
wait_for_boot_completed "$adb" "$serial"
ADB_SERIAL="$serial"
}
resolve_device() {
local adb="$1"
local devices
local count
local connected_avd
if [[ -n "$DEVICE" ]]; then
wait_for_explicit_device "$adb" "$DEVICE"
ADB_SERIAL="$DEVICE"
return
fi
devices="$(connected_devices "$adb")"
count="$(device_count "$devices")"
if [[ "$count" == "1" ]]; then
if connected_avd="$(running_avd_name "$adb" "$devices")"; then
if [[ "$connected_avd" != "$AVD" ]]; then
echo "Connected emulator '${connected_avd:-unknown}' is not the screenshot AVD '${AVD}'." >&2
echo "Stop it so the script can boot '${AVD}', or pass --device '${devices}' to override the no-cutout profile." >&2
return 1
fi
fi
ADB_SERIAL="$devices"
return
fi
if [[ "$count" == "0" ]]; then
boot_emulator "$adb" "$AVD"
return
else
echo "Multiple Android devices are connected. Pass --device <adb-serial>." >&2
fi
return 1
}
latest_debug_apk() {
local output_dir
local pattern
if [[ "$FORM_FACTOR" == "wear" ]]; then
output_dir="${ANDROID_DIR}/wear/build/outputs/apk/debug"
pattern='*-debug.apk'
else
output_dir="${ANDROID_DIR}/app/build/outputs/apk/play/debug"
pattern='*-play-debug.apk'
fi
if [[ ! -d "$output_dir" ]]; then
return 0
fi
find "$output_dir" -maxdepth 1 -name "$pattern" -print 2>/dev/null | sort | tail -n 1
}
scene_ready_text() {
if [[ "$FORM_FACTOR" == "wear" ]]; then
case "$1" in
# Chat follows the latest reply, so the session title can be outside the viewport.
chat) printf '%s\n' "Ready after the final store checks." ;;
voice) printf '%s\n' "Dictate" ;;
controls) printf '%s\n' "Gateway connected" ;;
*)
echo "Unknown Wear OS screenshot scene: $1" >&2
return 1
;;
esac
return
fi
case "$1" in
home) printf '%s\n' "Overview" ;;
# The screenshot fixture seeds chat history and restores at the live edge,
# so wait for the latest reply instead of empty-chat copy.
chat) printf '%s\n' "The Android release is close." ;;
settings) printf '%s\n' "OpenClaw mobile" ;;
voice-wake) printf '%s\n' "Wake listener" ;;
# Connected fixtures can push Add Gateway below the composed viewport, so
# wait for the gateway detail's always-visible subtitle instead.
gateway) printf '%s\n' "Connection between this phone and OpenClaw." ;;
*)
echo "Unknown Android screenshot scene: $1" >&2
return 1
;;
esac
}
wait_for_scene_ready() {
local adb="$1"
local serial="$2"
local scene="$3"
local dump_path="$4"
local marker
local timeout_seconds="${ANDROID_SCREENSHOT_SCENE_TIMEOUT_SECONDS:-45}"
local deadline=$((SECONDS + timeout_seconds))
marker="$(scene_ready_text "$scene")"
while (( SECONDS < deadline )); do
if "$adb" -s "$serial" exec-out uiautomator dump /dev/tty >"$dump_path" 2>/dev/null; then
# A newly created Wear AVD may cover the resumed app with its charging
# overlay after stay-awake is enabled. Dismiss it or first-run capture stalls.
if [[ "$FORM_FACTOR" == "wear" ]] && grep -Fq 'com.google.android.wearable.sysui:id/charging_container' "$dump_path"; then
"$adb" -s "$serial" shell input keyevent 4 >/dev/null 2>&1 || true
sleep 0.5
continue
fi
if grep -Fq "$marker" "$dump_path"; then
return
fi
fi
sleep 1
done
echo "Timed out waiting for scene '${scene}' to expose '${marker}' in the Android UI tree." >&2
return 1
}
sips_bin() {
if [[ -n "${SIPS:-}" ]]; then
printf '%s\n' "$SIPS"
return
fi
if command -v sips >/dev/null 2>&1; then
command -v sips
return
fi
echo "sips not found. Android release screenshots require macOS or an explicit SIPS executable." >&2
return 127
}
normalize_capture_for_play() {
local input_path="$1"
local output_path="$2"
local sips
local description
sips="$(sips_bin)"
"$sips" -s format jpeg -s formatOptions best "$input_path" --out "$output_path" >/dev/null
rm -f "$input_path"
description="$(file "$output_path")"
if [[ "$description" != *"${SCREENSHOT_SIZE/x/x}"* || "$description" != *"JPEG image data"* ]]; then
echo "Invalid Google Play screenshot output: ${description}" >&2
return 1
fi
}
write_artifact_manifest() {
local serial="$1"
local avd_name
local git_sha
local checksum_command
avd_name="$(running_avd_name "$ADB_BIN" "$serial")"
git_sha="$(git -C "$ROOT_DIR" rev-parse HEAD)"
if command -v shasum >/dev/null 2>&1; then
checksum_command=(shasum -a 256)
else
checksum_command=(sha256sum)
fi
{
printf 'git_sha=%s\n' "$git_sha"
printf 'form_factor=%s\n' "$FORM_FACTOR"
printf 'output_type=%s\n' "$OUTPUT_TYPE"
printf 'device=%s\n' "$serial"
printf 'avd=%s\n' "${avd_name:-unknown}"
printf 'locale=%s\n' "$LOCALE"
printf 'size=%s\n' "$SCREENSHOT_SIZE"
printf 'density=%s\n' "$SCREENSHOT_DENSITY"
printf 'format=high-quality JPEG\n'
printf 'scenes=%s\n' "${SCENES[*]}"
printf 'screenshots:\n'
"${checksum_command[@]}" "$ARTIFACT_DIR"/screenshots/*.jpg
} >"$ARTIFACT_DIR/manifest.txt"
}
OUTPUT_DIR="${ANDROID_DIR}/fastlane/metadata/android/${LOCALE}/images/${OUTPUT_TYPE}"
ADB_SERIAL=""
ADB_DISPLAY="${DEVICE:-<auto>}"
echo "Android screenshot output: ${OUTPUT_DIR}"
echo "Android screenshot artifacts: ${ARTIFACT_DIR}"
echo "Android screenshot form factor: ${FORM_FACTOR}"
echo "Android screenshot size: ${SCREENSHOT_SIZE}"
echo "Scenes: ${SCENES[*]}"
echo "ADB device: ${ADB_DISPLAY}"
echo "Screenshot AVD: ${AVD}"
echo "Screenshot device profile: ${SCREENSHOT_DEVICE_PROFILE}"
echo "Screenshot system image: ${SCREENSHOT_SYSTEM_IMAGE}"
if [[ "$DRY_RUN" == "1" ]]; then
echo "Dry run complete. No build, install, or capture commands were executed."
exit 0
fi
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR/screenshots" "$ARTIFACT_DIR/ui-dumps" "$ARTIFACT_DIR/activity-start"
ADB_BIN="$(adb_bin)"
resolve_device "$ADB_BIN"
require_emulator_device "$ADB_BIN" "$ADB_SERIAL"
stabilize_device_for_screenshots "$ADB_BIN" "$ADB_SERIAL"
configure_screenshot_display "$ADB_BIN" "$ADB_SERIAL"
mkdir -p "$OUTPUT_DIR"
rm -f "$OUTPUT_DIR"/*.png "$OUTPUT_DIR"/*.jpg "$OUTPUT_DIR"/*.jpeg
if [[ "$SKIP_INSTALL" != "1" ]]; then
if [[ "$SKIP_BUILD" != "1" ]]; then
(
cd "$ANDROID_DIR"
./gradlew "$GRADLE_ASSEMBLE_TASK"
)
fi
APK_PATH="$(latest_debug_apk)"
if [[ -z "$APK_PATH" ]]; then
echo "No existing ${FORM_FACTOR} debug APK found. Run without --skip-build first." >&2
exit 1
fi
"$ADB_BIN" -s "$ADB_SERIAL" install -r "$APK_PATH" >/dev/null
elif [[ "$SKIP_BUILD" != "1" ]]; then
(
cd "$ANDROID_DIR"
./gradlew "$GRADLE_ASSEMBLE_TASK"
)
fi
"$ADB_BIN" -s "$ADB_SERIAL" shell pm clear "$APP_PACKAGE" >/dev/null
"$ADB_BIN" -s "$ADB_SERIAL" shell pm grant "$APP_PACKAGE" android.permission.RECORD_AUDIO >/dev/null
"$ADB_BIN" -s "$ADB_SERIAL" logcat -c >/dev/null 2>&1 || true
for scene in "${SCENES[@]}"; do
output_path="${OUTPUT_DIR}/openclaw-${scene}.jpg"
raw_path="${OUTPUT_DIR}/openclaw-${scene}.raw.png"
artifact_path="${ARTIFACT_DIR}/screenshots/openclaw-${scene}.jpg"
ui_dump_path="${ARTIFACT_DIR}/ui-dumps/openclaw-${scene}.xml"
activity_start_path="${ARTIFACT_DIR}/activity-start/openclaw-${scene}.txt"
"$ADB_BIN" -s "$ADB_SERIAL" shell am force-stop "$APP_PACKAGE" >/dev/null
"$ADB_BIN" -s "$ADB_SERIAL" shell input keyevent 224 >/dev/null 2>&1 || true
"$ADB_BIN" -s "$ADB_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true
"$ADB_BIN" -s "$ADB_SERIAL" shell am start -W \
-n "$ACTIVITY_COMPONENT" \
--ez openclaw.screenshotMode true \
--es openclaw.screenshotScene "$scene" >"$activity_start_path"
wait_for_scene_ready "$ADB_BIN" "$ADB_SERIAL" "$scene" "$ui_dump_path"
sleep "${ANDROID_SCREENSHOT_SETTLE_SECONDS:-0.5}"
"$ADB_BIN" -s "$ADB_SERIAL" exec-out screencap -p >"$raw_path"
normalize_capture_for_play "$raw_path" "$output_path"
cp "$output_path" "$artifact_path"
echo "Captured ${output_path}"
done
"$ADB_BIN" -s "$ADB_SERIAL" logcat -d >"$ARTIFACT_DIR/logcat.txt" 2>&1 || true
write_artifact_manifest "$ADB_SERIAL"
echo "Android screenshots written to ${OUTPUT_DIR}"
echo "Android screenshot artifacts written to ${ARTIFACT_DIR}"