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`
This commit is contained in:
@@ -13,3 +13,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
.watch_serial
|
||||
|
||||
@@ -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")}\""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<!-- Allow cleartext only for the Android emulator's host loopback,
|
||||
where the local Axum dev server runs. Production HTTPS targets
|
||||
remain locked down by base-config. -->
|
||||
<!-- Allow cleartext only for development targets:
|
||||
- 10.0.2.2 is the Android emulator's host loopback alias.
|
||||
- 192.168.178.27 is the dev laptop on the home LAN, reachable
|
||||
from the real Pixel Watch over WiFi.
|
||||
Production HTTPS targets remain locked down by base-config. -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">192.168.178.27</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
+164
-9
@@ -1,6 +1,20 @@
|
||||
#!/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
|
||||
@@ -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 <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
|
||||
@@ -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 <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..."
|
||||
@@ -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 ;;
|
||||
|
||||
Reference in New Issue
Block a user