From 0dfe62b47d13b615c86d53c63f0d70ee47966ede Mon Sep 17 00:00:00 2001 From: Altay Date: Thu, 24 Sep 2026 20:51:36 +0300 Subject: [PATCH 1/2] feat(harness): add opt-in Google TV emulator profile Refs #176 --- AGENTS.md | 3 ++- docs/harness.md | 25 +++++++++++++++++---- scripts/bootstrap.sh | 33 +++++++++++++++++++--------- scripts/emulator.sh | 17 ++++++++------- scripts/lib.sh | 25 ++++++++++++++++----- scripts/test-emulator.sh | 47 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 66593bf2..5a2cd895 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,8 @@ rest. Idempotent. Installs cmdline-tools (via Homebrew if missing), accepts licenses, installs platform/build-tools for the compileSdk, the emulator, the API 37 Google Play phone image and API 36 Android TV image, creates -the two reusable AVDs, and writes +the two reusable AVDs (`--google-tv` adds the opt-in Google TV image and +AVD), and writes `local.properties` (`sdk.dir` plus `putioSdkKotlinPath`, defaulting to the sibling `../putio-sdk-kotlin` checkout, which must be cloned). diff --git a/docs/harness.md b/docs/harness.md index 9736f1af..3197a28c 100644 --- a/docs/harness.md +++ b/docs/harness.md @@ -14,6 +14,23 @@ Two reusable AVDs, arm64 on Apple Silicon (x86_64 elsewhere): | `putio-phone` | pixel_7 | `android-37.0;google_apis_playstore` | | `putio-tv` | tv_1080p | `android-36;android-tv` | +An opt-in Google TV AVD covers the Google TV launcher and Play surfaces. The +default bootstrap does not install its image or create it: + +| AVD | Device profile | System image | +| --- | --- | --- | +| `putio-google-tv` | tv_1080p | `android-36;google-tv` | + +```bash +./scripts/bootstrap.sh --google-tv # adds the image and AVD +./scripts/emulator.sh boot google-tv --headless +./scripts/emulator.sh stop google-tv +``` + +A fresh `putio-google-tv` boots into Google TV setup; finish or cancel it +before launcher proof. `scripts/prove.sh tv` still targets `putio-tv`; install +on the Google TV emulator with `adb -s install` for manual proof. + ```bash ./scripts/emulator.sh boot phone --headless # prints serial; reuses if running ./scripts/emulator.sh status @@ -37,11 +54,11 @@ category. The harness fails closed before install when that secure OAuth transport is unavailable. TV and the CI managed device remain API 36. Bootstrap never replaces a mismatched AVD. It still provisions the other -profile, then fails with an explicit command; stop and delete that exact +profiles, then fails with an explicit command; stop and delete that exact profile yourself before rerunning bootstrap. The harness owns only -`putio-phone`, `putio-tv`, and the `--ephemeral` AVDs it creates; other AVDs on -the machine are left alone, even when `avdmanager list avd` reports them as -unloadable. +`putio-phone`, `putio-tv`, the opt-in `putio-google-tv`, and the +`--ephemeral` AVDs it creates; other AVDs on the machine are left alone, even +when `avdmanager list avd` reports them as unloadable. ## Launch Proof diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index f98062f8..c93e097c 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -11,9 +11,23 @@ # Google Play phone image, the API 36 Android TV image, and both reusable # AVDs. Writes local.properties. # Idempotent: safe to re-run; already-installed packages are skipped. +# +# scripts/bootstrap.sh [--google-tv] +# +# --google-tv also installs the API 36 Google TV image and provisions the +# opt-in putio-google-tv AVD. source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +AVD_PROFILES=(phone tv) +while [[ $# -gt 0 ]]; do + case "$1" in + --google-tv) AVD_PROFILES=(phone tv google-tv) ;; + *) print_usage "${BASH_SOURCE[0]}"; exit 64 ;; + esac + shift +done + COMPILE_SDK_PLATFORM="platforms;android-37.0" BUILD_TOOLS="build-tools;37.0.0" @@ -32,8 +46,13 @@ SDKMANAGER="${SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" [[ -x "${SDKMANAGER}" ]] || die "sdkmanager missing under ${SDK_ROOT}" log "SDK root: ${SDK_ROOT}" +SDK_PACKAGES=("platform-tools" "emulator" "${COMPILE_SDK_PLATFORM}" "${BUILD_TOOLS}") +for profile in "${AVD_PROFILES[@]}"; do + SDK_PACKAGES+=("$(image_for "${profile}")") +done + packages_missing=0 -for pkg in "platform-tools" "emulator" "${COMPILE_SDK_PLATFORM}" "${BUILD_TOOLS}" "$(phone_image)" "$(tv_image)"; do +for pkg in "${SDK_PACKAGES[@]}"; do [[ -d "${SDK_ROOT}/$(echo "${pkg}" | tr ';' '/')" ]] || packages_missing=1 done @@ -44,15 +63,9 @@ if [[ "${packages_missing}" == "1" ]]; then (yes || true) | "${SDKMANAGER}" --sdk_root="${SDK_ROOT}" --licenses >/dev/null log "installing SDK packages (first run downloads several GB)" - "${SDKMANAGER}" --sdk_root="${SDK_ROOT}" --install \ - "platform-tools" \ - "emulator" \ - "${COMPILE_SDK_PLATFORM}" \ - "${BUILD_TOOLS}" \ - "$(phone_image)" \ - "$(tv_image)" + "${SDKMANAGER}" --sdk_root="${SDK_ROOT}" --install "${SDK_PACKAGES[@]}" - for pkg in "platform-tools" "emulator" "${COMPILE_SDK_PLATFORM}" "${BUILD_TOOLS}" "$(phone_image)" "$(tv_image)"; do + for pkg in "${SDK_PACKAGES[@]}"; do [[ -d "${SDK_ROOT}/$(echo "${pkg}" | tr ';' '/')" ]] || die "package ${pkg} missing after install" done else @@ -129,7 +142,7 @@ if [[ ! -d "${SDK_KOTLIN_PATH}" ]]; then die "putio-sdk-kotlin checkout missing at ${SDK_KOTLIN_PATH}; run: git clone git@github.com:putdotio/putio-sdk-kotlin.git '${SDK_KOTLIN_PATH}' (or point putioSdkKotlinPath in local.properties at an existing checkout), then re-run bootstrap" fi -provision_avds || exit 1 +provision_avds "${AVD_PROFILES[@]}" || exit 1 log "bootstrap complete" log "next: ./gradlew verify :app:assembleMobileProductionDebug :app:assembleTvProductionDebug" diff --git a/scripts/emulator.sh b/scripts/emulator.sh index 1342763a..980074f8 100755 --- a/scripts/emulator.sh +++ b/scripts/emulator.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash -# Manage the repo's emulators: putio-phone and putio-tv. +# Manage the repo's emulators: putio-phone, putio-tv, and the opt-in +# putio-google-tv (provisioned by `scripts/bootstrap.sh --google-tv`). # -# scripts/emulator.sh create [--name NAME] -# scripts/emulator.sh boot [--headless] [--name NAME] -# scripts/emulator.sh stop [--name NAME] +# scripts/emulator.sh create [--name NAME] +# scripts/emulator.sh boot [--headless] [--name NAME] +# scripts/emulator.sh stop [--name NAME] # scripts/emulator.sh stop -# scripts/emulator.sh delete [--name NAME] +# scripts/emulator.sh delete [--name NAME] # scripts/emulator.sh status # # Ownership contract: @@ -35,7 +36,7 @@ parse_profile_args() { HEADLESS="${PUTIO_EMULATOR_HEADLESS:-0}" while [[ $# -gt 0 ]]; do case "$1" in - phone|tv) PROFILE="$1" ;; + phone|tv|google-tv) PROFILE="$1" ;; --name) NAME="${2:?--name requires a value}"; shift ;; --headless) HEADLESS=1 ;; *) die "unknown argument: $1" ;; @@ -64,7 +65,7 @@ do_create() { local image create_out image="$(image_for "${PROFILE}")" [[ -d "${SDK_ROOT}/$(echo "${image}" | tr ';' '/')" ]] || \ - die "system image ${image} not installed; run scripts/bootstrap.sh" + die "system image ${image} not installed; run $(bootstrap_command_for "${PROFILE}")" if avd_registered "${NAME}"; then require_matching_avd_image log "AVD ${NAME} already exists (${image})" @@ -197,7 +198,7 @@ do_stop() { [[ $# -eq 1 ]] || usage serial="${target}" ;; - phone|tv) + phone|tv|google-tv) parse_profile_args "$@" serial="$(serial_for_avd "${NAME}")" || \ { log "no running emulator for AVD ${NAME}"; return 0; } diff --git a/scripts/lib.sh b/scripts/lib.sh index 1305a830..58a25b1d 100755 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -63,6 +63,7 @@ require_sdk_root() { # system images are not published for API 37 yet, so TV remains on API 36. PHONE_AVD="putio-phone" TV_AVD="putio-tv" +GOOGLE_TV_AVD="putio-google-tv" PHONE_API_LEVEL="37" CHROME_PACKAGE="com.android.chrome" AUTH_TAB_SERVICE_ACTION="android.support.customtabs.action.CustomTabsService" @@ -78,19 +79,30 @@ sdk_arch() { phone_image() { echo "system-images;android-37.0;google_apis_playstore;$(sdk_arch)"; } tv_image() { echo "system-images;android-36;android-tv;$(sdk_arch)"; } +# Opt-in: only `scripts/bootstrap.sh --google-tv` installs and provisions it. +google_tv_image() { echo "system-images;android-36;google-tv;$(sdk_arch)"; } # profile -> AVD name / image / avdmanager device id avd_name_for() { case "$1" in phone) echo "${PHONE_AVD}" ;; tv) echo "${TV_AVD}" ;; - *) die "unknown emulator profile '$1' (expected phone|tv)" ;; + google-tv) echo "${GOOGLE_TV_AVD}" ;; + *) die "unknown emulator profile '$1' (expected phone|tv|google-tv)" ;; esac } image_for() { case "$1" in phone) phone_image ;; tv) tv_image ;; + google-tv) google_tv_image ;; + esac +} + +bootstrap_command_for() { + case "$1" in + google-tv) echo "scripts/bootstrap.sh --google-tv" ;; + *) echo "scripts/bootstrap.sh" ;; esac } @@ -111,7 +123,7 @@ avd_stop_command() { avd_recreate_command() { local profile="$1" name="$2" if [[ "${name}" == "$(avd_name_for "${profile}")" ]]; then - echo "scripts/bootstrap.sh" + bootstrap_command_for "${profile}" else echo "scripts/emulator.sh create ${profile} --name ${name}" fi @@ -119,15 +131,16 @@ avd_recreate_command() { device_for() { case "$1" in phone) echo "pixel_7" ;; - tv) echo "tv_1080p" ;; + tv|google-tv) echo "tv_1080p" ;; esac } # A refused profile keeps its recovery message and fails the run, but must not -# stop the other profile from being provisioned. +# stop the other profiles from being provisioned. provision_avds() { - local profile failed=() - for profile in phone tv; do + local profile failed=() profiles=("$@") + [[ $# -gt 0 ]] || profiles=(phone tv) + for profile in "${profiles[@]}"; do "${REPO_ROOT}/scripts/emulator.sh" create "${profile}" || failed+=("${profile}") done [[ ${#failed[@]} -eq 0 ]] || { log "ERROR: AVD provisioning failed for: ${failed[*]}"; return 1; } diff --git a/scripts/test-emulator.sh b/scripts/test-emulator.sh index 09a9522a..e606a774 100755 --- a/scripts/test-emulator.sh +++ b/scripts/test-emulator.sh @@ -577,4 +577,51 @@ grep -Fq "AVD provisioning failed for: phone" "${provision_out}" || \ [[ "$(<"${state}/avd-operations")" == "create:${PHONE_AVD}:${target_image}" ]] || \ fail "recovered provisioning touched the existing TV AVD" +# Google TV is opt-in: default provisioning never creates it, and its recovery +# commands point at the opt-in bootstrap. +google_tv_target_image="system-images;android-36;google-tv;$(sdk_arch)" +[[ "$(google_tv_image)" == "${google_tv_target_image}" ]] || \ + fail "Google TV image is not pinned to API 36 Google TV" +reset_avd "${PHONE_AVD}" "${target_image}" 0 +google_tv_out="${tmpdir}/google-tv.log" +if "${REPO_ROOT}/scripts/emulator.sh" create google-tv >"${google_tv_out}" 2>&1; then + fail "Google TV AVD was created without its system image" +fi +grep -Fq "system image ${google_tv_target_image} not installed; run scripts/bootstrap.sh --google-tv" \ + "${google_tv_out}" || fail "missing Google TV image omitted the opt-in bootstrap command" +[[ ! -s "${state}/avd-operations" ]] || fail "missing Google TV image mutated state" + +mkdir -p "${fake_sdk}/$(tr ';' '/' <<<"${google_tv_target_image}")" +(provision_avds) >"${provision_out}" 2>&1 || fail "default provisioning failed" +[[ "$(<"${state}/avd-operations")" == "create:${TV_AVD}:${tv_target_image}" ]] || \ + fail "default provisioning did not keep to the phone and TV AVDs" + +: > "${state}/avd-operations" +(provision_avds phone tv google-tv) >"${provision_out}" 2>&1 || \ + fail "opt-in provisioning failed" +[[ "$(<"${state}/avd-operations")" == "create:${GOOGLE_TV_AVD}:${google_tv_target_image}" ]] || \ + fail "opt-in provisioning did not create only the Google TV AVD" +[[ "$(avd_image_for_name "${GOOGLE_TV_AVD}")" == "${google_tv_target_image}" ]] || \ + fail "Google TV AVD did not use the pinned Google TV image" + +reset_avd "${GOOGLE_TV_AVD}" "${tv_target_image}" 0 +if "${REPO_ROOT}/scripts/emulator.sh" create google-tv >"${google_tv_out}" 2>&1; then + fail "mismatched Google TV AVD was replaced" +fi +grep -Fq "explicitly run scripts/emulator.sh stop google-tv, then scripts/emulator.sh delete google-tv, then scripts/bootstrap.sh --google-tv" \ + "${google_tv_out}" || fail "mismatched Google TV AVD omitted its opt-in recovery command" +[[ ! -s "${state}/avd-operations" ]] || fail "mismatched Google TV AVD refusal mutated state" + +reset_avd "${GOOGLE_TV_AVD}" "${google_tv_target_image}" 0 +reset_runtime +google_tv_serial="$(PUTIO_EMULATOR_BOOT_TIMEOUT=10 "${REPO_ROOT}/scripts/emulator.sh" boot google-tv --headless 2>"${google_tv_out}")" || \ + fail "Google TV AVD did not boot" +[[ "${google_tv_serial}" == "emulator-5554" ]] || fail "Google TV boot printed '${google_tv_serial}'" +[[ "$(<"${state}/name")" == "${GOOGLE_TV_AVD}" ]] || fail "Google TV boot started the wrong AVD" +if grep -q 'ro.build.version.sdk\|com.android.chrome' "${state}/adb-calls"; then + fail "Google TV boot ran phone readiness checks" +fi +"${REPO_ROOT}/scripts/emulator.sh" stop google-tv >/dev/null 2>&1 || fail "could not stop the Google TV emulator" +[[ "$(<"${state}/running")" == "0" ]] || fail "Google TV stop left its emulator running" + echo "emulator contract tests passed" From eabe45f02d575dd5e0329d6d501b878eb29d4f01 Mon Sep 17 00:00:00 2001 From: Altay Date: Thu, 24 Sep 2026 20:55:41 +0300 Subject: [PATCH 2/2] fix(harness): reject unknown profiles in image_for --- scripts/lib.sh | 1 + scripts/test-emulator.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/lib.sh b/scripts/lib.sh index 58a25b1d..1612e151 100755 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -96,6 +96,7 @@ image_for() { phone) phone_image ;; tv) tv_image ;; google-tv) google_tv_image ;; + *) die "unknown emulator profile '$1' (expected phone|tv|google-tv)" ;; esac } diff --git a/scripts/test-emulator.sh b/scripts/test-emulator.sh index e606a774..8daa3bf0 100755 --- a/scripts/test-emulator.sh +++ b/scripts/test-emulator.sh @@ -618,7 +618,7 @@ google_tv_serial="$(PUTIO_EMULATOR_BOOT_TIMEOUT=10 "${REPO_ROOT}/scripts/emulato fail "Google TV AVD did not boot" [[ "${google_tv_serial}" == "emulator-5554" ]] || fail "Google TV boot printed '${google_tv_serial}'" [[ "$(<"${state}/name")" == "${GOOGLE_TV_AVD}" ]] || fail "Google TV boot started the wrong AVD" -if grep -q 'ro.build.version.sdk\|com.android.chrome' "${state}/adb-calls"; then +if grep -Eq 'ro\.build\.version\.sdk|com\.android\.chrome' "${state}/adb-calls"; then fail "Google TV boot ran phone readiness checks" fi "${REPO_ROOT}/scripts/emulator.sh" stop google-tv >/dev/null 2>&1 || fail "could not stop the Google TV emulator"