diff --git a/watch/wearos/.gitignore b/watch/wearos/.gitignore index aa724b7..8ce600e 100644 --- a/watch/wearos/.gitignore +++ b/watch/wearos/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +.watch_serial diff --git a/watch/wearos/app/build.gradle.kts b/watch/wearos/app/build.gradle.kts index e3514d3..7cca982 100644 --- a/watch/wearos/app/build.gradle.kts +++ b/watch/wearos/app/build.gradle.kts @@ -22,19 +22,27 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Build-time config sourced from local.properties (gitignored). - // Falls back to emulator loopback + sentinel key if absent. + // Build-time config resolved in this order: + // 1. Gradle CLI property (-Pdoctate.serverUrl=...) — used by run.sh + // to switch between emulator and watch builds without touching + // local.properties. + // 2. local.properties (gitignored) — sticky per-developer default. + // 3. Hardcoded fallback — emulator loopback + sentinel key. val localProps = Properties().apply { rootProject.file("local.properties").takeIf { it.exists() } ?.inputStream()?.use { load(it) } } + fun resolveProp(key: String, default: String): String = + (project.findProperty(key) as? String) + ?: localProps.getProperty(key) + ?: default buildConfigField( "String", "SERVER_URL", - "\"${localProps.getProperty("doctate.serverUrl", "http://10.0.2.2:3000")}\"" + "\"${resolveProp("doctate.serverUrl", "http://10.0.2.2:3000")}\"" ) buildConfigField( "String", "API_KEY", - "\"${localProps.getProperty("doctate.apiKey", "MISSING_API_KEY")}\"" + "\"${resolveProp("doctate.apiKey", "MISSING_API_KEY")}\"" ) } diff --git a/watch/wearos/app/src/main/res/xml/network_security_config.xml b/watch/wearos/app/src/main/res/xml/network_security_config.xml index 555630d..e1f38b1 100644 --- a/watch/wearos/app/src/main/res/xml/network_security_config.xml +++ b/watch/wearos/app/src/main/res/xml/network_security_config.xml @@ -5,10 +5,13 @@ - + 10.0.2.2 + 192.168.178.27 diff --git a/watch/wearos/run.sh b/watch/wearos/run.sh index d83dab6..b2c3a99 100755 --- a/watch/wearos/run.sh +++ b/watch/wearos/run.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # Build / install / run helper for the Wear OS app. # +# Usage: +# ./run.sh [target] [args] +# +# Optional target prefix (selects which device + which build config): +# watch Real Pixel Watch over ADB-WiFi. Uses the cached serial +# from .watch_serial and points the build at the dev +# laptop's LAN URL ($DOCTATE_DEV_SERVER). +# emulator Wear OS AVD on this host. Uses the first attached +# emulator-* serial; build keeps its emulator default +# (10.0.2.2:3000). +# (none) Backwards-compatible: no serial pinning, no URL override. +# ADB / Gradle pick the first attached device — fine when +# only one is connected. +# # Subcommands: # (none) install + start (default inner loop) # build ./gradlew :app:assembleDebug @@ -9,12 +23,26 @@ # stop adb shell am force-stop ... # logcat adb logcat, filtered to the app's PID # shot [path] screencap to PNG (default /tmp/wear_screen.png) +# devices list attached devices and which one this script would use +# connect [host:port] +# adb connect to a Pixel Watch over WiFi and remember +# the serial in .watch_serial. Without an argument: +# reuse the cached serial. # test run JVM unit tests, plus instrumented tests on emulator # (set SKIP_INSTRUMENTED=1 to skip the on-device tier) # clean ./gradlew clean # help show this help # # Env vars: +# DOCTATE_TARGET Same as the [target] prefix above (watch|emulator). +# The CLI prefix takes precedence when both are set. +# ADB_SERIAL Pin a specific device serial. Bypasses target logic. +# DOCTATE_DEV_SERVER +# LAN URL of the Axum dev server, used by 'watch' target. +# Default: http://192.168.178.27:3000 +# DOCTATE_API_KEY If set, passed as -Pdoctate.apiKey=... to Gradle and +# baked into BuildConfig.API_KEY at compile time. +# Otherwise the value from local.properties is used. # WEAR_AVD AVD name to auto-boot when no device is attached # (skips the interactive menu; useful for CI / scripted runs) # SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no emulator needed) @@ -25,6 +53,13 @@ set -euo pipefail PACKAGE="com.doctate.watch" ACTIVITY=".presentation.MainActivity" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WATCH_SERIAL_FILE="$SCRIPT_DIR/.watch_serial" +DEFAULT_DEV_SERVER="http://192.168.178.27:3000" + +# Populated by apply_target / parsed args. Empty = no override. +ADB_SERIAL="${ADB_SERIAL:-}" +DOCTATE_TARGET="${DOCTATE_TARGET:-}" +GRADLE_PROPS=() # --- Output helpers -------------------------------------------------------- if [[ -t 1 ]]; then @@ -61,13 +96,88 @@ detect_adb() { fi } +# Wrapper: calls adb with -s $ADB_SERIAL when set, otherwise plain adb. +# Bash idiom ${VAR:+ARG} expands to ARG only if VAR is non-empty. +adb_cmd() { + "$ADB" ${ADB_SERIAL:+-s "$ADB_SERIAL"} "$@" +} + +# --- Target resolution ----------------------------------------------------- +read_cached_watch_serial() { + if [[ -f "$WATCH_SERIAL_FILE" ]]; then + cat "$WATCH_SERIAL_FILE" | tr -d '\r\n' + fi +} + +first_emulator_serial() { + detect_adb + "$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1; exit}' +} + +# Picks an attached watch by Wear OS device code. The Pixel Watch 2's +# device code is "eos"; the Pixel Watch 1 is "rohan". This is a fallback +# for first-time users who haven't run ./run.sh connect yet. +auto_detect_watch_serial() { + detect_adb + "$ADB" devices -l | awk '$2=="device" && /device:(eos|rohan)/ {print $1; exit}' +} + +apply_target() { + case "$DOCTATE_TARGET" in + watch) + if [[ -z "$ADB_SERIAL" ]]; then + ADB_SERIAL="$(read_cached_watch_serial)" + if [[ -z "$ADB_SERIAL" ]]; then + ADB_SERIAL="$(auto_detect_watch_serial || true)" + fi + if [[ -z "$ADB_SERIAL" ]]; then + die "No watch serial known. Run './run.sh connect ' first." + fi + fi + local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}" + GRADLE_PROPS+=("-Pdoctate.serverUrl=$server") + info "Target: watch (serial=$ADB_SERIAL, server=$server)" + ;; + emulator) + if [[ -z "$ADB_SERIAL" ]]; then + ADB_SERIAL="$(first_emulator_serial)" + if [[ -z "$ADB_SERIAL" ]]; then + die "No emulator attached. Start one first." + fi + fi + info "Target: emulator (serial=$ADB_SERIAL, server defaults to 10.0.2.2:3000)" + ;; + "") + : # no override; ADB / Gradle pick the first device + ;; + *) + die "Unknown target: '$DOCTATE_TARGET' (expected: watch | emulator)" + ;; + esac + + if [[ -n "${DOCTATE_API_KEY:-}" ]]; then + GRADLE_PROPS+=("-Pdoctate.apiKey=$DOCTATE_API_KEY") + fi + if [[ -n "$ADB_SERIAL" ]]; then + GRADLE_PROPS+=("-Pandroid.injected.device.serial=$ADB_SERIAL") + fi +} + ensure_device() { detect_adb + if [[ -n "$ADB_SERIAL" ]]; then + # Pinned device must actually be attached. + if ! "$ADB" devices | awk 'NR>1 {print $1}' | grep -qx "$ADB_SERIAL"; then + die "Pinned device '$ADB_SERIAL' is not attached. Check './run.sh devices'." + fi + return + fi local count count=$("$ADB" devices | awk 'NR>1 && $2=="device"' | wc -l) if [[ "$count" -gt 0 ]]; then if [[ "$count" -gt 1 ]]; then info "Multiple devices attached; ADB will pick the first." + info "Tip: use './run.sh watch ...' or './run.sh emulator ...' to disambiguate." fi return fi @@ -135,7 +245,7 @@ start_emulator_and_wait() { disown info "Waiting for ADB to see the device..." - "$ADB" wait-for-device + adb_cmd wait-for-device info "Waiting for boot to complete (up to 180s)..." local boot="" @@ -147,7 +257,7 @@ start_emulator_and_wait() { fi sleep 2 elapsed=$((elapsed + 2)) - boot=$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true) + boot=$(adb_cmd shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true) done ok "AVD '$avd' is up (boot took ${elapsed}s)." } @@ -156,7 +266,7 @@ start_emulator_and_wait() { cmd_build() { detect_java info "Building debug APK..." - (cd "$SCRIPT_DIR" && ./gradlew :app:assembleDebug) + (cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:assembleDebug) ok "Built." } @@ -164,43 +274,78 @@ cmd_install() { detect_java ensure_device info "Installing on device..." - (cd "$SCRIPT_DIR" && ./gradlew :app:installDebug) + (cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:installDebug) ok "Installed." } cmd_start() { ensure_device info "Starting $PACKAGE/$ACTIVITY..." - "$ADB" shell am start -n "$PACKAGE/$ACTIVITY" >/dev/null + adb_cmd shell am start -n "$PACKAGE/$ACTIVITY" >/dev/null ok "Started." } cmd_stop() { ensure_device info "Stopping $PACKAGE..." - "$ADB" shell am force-stop "$PACKAGE" + adb_cmd shell am force-stop "$PACKAGE" ok "Stopped." } cmd_logcat() { ensure_device local pid - pid=$("$ADB" shell pidof "$PACKAGE" | tr -d '\r' | awk '{print $1}') + pid=$(adb_cmd shell pidof "$PACKAGE" | tr -d '\r' | awk '{print $1}') if [[ -z "$pid" ]]; then die "App $PACKAGE is not running. Use './run.sh start' first." fi info "Logcat for $PACKAGE (PID $pid). Ctrl-C to exit." - exec "$ADB" logcat --pid="$pid" + exec "$ADB" ${ADB_SERIAL:+-s "$ADB_SERIAL"} logcat --pid="$pid" } cmd_shot() { ensure_device local out="${1:-/tmp/wear_screen.png}" info "Screenshot -> $out" - "$ADB" exec-out screencap -p > "$out" + adb_cmd exec-out screencap -p > "$out" ok "Saved ($(wc -c < "$out") bytes)." } +cmd_devices() { + detect_adb + info "Attached devices:" + "$ADB" devices -l | sed '1d;/^$/d' | sed 's/^/ /' + if [[ -n "$ADB_SERIAL" ]]; then + info "Pinned by current target: $ADB_SERIAL" + fi + local cached + cached="$(read_cached_watch_serial)" + if [[ -n "$cached" ]]; then + info "Cached watch serial (.watch_serial): $cached" + fi +} + +cmd_connect() { + detect_adb + local target="${1:-}" + if [[ -z "$target" ]]; then + target="$(read_cached_watch_serial)" + if [[ -z "$target" ]]; then + die "Usage: ./run.sh connect (no cached serial in .watch_serial yet)." + fi + info "Reusing cached watch serial: $target" + fi + info "Connecting to $target..." + "$ADB" connect "$target" + # adb connect prints success or failure; verify it's actually 'device'. + if "$ADB" devices | awk 'NR>1 {print $1, $2}' | grep -qx "$target device"; then + printf '%s\n' "$target" > "$WATCH_SERIAL_FILE" + ok "Connected and cached as watch serial." + else + die "Connect did not yield a 'device' state for $target. Check WiFi-Debug pairing." + fi +} + cmd_clean() { detect_java info "Cleaning..." @@ -230,6 +375,14 @@ cmd_help() { } # --- Dispatch -------------------------------------------------------------- +# Optional target prefix (watch | emulator) before the subcommand. +if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then + DOCTATE_TARGET="$1" + shift +fi + +apply_target + case "${1:-}" in "") cmd_install; cmd_start ;; build) cmd_build ;; @@ -238,6 +391,8 @@ case "${1:-}" in stop) cmd_stop ;; logcat) cmd_logcat ;; shot) shift; cmd_shot "$@" ;; + devices) cmd_devices ;; + connect) shift; cmd_connect "$@" ;; test) cmd_test ;; clean) cmd_clean ;; -h|--help|help) cmd_help ;;