#!/bin/sh set -eu ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) ANDROID_ENV="$ROOT/.env" TEST_ENV="$ROOT/.env.android-test" run_id=$(date -u +%Y%m%d%H%M%S)-$$ android_api=${WNH_ANDROID_TEST_API:-37.0} # 1G is measured against the API 30/34/37 suite and is also exercised by the # API 24 minimum-SDK probe. It can still be overridden for a future test that # intentionally stores more device data. android_data_partition_size=${WNH_ANDROID_TEST_DATA_PARTITION_SIZE:-1G} if ! printf '%s\n' "$android_data_partition_size" | grep -Eq '^[1-9][0-9]*[GKM]$'; then echo "WNH_ANDROID_TEST_DATA_PARTITION_SIZE must be a positive integer followed by G, M, or K." >&2 exit 1 fi case "$android_api" in 24) android_system_image="system-images/android-24/google_apis/x86_64" ;; 30) android_system_image="system-images/android-30/google_apis/x86_64" ;; 34) android_system_image="system-images/android-34/google_apis/x86_64" ;; 37.0) android_system_image="system-images/android-37.0/google_apis_ps16k/x86_64" ;; *) echo "WNH_ANDROID_TEST_API must be one of: 24, 30, 34, 37.0." >&2 exit 1 ;; esac api_label=$(printf '%s' "$android_api" | tr '.' '-') image="who-need-help-android:instrumentation-api${api_label}-$run_id" container="who-need-help-android-instrumentation-api${api_label}-$run_id" avd_volume="who-need-help-android-avd-api${api_label}-$run_id" output="$ROOT/output/android-instrumentation/api${api_label}/$run_id" geo_pid="" death_runner_pid="" if [ ! -e /dev/kvm ]; then echo "/dev/kvm is required for the containerized Android emulator." >&2 exit 1 fi if [ ! -f "$ANDROID_ENV" ]; then echo "Missing $ANDROID_ENV. Copy .env.example to .env and configure Android timings." >&2 exit 1 fi "$ROOT/scripts/ensure-local-android-test-env.sh" set -a # shellcheck source=/dev/null . "$ANDROID_ENV" # shellcheck source=/dev/null . "$TEST_ENV" set +a : "${WNH_ANDROID_TEST_BASE_URL:?Set WNH_ANDROID_TEST_BASE_URL in .env.android-test}" : "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}" : "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}" android_test_port=${WNH_ANDROID_TEST_BASE_URL#http://127.0.0.1:} if [ "$android_test_port" = "$WNH_ANDROID_TEST_BASE_URL" ]; then echo "WNH_ANDROID_TEST_BASE_URL must be an http://127.0.0.1:PORT origin." >&2 exit 1 fi case "$android_test_port" in ""|*[!0-9]*) echo "WNH_ANDROID_TEST_BASE_URL must contain only a numeric port." >&2 exit 1 ;; esac if [ "$android_test_port" -lt 1 ] || [ "$android_test_port" -gt 65535 ]; then echo "WNH_ANDROID_TEST_BASE_URL port must be between 1 and 65535." >&2 exit 1 fi mkdir -p "$output" cleanup() { status=$? if [ -n "$geo_pid" ]; then kill "$geo_pid" >/dev/null 2>&1 || true wait "$geo_pid" >/dev/null 2>&1 || true fi if [ -n "$death_runner_pid" ]; then kill "$death_runner_pid" >/dev/null 2>&1 || true wait "$death_runner_pid" >/dev/null 2>&1 || true fi if [ "$status" -ne 0 ] && docker inspect "$container" >/dev/null 2>&1; then docker exec "$container" adb logcat -d > "$output/logcat.txt" 2>&1 || true docker exec "$container" adb shell dumpsys activity services \ org.whoneedhelp.mobile.debug > "$output/services.txt" 2>&1 || true docker exec "$container" adb shell dumpsys notification --noredact \ > "$output/notifications.txt" 2>&1 || true docker exec "$container" adb shell uiautomator dump /sdcard/window.xml \ > "$output/window-dump.txt" 2>&1 || true docker exec "$container" adb shell cat /sdcard/window.xml \ > "$output/window.xml" 2>&1 || true docker logs "$container" > "$output/emulator.log" 2>&1 || true fi docker rm -f "$container" >/dev/null 2>&1 || true docker volume rm -f "$avd_volume" >/dev/null 2>&1 || true docker image rm "$image" >/dev/null 2>&1 || true exit "$status" } trap cleanup EXIT HUP INT TERM docker build \ --build-arg "WNH_DEBUG_BASE_URL=$WNH_ANDROID_TEST_BASE_URL" \ --build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \ --build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \ --build-arg "ANDROID_EMULATOR_SYSTEM_IMAGE=$android_system_image" \ --build-arg "ANDROID_EMULATOR_DATA_PARTITION_SIZE=$android_data_partition_size" \ --target emulator \ -t "$image" \ "$ROOT/android" docker volume create "$avd_volume" >"$output/avd-volume.txt" docker run -d \ --name "$container" \ --device /dev/kvm \ --network none \ --mount \ "type=volume,source=$avd_volume,target=/home/gradle/.android/avd/who_need_help_test.avd" \ "$image" > "$output/container-id.txt" if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != "true" ]; then docker logs "$container" >"$output/emulator.log" 2>&1 || true echo "Android emulator exited before ADB became available; inspect $output/emulator.log." >&2 exit 1 fi docker exec "$container" adb wait-for-device booted="" attempt=0 while [ "$attempt" -lt 90 ]; do booted=$(docker exec "$container" adb shell getprop sys.boot_completed 2>/dev/null \ | tr -d '\r') if [ "$booted" = "1" ]; then break fi attempt=$((attempt + 1)) sleep 2 done if [ "$booted" != "1" ]; then echo "Android emulator did not finish booting." >&2 exit 1 fi docker exec "$container" adb shell input keyevent 82 docker exec "$container" adb shell settings put global window_animation_scale 0 docker exec "$container" adb shell settings put global transition_animation_scale 0 docker exec "$container" adb shell settings put global animator_duration_scale 0 if [ "$android_api" = "24" ]; then docker exec "$container" adb shell settings put secure location_mode 3 else docker exec "$container" adb shell cmd location set-location-enabled true fi docker exec "$container" adb emu geo fix -122.084000 37.422000 docker exec "$container" adb install -r \ /opt/who-need-help/who-need-help-debug.apk docker exec "$container" adb install -r \ /opt/who-need-help/who-need-help-debug-androidTest.apk docker exec "$container" adb shell pm list instrumentation \ > "$output/instrumentation.txt" ( while docker inspect "$container" >/dev/null 2>&1; do docker exec "$container" adb emu geo fix -122.084000 37.422000 \ >/dev/null 2>&1 || true sleep 2 done ) & geo_pid=$! set +e docker exec "$container" adb shell am instrument -w -r \ -e class org.whoneedhelp.mobile.AndroidClientInstrumentedTest \ org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \ > "$output/results.txt" 2>&1 instrumentation_status=$? set -e cat "$output/results.txt" if [ "$instrumentation_status" -ne 0 ] \ || ! grep -Eq '^OK \([0-9]+ tests?\)$' "$output/results.txt" \ || grep -Eq 'FAILURES!!!|INSTRUMENTATION_FAILED|Process crashed' "$output/results.txt"; then echo "Android instrumentation failed; diagnostics are retained in $output." >&2 exit 1 fi docker exec "$container" adb shell am force-stop org.whoneedhelp.mobile.debug docker exec "$container" adb root >"$output/adb-root.txt" docker exec "$container" adb wait-for-device docker exec "$container" adb shell am instrument -w -r \ -e class org.whoneedhelp.mobile.TrackingProcessDeathProbeTest \ org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \ >"$output/process-death-results.txt" 2>&1 & death_runner_pid=$! service_observed="" attempt=0 while [ "$attempt" -lt 20 ]; do if docker exec "$container" adb shell dumpsys activity services \ org.whoneedhelp.mobile.debug | grep -q 'org.whoneedhelp.mobile.TrackingService'; then service_observed=1 break fi attempt=$((attempt + 1)) sleep 1 done if [ -z "$service_observed" ]; then echo "Process-death probe never observed the foreground service." >&2 exit 1 fi app_pid=$(docker exec "$container" adb shell pidof org.whoneedhelp.mobile.debug | tr -d '\r') case "$app_pid" in ""|*[!0-9]*) echo "Process-death probe could not identify one target process." >&2 exit 1 ;; esac docker exec "$container" adb logcat -c docker exec "$container" adb shell kill -9 "$app_pid" set +e wait "$death_runner_pid" death_status=$? set -e death_runner_pid="" cat "$output/process-death-results.txt" docker exec "$container" adb logcat -d >"$output/process-death-logcat.txt" if ! grep -Eq \ 'Process org\.whoneedhelp\.mobile\.debug .* has died:.*FGS|Crash of app org\.whoneedhelp\.mobile\.debug running instrumentation|Process crashed' \ "$output/process-death-logcat.txt"; then echo "The expected instrumentation process death was not observed." >&2 exit 1 fi printf 'Expected killed instrumentation exit status: %s\n' "$death_status" \ >"$output/process-death-status.txt" attempt=0 while [ "$attempt" -lt 10 ]; do if ! docker exec "$container" adb shell dumpsys activity services \ org.whoneedhelp.mobile.debug | grep -q 'org.whoneedhelp.mobile.TrackingService'; then break fi attempt=$((attempt + 1)) sleep 1 done if docker exec "$container" adb shell dumpsys activity services \ org.whoneedhelp.mobile.debug | grep -q 'org.whoneedhelp.mobile.TrackingService'; then echo "START_NOT_STICKY tracking restarted after process death." >&2 exit 1 fi if docker exec "$container" adb shell dumpsys notification --noredact | grep -q 'Sharing live location'; then echo "The tracking notification remained after process death." >&2 exit 1 fi echo "Android API $android_api instrumentation and process-death probes passed." echo "Evidence: $output"