Download scripts/android-screenshots.sh from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 25.5 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/scripts/android-screenshots.sh
- Command line
-
hf download hf://SaylorTwift/openclaw/scripts/android-screenshots.sh
-
curl -L -o android-screenshots.sh https://huggingface.co/SaylorTwift/openclaw/resolve/main/scripts/android-screenshots.sh
25.5 kB
| # 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_emulator_log() { | |
| if [[ -n "$EMULATOR_LOG" && -f "$EMULATOR_LOG" ]]; then | |
| rm -f "$EMULATOR_LOG" | |
| fi | |
| } | |
| cleanup() { | |
| restore_device_display | |
| restore_device_timezone | |
| cleanup_started_emulator | |
| cleanup_emulator_log | |
| } | |
| 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="$(mktemp "${TMPDIR:-/tmp}/openclaw-android-screenshot-emulator.XXXXXX.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 | |
| "$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) printf '%s\n' "Release planning" ;; | |
| 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 | |
| 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 | |
| rm -rf "$ARTIFACT_DIR" | |
| mkdir -p "$ARTIFACT_DIR/screenshots" "$ARTIFACT_DIR/ui-dumps" "$ARTIFACT_DIR/activity-start" | |
| 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 | |
| if [[ -n "$EMULATOR_LOG" && -f "$EMULATOR_LOG" ]]; then | |
| cp "$EMULATOR_LOG" "$ARTIFACT_DIR/emulator.log" | |
| fi | |
| write_artifact_manifest "$ADB_SERIAL" | |
| echo "Android screenshots written to ${OUTPUT_DIR}" | |
| echo "Android screenshot artifacts written to ${ARTIFACT_DIR}" | |