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 ;;