4d9b6126fb
Drop DOCTATE_DEV_SERVER, DOCTATE_API_KEY, and DEFAULT_DEV_SERVER — the profile file is the only source for URL + key now. refs #3
625 lines
22 KiB
Bash
Executable File
625 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build / install / run helper for the Wear OS app.
|
|
#
|
|
# Usage:
|
|
# ./run.sh <target> [profile] <subcommand> [args]
|
|
#
|
|
# A target prefix is REQUIRED for every device-bound subcommand. The script
|
|
# never silently picks "the first attached device" — if more than one candidate
|
|
# exists it asks, and if ADB_SERIAL/WEAR_AVD pins a choice it uses that.
|
|
#
|
|
# Target prefix (or DOCTATE_TARGET env var):
|
|
# watch <profile>
|
|
# Real Pixel Watch over ADB-WiFi. <profile> is the basename of
|
|
# a file in profiles.d/<profile>.sh that holds SERVER_URL,
|
|
# API_KEY, and ALLOWED_SERIALS (see profiles.d/*.sh.example).
|
|
# The build refuses to proceed if the attached watch's ADB
|
|
# serial is not listed in the profile's ALLOWED_SERIALS — this
|
|
# protects against installing the wrong key on the wrong watch.
|
|
# emulator Wear OS AVD on this host. No profile — uses the hardcoded
|
|
# emulator-loopback URL (10.0.2.2:3000) and the build's
|
|
# MISSING_API_KEY sentinel.
|
|
# - 0 emulators running → menu of AVDs from `emulator -list-avds`
|
|
# - ≥1 running → menu of running serials + "Start a new AVD"
|
|
# To bypass the menu pin the choice via ADB_SERIAL=<serial> or
|
|
# WEAR_AVD=<name>.
|
|
#
|
|
# Subcommands that need a target:
|
|
# (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)
|
|
# test run JVM unit tests, plus instrumented tests on the target
|
|
# (set SKIP_INSTRUMENTED=1 to skip the on-device tier)
|
|
#
|
|
# Target-free subcommands (run without a prefix):
|
|
# devices list attached devices
|
|
# 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.
|
|
# clean ./gradlew clean
|
|
# help show this help
|
|
#
|
|
# Env vars:
|
|
# DOCTATE_TARGET Same as the <target> prefix (watch|emulator).
|
|
# The CLI prefix takes precedence when both are set.
|
|
# DOCTATE_PROFILE Same as the [profile] positional. The CLI value wins.
|
|
# ADB_SERIAL Pin a specific device serial. Skips the menu entirely
|
|
# AND the profile allow-list (it is still verified — pinning
|
|
# to a non-allowed serial dies with the same refusal).
|
|
# WEAR_AVD AVD name to auto-boot when no emulator is attached
|
|
# (skips the AVD menu; useful for CI / scripted runs).
|
|
# SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no device 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"
|
|
|
|
# Populated by apply_target / parsed args. Empty = no override.
|
|
ADB_SERIAL="${ADB_SERIAL:-}"
|
|
DOCTATE_TARGET="${DOCTATE_TARGET:-}"
|
|
DOCTATE_PROFILE="${DOCTATE_PROFILE:-}"
|
|
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
|
|
}
|
|
|
|
# Interactive picker. Writes the chosen item to the global PICKED so the
|
|
# caller doesn't need $(...) (which would swallow `die` inside a subshell).
|
|
# Aborts (via die) on bad input or when stdin is not a TTY.
|
|
# Usage: pick_from_list "Prompt" "label1" "label2" ...; use "$PICKED"
|
|
PICKED=""
|
|
pick_from_list() {
|
|
local prompt="$1"; shift
|
|
local -a items=("$@")
|
|
if [[ ! -t 0 ]]; then
|
|
die "$prompt need a TTY to prompt; pin via ADB_SERIAL / WEAR_AVD instead. Items: ${items[*]}"
|
|
fi
|
|
info "$prompt"
|
|
local i
|
|
for i in "${!items[@]}"; do
|
|
printf " %d) %s\n" "$((i+1))" "${items[$i]}"
|
|
done
|
|
printf " 0) abort\n"
|
|
local choice
|
|
read -r -p "Choose: " 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 "${#items[@]}" ]]; then
|
|
die "Out of range: $choice (have ${#items[@]} items)."
|
|
fi
|
|
PICKED="${items[$((choice-1))]}"
|
|
}
|
|
|
|
# Lists all running emulator serials (one per line).
|
|
list_emulator_serials() {
|
|
detect_adb
|
|
"$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1}'
|
|
}
|
|
|
|
# Lists attached Wear OS watch serials (one per line). Pixel Watch 2
|
|
# reports device code "eos", Pixel Watch 1 is "rohan".
|
|
list_watch_serials() {
|
|
detect_adb
|
|
"$ADB" devices -l | awk '$2=="device" && /device:(eos|rohan)/ {print $1}'
|
|
}
|
|
|
|
# Human-readable label for a serial, built from `adb devices -l` metadata.
|
|
# Falls back to the bare serial if no extra info is available.
|
|
serial_label() {
|
|
local serial="$1"
|
|
local line
|
|
line=$("$ADB" devices -l | awk -v s="$serial" '$1==s {$1=""; sub(/^ +/,""); print}')
|
|
if [[ -n "$line" ]]; then
|
|
printf '%s — %s\n' "$serial" "$line"
|
|
else
|
|
printf '%s\n' "$serial"
|
|
fi
|
|
}
|
|
|
|
# Output variable for the resolve_*_serial functions. Using a global
|
|
# instead of stdout+$() lets `die` inside these functions abort the main
|
|
# shell directly — $() would wrap everything in a subshell and swallow
|
|
# the exit, which is how the earlier version silently leaked past errors.
|
|
RESOLVED_SERIAL=""
|
|
|
|
# Resolves the emulator serial the user wants to target. The script never
|
|
# auto-picks a running emulator — even when only one is up, the user may
|
|
# want to boot a different AVD instead. Bypass the menu by setting
|
|
# ADB_SERIAL=<serial> or WEAR_AVD=<name>.
|
|
# - 0 emulators running → boot one (WEAR_AVD env or AVD menu); take its serial.
|
|
# - ≥1 emulators running → always prompt; "Start a new AVD" is the last option.
|
|
resolve_emulator_serial() {
|
|
detect_adb
|
|
local -a running
|
|
mapfile -t running < <(list_emulator_serials)
|
|
|
|
if [[ "${#running[@]}" -eq 0 ]]; then
|
|
boot_emulator_or_die
|
|
mapfile -t running < <(list_emulator_serials)
|
|
if [[ "${#running[@]}" -eq 0 ]]; then
|
|
die "Boot reported success but no emulator serial visible."
|
|
fi
|
|
RESOLVED_SERIAL="${running[0]}"
|
|
return
|
|
fi
|
|
|
|
local -a labels=()
|
|
local s
|
|
for s in "${running[@]}"; do
|
|
labels+=("$(serial_label "$s")")
|
|
done
|
|
local start_new_label="Start a new AVD"
|
|
labels+=("$start_new_label")
|
|
|
|
pick_from_list "Pick emulator:" "${labels[@]}"
|
|
local choice="$PICKED"
|
|
|
|
if [[ "$choice" == "$start_new_label" ]]; then
|
|
local -a before=("${running[@]}")
|
|
boot_emulator_or_die
|
|
local -a after
|
|
mapfile -t after < <(list_emulator_serials)
|
|
local new=""
|
|
local a p found
|
|
for a in "${after[@]}"; do
|
|
found=0
|
|
for p in "${before[@]}"; do
|
|
[[ "$p" == "$a" ]] && { found=1; break; }
|
|
done
|
|
if [[ "$found" -eq 0 ]]; then
|
|
new="$a"
|
|
break
|
|
fi
|
|
done
|
|
[[ -z "$new" ]] && die "Could not identify newly booted emulator serial."
|
|
RESOLVED_SERIAL="$new"
|
|
return
|
|
fi
|
|
|
|
# Chosen label has the form "emulator-5554 — <metadata>"; strip to serial.
|
|
RESOLVED_SERIAL="${choice%% *}"
|
|
}
|
|
|
|
# Resolves the watch serial to target. Prefers the cached pairing when
|
|
# it's actually attached, falls back to a menu on ambiguity.
|
|
resolve_watch_serial() {
|
|
detect_adb
|
|
local -a attached
|
|
mapfile -t attached < <(list_watch_serials)
|
|
local cached
|
|
cached="$(read_cached_watch_serial)"
|
|
|
|
if [[ -n "$cached" ]]; then
|
|
local s
|
|
for s in "${attached[@]}"; do
|
|
if [[ "$s" == "$cached" ]]; then
|
|
RESOLVED_SERIAL="$cached"
|
|
return
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "${#attached[@]}" -eq 0 ]]; then
|
|
if [[ -n "$cached" ]]; then
|
|
die "Cached watch serial '$cached' is not attached. Try './run.sh connect $cached' or './run.sh connect <ip:port>' for a fresh pairing."
|
|
fi
|
|
die "No watch attached. Run './run.sh connect <ip:port>' first."
|
|
fi
|
|
|
|
if [[ "${#attached[@]}" -eq 1 ]]; then
|
|
RESOLVED_SERIAL="${attached[0]}"
|
|
return
|
|
fi
|
|
|
|
local -a labels=()
|
|
local s
|
|
for s in "${attached[@]}"; do
|
|
labels+=("$(serial_label "$s")")
|
|
done
|
|
pick_from_list "Multiple watches attached — pick one:" "${labels[@]}"
|
|
RESOLVED_SERIAL="${PICKED%% *}"
|
|
}
|
|
|
|
# --- Profile loading (watch target only) -----------------------------------
|
|
# Profiles live in profiles.d/<name>.sh and define SERVER_URL, API_KEY, and
|
|
# ALLOWED_SERIALS as a bash array. The .sh files are gitignored — see
|
|
# profiles.d/example-profile.sh.example for the format.
|
|
|
|
PROFILE_NAME=""
|
|
SERVER_URL=""
|
|
API_KEY=""
|
|
ALLOWED_SERIALS=()
|
|
|
|
load_profile() {
|
|
local name="$1"
|
|
local file="$SCRIPT_DIR/profiles.d/$name.sh"
|
|
if [[ ! -f "$file" ]]; then
|
|
local available
|
|
available="$(cd "$SCRIPT_DIR/profiles.d" 2>/dev/null && \
|
|
ls *.sh 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
|
|
[[ -z "$available" ]] && available="<none — create one in profiles.d/ from the .sh.example template>"
|
|
die "Unknown profile '$name'. Available: ${available}"
|
|
fi
|
|
# Reset to detect missing fields in the profile file.
|
|
SERVER_URL=""; API_KEY=""; ALLOWED_SERIALS=()
|
|
# shellcheck disable=SC1090
|
|
source "$file"
|
|
[[ -n "$SERVER_URL" ]] || die "Profile '$name': SERVER_URL missing"
|
|
[[ -n "$API_KEY" ]] || die "Profile '$name': API_KEY missing"
|
|
[[ "${#ALLOWED_SERIALS[@]}" -gt 0 ]] || die "Profile '$name': ALLOWED_SERIALS empty"
|
|
PROFILE_NAME="$name"
|
|
}
|
|
|
|
# Aborts (via die) if $1 is not present in ALLOWED_SERIALS. The error message
|
|
# names BOTH the attached serial and the allow-list, so the user can see at a
|
|
# glance whether they grabbed the wrong watch or typed the wrong profile.
|
|
verify_serial_allowed() {
|
|
local serial="$1"
|
|
local s
|
|
for s in "${ALLOWED_SERIALS[@]}"; do
|
|
[[ "$s" == "$serial" ]] && return 0
|
|
done
|
|
die "Watch serial '$serial' is NOT in ALLOWED_SERIALS for profile '$PROFILE_NAME'.
|
|
Allowed: ${ALLOWED_SERIALS[*]}
|
|
This refusal protects against installing the wrong key on the wrong watch
|
|
(e.g. Brummel-key on Krey's watch → patient data goes to dev server)."
|
|
}
|
|
|
|
apply_target() {
|
|
case "$DOCTATE_TARGET" in
|
|
watch)
|
|
[[ -n "$DOCTATE_PROFILE" ]] || die \
|
|
"Watch target requires a profile. Usage: ./run.sh watch <profile> <subcmd>
|
|
Available profiles: $(cd "$SCRIPT_DIR/profiles.d" 2>/dev/null && ls *.sh 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
|
|
load_profile "$DOCTATE_PROFILE"
|
|
if [[ -z "$ADB_SERIAL" ]]; then
|
|
resolve_watch_serial
|
|
ADB_SERIAL="$RESOLVED_SERIAL"
|
|
fi
|
|
verify_serial_allowed "$ADB_SERIAL"
|
|
GRADLE_PROPS+=(
|
|
"-Pdoctate.serverUrl=$SERVER_URL"
|
|
"-Pdoctate.apiKey=$API_KEY"
|
|
"-Pdoctate.profileName=$DOCTATE_PROFILE"
|
|
)
|
|
info "Target: watch (profile=$DOCTATE_PROFILE, serial=$ADB_SERIAL, server=$SERVER_URL)"
|
|
;;
|
|
emulator)
|
|
if [[ -z "$ADB_SERIAL" ]]; then
|
|
resolve_emulator_serial
|
|
ADB_SERIAL="$RESOLVED_SERIAL"
|
|
fi
|
|
info "Target: emulator (serial=$ADB_SERIAL, server defaults to 10.0.2.2:3000)"
|
|
;;
|
|
"")
|
|
die "No target selected. Use './run.sh watch …' or './run.sh emulator …' (or set DOCTATE_TARGET)."
|
|
;;
|
|
*)
|
|
die "Unknown target: '$DOCTATE_TARGET' (expected: watch | emulator)"
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$ADB_SERIAL" ]]; then
|
|
GRADLE_PROPS+=("-Pandroid.injected.device.serial=$ADB_SERIAL")
|
|
# AGP's `android.injected.device.serial` silently falls back to "all
|
|
# attached devices" when the serial doesn't match exactly (notably
|
|
# with mDNS-TLS serials like adb-<hwid>-<suffix>._adb-tls-connect._tcp).
|
|
# ANDROID_SERIAL works one level deeper on every spawned adb binary,
|
|
# so the filter holds even when AGP's own matcher doesn't.
|
|
export ANDROID_SERIAL="$ADB_SERIAL"
|
|
fi
|
|
}
|
|
|
|
ensure_device() {
|
|
detect_adb
|
|
# apply_target must have resolved a serial before any device-bound command runs.
|
|
if [[ -z "$ADB_SERIAL" ]]; then
|
|
die "Internal error: ADB_SERIAL not set. This is a bug in run.sh dispatch."
|
|
fi
|
|
if ! "$ADB" devices | awk 'NR>1 && $2=="device" {print $1}' | grep -qx "$ADB_SERIAL"; then
|
|
die "Pinned device '$ADB_SERIAL' is no longer attached. Check './run.sh devices'."
|
|
fi
|
|
}
|
|
|
|
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"
|
|
|
|
# -no-snapshot-load forces a cold boot. Quick Boot's RAM snapshot freezes
|
|
# the wallclock from the last session, and Wear OS in-AVD NTP often does
|
|
# not catch up — case timestamps end up days in the past. Cold boot
|
|
# picks up the host RTC at startup, no in-app correction needed.
|
|
info "Starting AVD '$avd' (cold boot, background)..."
|
|
nohup "$emulator_bin" -avd "$avd" -no-snapshot-load >/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 --------------------------------------------------------------
|
|
# Required target prefix (watch | emulator) before the subcommand. The legacy
|
|
# "(none)" fallback is gone: every device-bound subcommand must know which
|
|
# device it's talking to, otherwise we'd quietly pick "the first" — which is
|
|
# exactly what the user asked to stop doing.
|
|
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
|
|
DOCTATE_TARGET="$1"
|
|
shift
|
|
# The watch target requires a profile name as the next positional. The
|
|
# emulator target does not — it always uses the hardcoded loopback URL.
|
|
if [[ "$DOCTATE_TARGET" == "watch" ]]; then
|
|
# Target-free subcommands that follow `watch` directly (none today,
|
|
# but kept defensively) should not be misread as a profile name.
|
|
case "${1:-}" in
|
|
""|help|-h|--help|devices|connect|clean)
|
|
# No profile, fall through — apply_target will die with a
|
|
# clear usage message if the subcommand actually needs one.
|
|
;;
|
|
*)
|
|
DOCTATE_PROFILE="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
# Subcommands that are device- and target-agnostic bypass apply_target so
|
|
# they don't trigger a resolver menu as a side-effect. Ordering matters:
|
|
# this short-circuit must run before apply_target.
|
|
case "${1:-}" in
|
|
help|-h|--help) cmd_help; exit 0 ;;
|
|
devices) cmd_devices; exit 0 ;;
|
|
connect) shift; cmd_connect "$@"; exit 0 ;;
|
|
clean) cmd_clean; exit 0 ;;
|
|
esac
|
|
|
|
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 "$@" ;;
|
|
test) cmd_test ;;
|
|
*) die "Unknown command: $1 (try './run.sh help')" ;;
|
|
esac
|