mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
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
777 lines
25 KiB
Bash
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}"
|