Files
doctate/watch/wearos/run.sh
T
Brummel 3429e8bdf7 feat(watch): support real Pixel Watch 2 via ADB-WiFi + LAN dev server
Real hardware has no USB and can't resolve 10.0.2.2, so we pair over
ADB-WiFi and point the app at the dev laptop's LAN URL. run.sh gains a
`watch`/`emulator` target prefix so both devices can be attached in
parallel without serial-picking roulette.

- network_security_config.xml: whitelist 192.168.178.27 for cleartext
- build.gradle.kts: resolveProp() threads Gradle CLI `-P` overrides
  into BuildConfig (SERVER_URL, API_KEY) ahead of local.properties
- run.sh: adb_cmd wrapper honours ADB_SERIAL; new `devices` and
  `connect [ip:port]` subcommands; `.watch_serial` caches the paired
  endpoint so `./run.sh watch install` just works on subsequent runs
- .gitignore: ignore `.watch_serial`
2026-04-23 17:54:15 +02:00

401 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build / install / run helper for the Wear OS app.
#
# Usage:
# ./run.sh [target] <subcommand> [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
# install ./gradlew :app:installDebug
# start adb shell am start ...
# 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)
set -euo pipefail
# --- Config ----------------------------------------------------------------
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
C_INFO=$'\e[1;34m'; C_OK=$'\e[1;32m'; C_ERR=$'\e[1;31m'; C_OFF=$'\e[0m'
else
C_INFO=""; C_OK=""; C_ERR=""; C_OFF=""
fi
info() { printf "%s==>%s %s\n" "$C_INFO" "$C_OFF" "$*"; }
ok() { printf "%s OK%s %s\n" "$C_OK" "$C_OFF" "$*"; }
die() { printf "%sERR%s %s\n" "$C_ERR" "$C_OFF" "$*" >&2; exit 1; }
# --- Toolchain detection ---------------------------------------------------
detect_java() {
if [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then
return
fi
for candidate in /opt/android-studio/jbr /usr/lib/jvm/java-21-openjdk; do
if [[ -x "$candidate/bin/java" ]]; then
export JAVA_HOME="$candidate"
return
fi
done
die "No JDK found. Set JAVA_HOME or install jdk21-openjdk."
}
detect_adb() {
if [[ -n "${ADB:-}" && -x "$ADB" ]]; then return; fi
if [[ -x "$HOME/Android/Sdk/platform-tools/adb" ]]; then
ADB="$HOME/Android/Sdk/platform-tools/adb"
elif command -v adb >/dev/null 2>&1; then
ADB="$(command -v adb)"
else
die "adb not found. Expected ~/Android/Sdk/platform-tools/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 <ip:port>' 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
boot_emulator_or_die
}
boot_emulator_or_die() {
local emulator_bin="$HOME/Android/Sdk/emulator/emulator"
if [[ ! -x "$emulator_bin" ]]; then
die "No device attached and emulator not found at $emulator_bin."
fi
local -a avds
mapfile -t avds < <("$emulator_bin" -list-avds 2>/dev/null)
if [[ "${#avds[@]}" -eq 0 ]]; then
die "No device attached and no AVDs configured. Create one in Android Studio."
fi
local chosen=""
# Env-var override: skip the menu (CI / scripted runs).
if [[ -n "${WEAR_AVD:-}" ]]; then
local a
for a in "${avds[@]}"; do
if [[ "$a" == "$WEAR_AVD" ]]; then
chosen="$WEAR_AVD"
break
fi
done
if [[ -z "$chosen" ]]; then
die "WEAR_AVD='$WEAR_AVD' not in available AVDs: ${avds[*]}"
fi
elif [[ -t 0 ]]; then
info "No device attached. Available AVDs:"
local i
for i in "${!avds[@]}"; do
printf " %d) %s\n" "$((i+1))" "${avds[$i]}"
done
printf " 0) abort\n"
local choice
read -r -p "Start which? " choice
if [[ ! "$choice" =~ ^[0-9]+$ ]]; then
die "Invalid choice: '$choice' (expected a number)."
fi
if [[ "$choice" -eq 0 ]]; then
die "Aborted by user."
fi
if [[ "$choice" -lt 1 || "$choice" -gt "${#avds[@]}" ]]; then
die "Out of range: $choice (have ${#avds[@]} AVDs)."
fi
chosen="${avds[$((choice-1))]}"
else
die "No device attached. Set WEAR_AVD=<name> or run interactively. Available: ${avds[*]}"
fi
start_emulator_and_wait "$emulator_bin" "$chosen"
}
start_emulator_and_wait() {
local emulator_bin="$1"
local avd="$2"
info "Starting AVD '$avd' (background)..."
nohup "$emulator_bin" -avd "$avd" >/dev/null 2>&1 &
disown
info "Waiting for ADB to see the device..."
adb_cmd wait-for-device
info "Waiting for boot to complete (up to 180s)..."
local boot=""
local elapsed=0
local timeout=180
until [[ "$boot" == "1" ]]; do
if [[ "$elapsed" -ge "$timeout" ]]; then
die "Boot timeout after ${timeout}s (AVD '$avd')."
fi
sleep 2
elapsed=$((elapsed + 2))
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)."
}
# --- Subcommands -----------------------------------------------------------
cmd_build() {
detect_java
info "Building debug APK..."
(cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:assembleDebug)
ok "Built."
}
cmd_install() {
detect_java
ensure_device
info "Installing on device..."
(cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:installDebug)
ok "Installed."
}
cmd_start() {
ensure_device
info "Starting $PACKAGE/$ACTIVITY..."
adb_cmd shell am start -n "$PACKAGE/$ACTIVITY" >/dev/null
ok "Started."
}
cmd_stop() {
ensure_device
info "Stopping $PACKAGE..."
adb_cmd shell am force-stop "$PACKAGE"
ok "Stopped."
}
cmd_logcat() {
ensure_device
local pid
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" ${ADB_SERIAL:+-s "$ADB_SERIAL"} logcat --pid="$pid"
}
cmd_shot() {
ensure_device
local out="${1:-/tmp/wear_screen.png}"
info "Screenshot -> $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 <ip:port> (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..."
(cd "$SCRIPT_DIR" && ./gradlew clean)
ok "Clean."
}
cmd_test() {
detect_java
info "Running JVM unit tests..."
(cd "$SCRIPT_DIR" && ./gradlew :app:testDebugUnitTest)
ok "Unit tests passed."
if [[ "${SKIP_INSTRUMENTED:-0}" == "1" ]]; then
info "SKIP_INSTRUMENTED=1 set; skipping connectedAndroidTest."
return
fi
ensure_device
info "Running instrumented tests..."
(cd "$SCRIPT_DIR" && ./gradlew :app:connectedDebugAndroidTest)
ok "Instrumented tests passed."
}
cmd_help() {
# Print the leading comment block (lines 2..first non-comment line).
awk 'NR==1 { next } /^[^[:space:]#]/ { exit } { print }' "$0" \
| sed 's/^# \{0,1\}//'
}
# --- 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 ;;
install) cmd_install ;;
start) cmd_start ;;
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 ;;
*) die "Unknown command: $1 (try './run.sh help')" ;;
esac