From 2a5b46a2124967fdefeb1413c7ffccaa650e1b95 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 10 Jul 2026 15:36:53 +0200 Subject: [PATCH] feat(dev): improve wireless ADB reconnect and mDNS probing Align in-app dev helpers with adb-reconnect pairing flow and expand tests for Android 11+ TLS wireless debugging. Co-authored-by: Cursor --- .../androidcast/dev/DevAdbConnectInfo.java | 34 +- .../foxx/androidcast/dev/DevAdbMdnsProbe.java | 82 +++ .../androidcast/dev/DevAdbWifiHelper.java | 28 + .../androidcast/dev/DevAdbWifiHelperTest.java | 6 + scripts/adb-reconnect.sh | 648 +++++++++++++++++- 5 files changed, 769 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevAdbConnectInfo.java b/app/src/main/java/com/foxx/androidcast/dev/DevAdbConnectInfo.java index 03460c1..a3e78f0 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevAdbConnectInfo.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevAdbConnectInfo.java @@ -26,6 +26,7 @@ public final class DevAdbConnectInfo { public final boolean adbWifiEnabled; public final boolean secureSettingsGranted; public final int legacyPort; + public final int pairingPort; public final long updatedMs; public final RaControlEndpoint raControl; @@ -50,7 +51,7 @@ public final class DevAdbConnectInfo { boolean secureSettingsGranted, int legacyPort, long updatedMs) { - this(ips, connect, adbEnabled, adbWifiEnabled, secureSettingsGranted, legacyPort, updatedMs, null); + this(ips, connect, adbEnabled, adbWifiEnabled, secureSettingsGranted, legacyPort, -1, updatedMs, null); } public DevAdbConnectInfo( @@ -62,12 +63,26 @@ public final class DevAdbConnectInfo { int legacyPort, long updatedMs, RaControlEndpoint raControl) { + this(ips, connect, adbEnabled, adbWifiEnabled, secureSettingsGranted, legacyPort, -1, updatedMs, raControl); + } + + public DevAdbConnectInfo( + List ips, + List connect, + boolean adbEnabled, + boolean adbWifiEnabled, + boolean secureSettingsGranted, + int legacyPort, + int pairingPort, + long updatedMs, + RaControlEndpoint raControl) { this.ips = ips != null ? ips : Collections.emptyList(); this.connect = connect != null ? connect : Collections.emptyList(); this.adbEnabled = adbEnabled; this.adbWifiEnabled = adbWifiEnabled; this.secureSettingsGranted = secureSettingsGranted; this.legacyPort = legacyPort; + this.pairingPort = pairingPort; this.updatedMs = updatedMs; this.raControl = raControl; } @@ -90,6 +105,16 @@ public final class DevAdbConnectInfo { root.put("connect", toJsonArray(connect)); root.put("tls_port", findFirstNonLegacyPort(connect)); + if (pairingPort > 0) { + root.put("pairing_port", pairingPort); + JSONArray pairCmds = new JSONArray(); + for (String ip : ips) { + if (!TextUtils.isEmpty(ip)) { + pairCmds.put("adb pair " + ip + ":" + pairingPort); + } + } + root.put("pair_commands", pairCmds); + } root.put("needs_secure_grant", !secureSettingsGranted); JSONArray commands = new JSONArray(); for (String endpoint : connect) { @@ -98,8 +123,11 @@ public final class DevAdbConnectInfo { } } root.put("commands", commands); - root.put("hint", "Grant WRITE_SECURE_SETTINGS once via USB; use tls_port " - + "(not 5555) on Android 11+. Host: avahi-browse -rpt _adb-tls-connect._tcp"); + root.put("hint", "Android 11+ wireless adb: pair this PC once (adb pair IP:pairing_port CODE), " + + "then adb connect IP:tls_port. Open Wireless debugging → Pair device for the 6-digit code. " + + "Host: avahi-browse -rpt _adb-tls-connect._tcp"); + root.put("connect_note", "If adb connect fails with 'failed to connect', the PC is not paired — " + + "use pair_commands while the pairing dialog is open on the device."); if (raControl != null && raControl.port > 0) { JSONObject ra = new JSONObject(); ra.put("port", raControl.port); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevAdbMdnsProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevAdbMdnsProbe.java index 5be8de0..447c91d 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevAdbMdnsProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevAdbMdnsProbe.java @@ -24,6 +24,88 @@ final class DevAdbMdnsProbe { private DevAdbMdnsProbe() {} + static List discoverPairingPorts(Context context, long timeoutMs) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + return new ArrayList<>(); + } + NsdManager nsd = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + if (nsd == null) { + return new ArrayList<>(); + } + Set ports = new LinkedHashSet<>(); + Set localIps = new LinkedHashSet<>(DevAdbWifiHelper.collectWifiIpv4Addresses(context)); + CountDownLatch done = new CountDownLatch(1); + AtomicBoolean stopped = new AtomicBoolean(false); + + NsdManager.DiscoveryListener listener = new NsdManager.DiscoveryListener() { + @Override + public void onDiscoveryStarted(String serviceType) {} + + @Override + public void onServiceFound(NsdServiceInfo serviceInfo) { + nsd.resolveService(serviceInfo, new NsdManager.ResolveListener() { + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {} + + @Override + public void onServiceResolved(NsdServiceInfo info) { + int port = info.getPort(); + if (port <= 0 || port >= 65536) { + return; + } + String host = info.getHost() != null ? info.getHost().getHostAddress() : ""; + if (!localIps.isEmpty() + && host != null + && !host.isEmpty() + && !localIps.contains(host)) { + return; + } + ports.add(port); + } + }); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) {} + + @Override + public void onDiscoveryStopped(String serviceType) { + if (stopped.compareAndSet(false, true)) { + done.countDown(); + } + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + if (stopped.compareAndSet(false, true)) { + done.countDown(); + } + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + if (stopped.compareAndSet(false, true)) { + done.countDown(); + } + } + }; + + try { + nsd.discoverServices("_adb-tls-pairing._tcp", NsdManager.PROTOCOL_DNS_SD, listener); + if (!done.await(timeoutMs, TimeUnit.MILLISECONDS)) { + Log.d(DevAdbConnectInfo.LOG_TAG, "mDNS pairing discovery timed out"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + try { + nsd.stopServiceDiscovery(listener); + } catch (Exception ignored) { + } + } + return new ArrayList<>(ports); + } + static List discoverPorts(Context context, long timeoutMs) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return new ArrayList<>(); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java b/app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java index 4a421d9..1440286 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java @@ -59,6 +59,7 @@ public final class DevAdbWifiHelper { connect.addAll(parseDumpsysEndpoints(runDumpsysAdb(app))); List dedupedConnect = orderConnectEndpoints(dedupe(connect), ports); + int pairingPort = discoverPairingPort(app, runDumpsysAdb(app)); long now = System.currentTimeMillis(); DevAdbConnectInfo.RaControlEndpoint raControl = RemoteAccessControlClient.snapshotForAdb(app); return new DevAdbConnectInfo( @@ -68,6 +69,7 @@ public final class DevAdbWifiHelper { adbWifiEnabled, secure, DevAdbConnectInfo.LEGACY_TCP_PORT, + pairingPort, now, raControl); } @@ -282,6 +284,32 @@ public final class DevAdbWifiHelper { return new ArrayList<>(endpoints); } + /** Pairing port from {@code dumpsys adb} (wireless debug — use with {@code adb pair}, not connect). */ + static int parseDumpsysPairingPort(String dumpsys) { + if (TextUtils.isEmpty(dumpsys)) { + return -1; + } + Pattern pairing = Pattern.compile("(?i)pairing\\s+port[^0-9]{0,20}(\\d{2,5})"); + Matcher m = pairing.matcher(dumpsys); + if (m.find()) { + return parsePositiveInt(m.group(1)); + } + return -1; + } + + private static int discoverPairingPort(Context context, String dumpsys) { + int fromDumpsys = parseDumpsysPairingPort(dumpsys); + if (fromDumpsys > 0) { + return fromDumpsys; + } + for (Integer port : DevAdbMdnsProbe.discoverPairingPorts(context, 1500L)) { + if (port != null && port > 0) { + return port; + } + } + return -1; + } + private static List dedupe(List values) { Set out = new LinkedHashSet<>(); for (String value : values) { diff --git a/app/src/test/java/com/foxx/androidcast/dev/DevAdbWifiHelperTest.java b/app/src/test/java/com/foxx/androidcast/dev/DevAdbWifiHelperTest.java index ffe9c4d..8eb7597 100644 --- a/app/src/test/java/com/foxx/androidcast/dev/DevAdbWifiHelperTest.java +++ b/app/src/test/java/com/foxx/androidcast/dev/DevAdbWifiHelperTest.java @@ -19,6 +19,12 @@ public class DevAdbWifiHelperTest { assertEquals(1, endpoints.size()); } + @Test + public void parseDumpsysPairingPort_findsPairingPortLine() { + String dumpsys = "TLS port: 45139\nPairing port: 37265\n"; + assertEquals(37265, DevAdbWifiHelper.parseDumpsysPairingPort(dumpsys)); + } + @Test public void mergeEndpoints_buildsCartesianProduct() { List connect = DevAdbConnectInfo.mergeEndpoints( diff --git a/scripts/adb-reconnect.sh b/scripts/adb-reconnect.sh index 344dff5..2dd2266 100755 --- a/scripts/adb-reconnect.sh +++ b/scripts/adb-reconnect.sh @@ -1,43 +1,639 @@ #!/usr/bin/env bash -# ADB reconnection helpers. Source this file. -# Provides: adb_reconnect_init_hints, adb_reconnect_all, adb_http_status, adb_prepare_device_for_deploy, adb_update_connect_cache +# Wireless adb discovery + reconnect helpers (sourced by rebuild.sh). +# Priority: app HTTP :5039/adb.json → avahi → cache → mdns → :5555 hints. +# Only IPv4:PORT is used for adb connect (no mDNS service names). -DEV_ADB_HTTP_PORT="${DEV_ADB_HTTP_PORT:-5039}" -ADB_TCP_PROBE="${ADB_TCP_PROBE:-no}" +: "${ADB_CONNECT_RETRIES:=3}" +: "${ADB_CONNECT_DELAY_SEC:=1}" +: "${ADB_TCP_PROBE:=yes}" +: "${ADB_TCP_PROBE_SEC:=1}" +: "${DEV_ADB_HTTP_PORT:=5039}" +: "${ADB_CONNECT_CACHE:=${ROOT:-.}/.adb_connect_cache}" +: "${ADB_HTTP_CACHE_DIR:=${ROOT:-.}/.adb_http_cache}" +: "${ADB_RECONNECT_POKE_DELAY_SEC:=2}" +: "${ADB_MDNS_AUTO_CONNECT:=adb-tls-connect,adb}" +: "${ADB_KILL_SERVER:=no}" +: "${ADB_DISCONNECT_BEFORE_CONNECT:=no}" + +declare -gA CONNECTIONS_LIST_HINTS adb_reconnect_init_hints() { - : # Placeholder: load hints from local config if present + CONNECTIONS_LIST_HINTS["192_168_33_153"]="[A13] Android Projector" + CONNECTIONS_LIST_HINTS["192_168_33_39"]="[A16] Doogee Tab G6 Max / P8_EEA" + CONNECTIONS_LIST_HINTS["192_168_33_106"]="[A11] BlackView BL6000Pro / BL6000Pro_EEA" + CONNECTIONS_LIST_HINTS["192_168_33_173"]="[A11] BlackView BL6000Pro / BL6000Pro" } -adb_http_status() { - local ip="$1" - local port="${2:-$DEV_ADB_HTTP_PORT}" - local url="http://${ip}:${port}/adb.json" - local result - result="$(curl -sf --max-time 3 "$url" 2>/dev/null || echo "{}")" - echo "adb_http_status $ip: $result" +adb_hint_key_for_ip() { + local ip=${1} + ip="$(echo "${ip}" | sed -e 's@\.@_@g' -e 's@:.*@@' | tr -d '\n')" + printf '%s' "${ip}" } -adb_reconnect_all() { - local label="${1:-reconnect}" +adb_ip_for_hint_key() { + local ip=${1} + ip="$(echo "${ip}" | sed -e 's/_/./g' -e 's/:.*//' | tr -d '\n')" + printf '%s' "${ip}" +} - # Try any IPs registered via HTTP discovery - if [[ "$ADB_TCP_PROBE" == "yes" ]]; then - for ip in $(adb devices | awk 'NR>1 {print $1}' | grep -E '^[0-9]+\.' | cut -d: -f1 | sort -u); do - adb connect "$ip:5555" >/dev/null 2>&1 || true - done +adb_hint_for_endpoint() { + local endpoint=${1} + local key + key="$(adb_hint_key_for_ip "${endpoint}")" + printf '%s' "${CONNECTIONS_LIST_HINTS[${key}]:-}" +} + +adb_endpoint_host() { + local endpoint=${1} + if [[ "$endpoint" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+): ]]; then + printf '%s' "${BASH_REMATCH[1]}" + fi +} + +adb_is_connectable_ip_port() { + local endpoint=${1} + [[ "$endpoint" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+$ ]] +} + +adb_tcp_port_open() { + local endpoint=${1} + local host port + host="$(adb_endpoint_host "$endpoint")" + port="${endpoint##*:}" + [[ -n "$host" && "$port" =~ ^[0-9]+$ ]] || return 1 + if command -v nc >/dev/null 2>&1; then + nc -z -w "${ADB_TCP_PROBE_SEC}" "$host" "$port" >/dev/null 2>&1 + return $? + fi + timeout "${ADB_TCP_PROBE_SEC}" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null +} + +adb_collect_candidate_ips() { + local ip endpoint key + if [[ -f "$ADB_CONNECT_CACHE" ]]; then + while IFS= read -r endpoint; do + [[ -z "$endpoint" ]] && continue + ip="${endpoint%%:*}" + [[ -n "$ip" ]] && printf '%s\n' "$ip" + done < "$ADB_CONNECT_CACHE" + fi + for key in "${!CONNECTIONS_LIST_HINTS[@]}"; do + adb_ip_for_hint_key "$key" + done + adb devices -l 2>/dev/null | awk '/^[^ ]+ +device/ { print $1 }' | while read -r serial; do + [[ "$serial" == *:* ]] || continue + printf '%s\n' "${serial%%:*}" + done +} + +adb_json_valid() { + local json=${1} + [[ -n "$json" ]] || return 1 + printf '%s' "$json" | python3 -c 'import json,sys; json.loads(sys.stdin.read())' 2>/dev/null +} + +adb_json_connect_lines() { + local json=${1} + [[ -n "$json" ]] || return 0 + printf '%s' "$json" | python3 -c ' +import json, sys, re +raw = sys.stdin.read() +if not raw.strip(): + raise SystemExit(0) +try: + data = json.loads(raw) +except json.JSONDecodeError: + raise SystemExit(0) +IP_PORT = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}:\d+$") +seen = set() +def emit(ep): + if ep and IP_PORT.match(ep) and ep not in seen: + seen.add(ep) + print(ep) +tls = int(data.get("tls_port") or 0) +if tls > 0: + for ip in data.get("ips") or []: + if ip: + emit(f"{ip}:{tls}") +for item in data.get("connect") or []: + emit(item) +for item in data.get("commands") or []: + if isinstance(item, str) and item.startswith("adb connect "): + emit(item[len("adb connect "):]) +' 2>/dev/null || true +} + +adb_json_pairing_lines() { + local json=${1} + [[ -n "$json" ]] || return 0 + printf '%s' "$json" | python3 -c ' +import json, sys, re +raw = sys.stdin.read() +if not raw.strip(): + raise SystemExit(0) +try: + data = json.loads(raw) +except json.JSONDecodeError: + raise SystemExit(0) +IP_PORT = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}:\d+$") +seen = set() +def emit(ep): + if ep and IP_PORT.match(ep) and ep not in seen: + seen.add(ep) + print(ep) +pair = int(data.get("pairing_port") or 0) +if pair > 0: + for ip in data.get("ips") or []: + if ip: + emit(f"{ip}:{pair}") +for item in data.get("pair_commands") or []: + if isinstance(item, str) and item.startswith("adb pair "): + emit(item[len("adb pair "):]) +' 2>/dev/null || true +} + +adb_curl_adb_json() { + local ip=${1} + local json cached="${ADB_HTTP_CACHE_DIR}/${ip}.json" + json="$(curl -m 5 -sS "http://${ip}:${DEV_ADB_HTTP_PORT}/adb.json" 2>/dev/null || true)" + if adb_json_valid "$json"; then + mkdir -p "$ADB_HTTP_CACHE_DIR" + printf '%s' "$json" >"$cached" + printf '%s' "$json" + return 0 + fi + if [[ -f "$cached" ]] && adb_json_valid "$(cat "$cached")"; then + cat "$cached" + return 0 + fi + return 1 +} + +adb_fetch_http_endpoints() { + local ip json + for ip in $(adb_collect_candidate_ips | sort -u); do + [[ -z "$ip" ]] && continue + json="$(adb_curl_adb_json "$ip" 2>/dev/null || true)" + [[ -z "$json" ]] && continue + adb_json_connect_lines "$json" + done +} + +adb_discover_mdns_endpoints() { + local line hostport + while IFS= read -r line; do + [[ "$line" == List* ]] && continue + [[ "$line" == error:* ]] && continue + [[ "$line" == *"_adb"* ]] || continue + hostport="$(awk '{print $NF}' <<<"$line")" + if adb_is_connectable_ip_port "$hostport"; then + printf '%s\n' "$hostport" + fi + done < <(adb mdns services 2>&1 || true) +} + +adb_discover_avahi_endpoints() { + command -v avahi-browse >/dev/null 2>&1 || return 0 + local svc + for svc in _adb-tls-connect._tcp _adb._tcp; do + avahi-browse -rpt "$svc" 2>/dev/null | awk -F';' ' + $1 == "=" && $3 == "IPv4" && $8 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && $9 ~ /^[0-9]+$/ { + print $8 ":" $9 + }' + done +} + +adb_discover_avahi_pairing_endpoints() { + command -v avahi-browse >/dev/null 2>&1 || return 0 + avahi-browse -rpt _adb-tls-pairing._tcp 2>/dev/null | awk -F';' ' + $1 == "=" && $3 == "IPv4" && $8 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && $9 ~ /^[0-9]+$/ { + print $8 ":" $9 + }' +} + +adb_pull_device_json_endpoints() { + local serial json tmp + while IFS= read -r serial; do + [[ -z "$serial" ]] && continue + tmp="$(mktemp)" + if adb -s "$serial" exec-out run-as com.foxx.androidcast cat files/dev/adb_connect.json \ + >"$tmp" 2>/dev/null; then + json="$(cat "$tmp")" + rm -f "$tmp" + adb_json_connect_lines "$json" + else + rm -f "$tmp" + fi + done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }') +} + +adb_connect_failure_hint() { + local endpoint=${1} + local out=${2} + local host ip json pair_ep + host="$(adb_endpoint_host "$endpoint")" + [[ -n "$host" ]] || return 0 + if [[ "$out" != *"failed to connect"* && "$out" != *"failed to authenticate"* ]]; then + return 0 + fi + echo " hint: Android 11+ wireless adb requires one-time TLS pairing on this PC." >&2 + json="$(adb_curl_adb_json "$host" 2>/dev/null || true)" + if [[ -n "$json" ]]; then + while IFS= read -r pair_ep; do + [[ -z "$pair_ep" ]] && continue + echo " 1) On device: Developer options → Wireless debugging → Pair device with pairing code" >&2 + echo " 2) adb pair ${pair_ep} (enter 6-digit code when prompted)" >&2 + echo " 3) adb connect ${endpoint}" >&2 + return 0 + done < <(adb_json_pairing_lines "$json") + fi + while IFS= read -r pair_ep; do + [[ -z "$pair_ep" ]] && continue + echo " pairing (avahi): adb pair ${pair_ep}" >&2 + return 0 + done < <(adb_discover_avahi_pairing_endpoints | grep "^${host}:" || true) + echo " Open Wireless debugging → Pair device; then: ADB_PAIR_CODE=123456 $0 adb_pair ${host}" >&2 +} + +adb_pair_device() { + local ip=${1} + local code=${2:-${ADB_PAIR_CODE:-}} + local pair_ep json + [[ -n "$ip" ]] || { echo "usage: adb_pair IP [6-digit-code]" >&2; return 1; } + json="$(adb_curl_adb_json "$ip" 2>/dev/null || true)" + pair_ep="$(adb_json_pairing_lines "$json" 2>/dev/null | grep "^${ip}:" | head -1 || true)" + if [[ -z "$pair_ep" ]]; then + pair_ep="$(adb_discover_avahi_pairing_endpoints | grep "^${ip}:" | head -1 || true)" + fi + if [[ -z "$pair_ep" && -n "${ADB_PAIRING_PORT:-}" ]]; then + pair_ep="${ip}:${ADB_PAIRING_PORT}" + fi + if [[ -z "$pair_ep" ]]; then + echo "${ip}: no pairing port (open Wireless debugging → Pair device with pairing code on tablet)" >&2 + return 1 + fi + if [[ -z "$code" ]]; then + echo "${ip}: pair endpoint ${pair_ep}" >&2 + echo "Run: ADB_PAIR_CODE=123456 $0 adb_pair ${ip}" >&2 + return 1 + fi + echo "==> adb pair ${pair_ep}" >&2 + adb pair "${pair_ep}" "$code" +} + +adb_endpoint_rank() { + local e=${1} + if [[ "$e" == *":5555" ]]; then + printf '9' + else + printf '2' + fi +} + +# One endpoint per IPv4 host; keep best rank (HTTP/avahi TLS beats :5555). +adb_discover_endpoints() { + local endpoint host rank best_rank existing + declare -A seen=() + declare -A host_best=() + declare -A host_rank=() + local -a ordered=() + + consider_endpoint() { + local e=${1} + [[ -z "$e" ]] && return 0 + if [[ "$e" == *_* && "$e" != *":"* ]]; then + e="$(adb_ip_for_hint_key "$e")" + fi + [[ -z "$e" ]] && return 0 + if ! adb_is_connectable_ip_port "$e"; then + return 0 + fi + host="$(adb_endpoint_host "$e")" + rank="$(adb_endpoint_rank "$e")" + if [[ -n "$host" ]]; then + existing="${host_best[$host]:-}" + if [[ -n "$existing" ]]; then + best_rank="${host_rank[$host]}" + if [[ "$rank" -ge "$best_rank" ]]; then + return 0 + fi + seen["$existing"]=0 + for i in "${!ordered[@]}"; do + if [[ "${ordered[$i]}" == "$existing" ]]; then + unset 'ordered[i]' + fi + done + ordered=("${ordered[@]}") + fi + host_best["$host"]="$e" + host_rank["$host"]="$rank" + fi + [[ -n "${seen[$e]:-}" ]] && return 0 + seen["$e"]=1 + ordered+=("$e") + } + + # HTTP first — matches curl the user runs manually. + while IFS= read -r endpoint; do + consider_endpoint "$endpoint" + done < <(adb_fetch_http_endpoints) + + while IFS= read -r endpoint; do + consider_endpoint "$endpoint" + done < <(adb_discover_avahi_endpoints) + + if [[ -f "$ADB_CONNECT_CACHE" ]]; then + while IFS= read -r endpoint; do + consider_endpoint "$endpoint" + done < "$ADB_CONNECT_CACHE" fi - # mDNS / default discovery (adb already handles this) - true + while IFS= read -r endpoint; do + consider_endpoint "$endpoint" + done < <(adb_pull_device_json_endpoints) + + while IFS= read -r endpoint; do + consider_endpoint "$endpoint" + done < <(adb_discover_mdns_endpoints) + + local key ip + for key in "${!CONNECTIONS_LIST_HINTS[@]}"; do + ip="$(adb_ip_for_hint_key "$key")" + [[ -n "${host_best[$ip]:-}" ]] && continue + consider_endpoint "${ip}:5555" + done + + adb_filter_endpoints_by_tcp "${ordered[@]}" } -adb_prepare_device_for_deploy() { - local serial="$1" - # Disable screen lock prompts, maximize brightness — non-critical - adb -s "$serial" shell settings put global package_verifier_enable 0 >/dev/null 2>&1 || true +# Drop closed ports (fast nc/bash probe). If none open, return candidates anyway. +adb_filter_endpoints_by_tcp() { + local -a all=("$@") + local -a open=() + local e + if [[ "${ADB_TCP_PROBE}" =~ ^[nN] ]]; then + printf '%s\n' "${all[@]}" + return 0 + fi + for e in "${all[@]}"; do + [[ -z "$e" ]] && continue + if adb_tcp_port_open "$e"; then + open+=("$e") + else + echo " probe ${e} — closed (skip)" >&2 + fi + done + if [[ ${#open[@]} -gt 0 ]]; then + printf '%s\n' "${open[@]}" + else + printf '%s\n' "${all[@]}" + fi +} + +adb_poke_refresh_broadcast() { + local serial + while IFS= read -r serial; do + [[ -z "$serial" ]] && continue + adb -s "$serial" shell am broadcast \ + -a com.foxx.androidcast.dev.REFRESH_ADB_WIFI \ + -n com.foxx.androidcast/.dev.DevAdbWifiReceiver >/dev/null 2>&1 || true + done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }') +} + +# Wireless TLS adb often shows as adb-xxx._adb-tls-connect._tcp, not IP:port. +adb_resolve_device_serial() { + local endpoint=${1} + local serial state host count=0 last="" + if adb -s "$endpoint" get-state 2>/dev/null | grep -q '^device$'; then + printf '%s' "$endpoint" + return 0 + fi + host="$(adb_endpoint_host "$endpoint")" + while IFS= read -r serial state; do + [[ "$state" != "device" ]] && continue + count=$((count + 1)) + last="$serial" + if [[ "$serial" == "$endpoint" ]]; then + printf '%s' "$serial" + return 0 + fi + if [[ -n "$host" && "$serial" == *"$host"* ]]; then + printf '%s' "$serial" + return 0 + fi + done < <(adb devices 2>/dev/null | awk 'NR > 1 && NF >= 2 { print $1, $2 }') + if [[ "$count" -eq 1 && -n "$last" ]]; then + printf '%s' "$last" + return 0 + fi + return 1 +} + +adb_parse_connect_serial() { + local out=${1} + if [[ "$out" =~ [Cc]onnected\ to\ ([^[:space:]]+) ]]; then + printf '%s' "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} + +adb_host_has_device() { + local host=${1} + local serial + serial="$(adb_resolve_device_serial "${host}:1" 2>/dev/null || true)" + [[ -n "$serial" ]] +} + +adb_reconnect_endpoint() { + local endpoint="$1" + [[ -z "$endpoint" ]] && return 1 + if ! adb_is_connectable_ip_port "$endpoint"; then + echo " skip ${endpoint} — not IPv4:PORT" >&2 + return 1 + fi + local attempt out hint resolved + hint="$(adb_hint_for_endpoint "${endpoint}")" + [[ -n "${hint}" ]] && hint="(${hint})" + if [[ "${ADB_TCP_PROBE}" =~ [yY] ]] && ! adb_tcp_port_open "$endpoint"; then + echo " skip ${endpoint} — TCP closed ${hint}" >&2 + return 1 + fi + resolved="$(adb_resolve_device_serial "$endpoint" 2>/dev/null || true)" + if [[ -n "$resolved" ]]; then + echo " ${endpoint} already connected as ${resolved} ${hint}" >&2 + printf '%s' "$resolved" + return 0 + fi + echo " adb connect ${endpoint} ${hint}" >&2 + if [[ "${ADB_DISCONNECT_BEFORE_CONNECT}" =~ [yY] ]]; then + adb disconnect "$endpoint" >/dev/null 2>&1 || true + fi + for (( attempt = 1; attempt <= ADB_CONNECT_RETRIES; attempt++ )); do + out="$(adb connect "$endpoint" 2>&1)" || true + sleep 1 + resolved="$(adb_parse_connect_serial "$out" 2>/dev/null || true)" + if [[ -z "$resolved" ]]; then + resolved="$(adb_resolve_device_serial "$endpoint" 2>/dev/null || true)" + fi + if [[ -n "$resolved" ]]; then + echo " ${resolved} — ready (${out})" >&2 + printf '%s' "$resolved" + return 0 + fi + if [[ "$attempt" -lt "$ADB_CONNECT_RETRIES" ]]; then + sleep "$ADB_CONNECT_DELAY_SEC" + fi + done + echo " ${endpoint} — not reachable (${out})" >&2 + adb_connect_failure_hint "$endpoint" "$out" + return 1 } adb_update_connect_cache() { - : # Placeholder: persist known device IPs + local serial + mkdir -p "$(dirname "$ADB_CONNECT_CACHE")" + : >"$ADB_CONNECT_CACHE" + while IFS= read -r serial; do + adb_is_connectable_ip_port "$serial" || continue + echo "$serial" >>"$ADB_CONNECT_CACHE" + done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }') } + +adb_reconnect_prepare() { + export ADB_MDNS_AUTO_CONNECT + adb start-server >/dev/null 2>&1 || true + if [[ "${ADB_KILL_SERVER}" =~ [yY] ]]; then + adb kill-server >/dev/null 2>&1 || true + adb start-server >/dev/null 2>&1 || true + fi + adb reconnect offline >/dev/null 2>&1 || true +} + +adb_reconnect_all() { + local label=${1:-post-build} + echo "==> Reconnecting wireless adb targets (${label})" + adb_reconnect_prepare + adb_poke_refresh_broadcast + if [[ "${ADB_RECONNECT_POKE_DELAY_SEC:-0}" -gt 0 ]]; then + sleep "$ADB_RECONNECT_POKE_DELAY_SEC" + fi + + if [[ -n "${ADB_PAIR_CODE:-}" ]]; then + local _pair_ip + for _pair_ip in $(adb_collect_candidate_ips | sort -u); do + adb_pair_device "$_pair_ip" "$ADB_PAIR_CODE" && echo " paired ${_pair_ip}" >&2 || true + done + fi + + local endpoint connected=0 host resolved + declare -A connected_hosts=() + while IFS= read -r serial; do + host="$(adb_endpoint_host "$serial")" + [[ -n "$host" ]] && connected_hosts["$host"]="$serial" + done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }') + + while IFS= read -r endpoint; do + [[ -z "$endpoint" ]] && continue + host="$(adb_endpoint_host "$endpoint")" + if [[ -n "$host" && -n "${connected_hosts[$host]:-}" ]]; then + echo " skip ${endpoint} — ${host} already on ${connected_hosts[$host]}" + continue + fi + if resolved="$(adb_reconnect_endpoint "$endpoint")"; then + connected=$((connected + 1)) + host="$(adb_endpoint_host "$resolved")" + [[ -z "$host" ]] && host="$(adb_endpoint_host "$endpoint")" + [[ -n "$host" ]] && connected_hosts["$host"]="$resolved" + fi + done < <(adb_discover_endpoints) + + if [[ "$connected" -eq 0 ]]; then + echo " retry: HTTP-only pass" + while IFS= read -r endpoint; do + [[ -z "$endpoint" ]] && continue + if resolved="$(adb_reconnect_endpoint "$endpoint")"; then + connected=$((connected + 1)) + fi + done < <(adb_fetch_http_endpoints) + fi + + adb_update_connect_cache + adb devices -l + if [[ "$connected" -gt 0 ]]; then + return 0 + fi + return 1 +} + +adb_count_ready_devices() { + adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { c++ } END { print c+0 }' +} + +adb_prepare_device_for_deploy() { + local serial=${1:-} + local -a adb_args=() + [[ -n "$serial" ]] && adb_args=(-s "$serial") + adb "${adb_args[@]}" shell input keyevent KEYCODE_WAKEUP >/dev/null 2>&1 \ + || adb "${adb_args[@]}" shell input keyevent 224 >/dev/null 2>&1 \ + || true + adb "${adb_args[@]}" shell am broadcast \ + -a com.foxx.androidcast.dev.PREPARE_FOR_DEPLOY \ + -n com.foxx.androidcast/.dev.DebugDeployWakeReceiver >/dev/null 2>&1 || true +} + +adb_http_status() { + local ip=${1} + local json note + json="$(adb_curl_adb_json "$ip" 2>/dev/null || true)" + if [[ -z "$json" ]]; then + echo "${ip}: no response (app not running or port ${DEV_ADB_HTTP_PORT} closed)" + return 1 + fi + echo "${ip}: ok" + adb_json_connect_lines "$json" | sed 's/^/ connect /' + if adb_json_pairing_lines "$json" | grep -q .; then + adb_json_pairing_lines "$json" | sed 's/^/ pair /' + fi + note="$(printf '%s' "$json" | python3 -c 'import json,sys +try: + d=json.load(sys.stdin) + print(d.get("connect_note") or d.get("hint") or "", end="") +except Exception: + pass' 2>/dev/null || true)" + if [[ -n "$note" ]]; then + echo " note: ${note}" + fi +} + +# --- CLI (when executed, not sourced) --- +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + _ADB_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + ROOT="${ROOT:-$(cd "${_ADB_SCRIPT_DIR}/../ac-mobile-android" 2>/dev/null && pwd || pwd)}" + export ROOT + adb_reconnect_init_hints + cmd=${1:-help} + shift || true + case "$cmd" in + adb_http_status) adb_http_status "$@"; exit $? ;; + adb_reconnect_all) adb_reconnect_all "$@"; exit $? ;; + adb_pair) adb_pair_device "$@"; exit $? ;; + help|-h|--help) + cat < [args] + +Commands: + adb_http_status IP curl :5039/adb.json and show connect/pair endpoints + adb_reconnect_all [label] discover + adb connect (HTTP → avahi → cache) + adb_pair IP [CODE] adb pair (CODE or env ADB_PAIR_CODE; port from JSON/avahi) + +Env: DEV_ADB_HTTP_PORT=5039, ADB_PAIR_CODE=123456, ADB_PAIRING_PORT=NNNN, ROOT=project dir +EOF + exit 0 + ;; + *) + echo "Unknown command: $cmd (try help)" >&2 + exit 1 + ;; + esac +fi