From 40b8dad48c5d291a4d892aa6dd52fb5543865870 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Tue, 11 Aug 2026 11:30:38 +0200 Subject: [PATCH] fix(adb): redial the port that actually worked, not always 5555 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `redial` hardcoded 5555, which is where `adb tcpip` always lands — but Android 11+ *Wireless debugging* picks a RANDOM port and keeps it while the toggle stays on. That matters beyond tidiness: Wireless debugging is the only way to run adb with **USB debugging off**, and plenty of banking/DRM apps refuse to start while `Settings.Global.adb_enabled` is 1. So the configuration a user needs in order to keep those apps working is exactly the one where redial could never reconnect: after a Wi-Fi roam, a suspend or an `adb kill-server`, `scan_transports` finds nothing, the redial knocks on a port nobody is listening on, and Universal Control arms with no cursor ever reaching the phone — silent on both screens. `scan_transports` now notes the port of whatever network transport is attached (`rsplit(':')`, so the bracketed IPv6 form works too) and persists it to ~/.cache/vortex/last_adb_port, mirroring how lan.rs caches the peer IP — the phone keeps that port across our own restarts, so memory alone would forget it exactly when it is needed. Redial then tries the remembered port first and 5555 second, so: - a Wireless-debugging port is reconnected without a cable; - a stale entry (phone re-paired, or moved back to `adb tcpip`) still recovers on the same pass rather than needing a manual reconnect; - and a success on either port becomes the next first guess, so the cache self-corrects. The 5555 fallback is skipped when it IS the remembered port — each miss costs about a second of `adb connect` blocking. Verified end-to-end by planting a bogus port and dropping the transport: tried :39999, fell back to :5555, reconnected, and rewrote the cache. `redial_port_order` is split out as the pure half so the ordering is covered by a unit test without a global or the filesystem in the way. --- linux/ui-tauri/src-tauri/src/mirror_inject.rs | 123 ++++++++++++++++-- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/linux/ui-tauri/src-tauri/src/mirror_inject.rs b/linux/ui-tauri/src-tauri/src/mirror_inject.rs index 75a5452..f64de79 100644 --- a/linux/ui-tauri/src-tauri/src/mirror_inject.rs +++ b/linux/ui-tauri/src-tauri/src/mirror_inject.rs @@ -63,9 +63,75 @@ struct Injector { /// Which device `adb` should talk to, remembered between calls. static ADB_SERIAL: Mutex> = Mutex::new(None); -/// The port `adb tcpip` puts the phone's daemon on, and the one [`redial`] dials. +/// The port `adb tcpip` puts the phone's daemon on, and [`redial`]'s last resort. const WIRELESS_PORT: u16 = 5555; +/// Port the most recent network transport was actually attached on. +/// +/// `adb tcpip` always lands on [`WIRELESS_PORT`], but Android 11+ *Wireless +/// debugging* — the only way to run adb with **USB debugging switched off**, +/// which many banking/DRM apps insist on — picks a RANDOM port and keeps it for +/// as long as the toggle stays on. Assuming 5555 there means [`redial`] can +/// never get back on after a Wi-Fi roam or a suspend: `scan_transports` finds +/// nothing, the redial dials a port nobody is listening on, and Universal +/// Control arms with no cursor ever appearing on the phone. +/// +/// Persisted, because the phone keeps that port across our own restarts. +static LAST_ADB_PORT: Mutex> = Mutex::new(None); + +fn adb_port_path() -> Option { + let mut p = std::path::PathBuf::from(std::env::var_os("HOME")?); + p.push(".cache/vortex/last_adb_port"); + Some(p) +} + +/// Note the port a network transport is attached on, in memory and on disk. +fn remember_adb_port(port: u16) { + let changed = { + let mut g = LAST_ADB_PORT.lock().unwrap_or_else(|e| e.into_inner()); + let changed = *g != Some(port); + *g = Some(port); + changed + }; + if changed { + if let Some(p) = adb_port_path() { + let _ = vortex_l3_daemon::core::fs_private::write_private( + &p, + port.to_string().as_bytes(), + ); + } + tracing::debug!(port, "mirror inject: remembered wireless adb port"); + } +} + +/// Ports [`redial`] should try, best guess first and never duplicated. +/// +/// The remembered port comes first; 5555 stays as a fallback so a stale +/// remembered port (phone re-paired, Wireless debugging toggled off and the +/// legacy `adb tcpip` used instead) still recovers on the same pass. +fn redial_ports() -> Vec { + let remembered = LAST_ADB_PORT + .lock() + .ok() + .and_then(|g| *g) + .or_else(|| { + adb_port_path() + .and_then(|p| std::fs::read_to_string(p).ok()) + .and_then(|s| s.trim().parse::().ok()) + }); + redial_port_order(remembered) +} + +/// The pure half of [`redial_ports`], split out so the ordering is testable +/// without a global or the filesystem in the way. +fn redial_port_order(remembered: Option) -> Vec { + match remembered { + Some(p) if p != WIRELESS_PORT => vec![p, WIRELESS_PORT], + Some(p) => vec![p], + None => vec![WIRELESS_PORT], + } +} + /// Floor between redial attempts: `adb connect` blocks for about a second when /// nothing answers, and [`adb_serial`] sits in front of every injector call. const REDIAL_COOLDOWN: Duration = Duration::from_secs(10); @@ -87,6 +153,15 @@ fn scan_transports() -> Option { let slot = if serial.contains(':') { &mut net } else { &mut usb }; slot.get_or_insert_with(|| serial.to_string()); } + // Whatever port the network transport is on is the one worth redialling — + // `rsplit` also handles the bracketed IPv6 form (`[::1]:37129`). + if let Some(port) = net + .as_deref() + .and_then(|n| n.rsplit(':').next()) + .and_then(|p| p.parse::().ok()) + { + remember_adb_port(port); + } usb.or(net) } @@ -116,19 +191,28 @@ fn redial() -> bool { else { return false; }; - let target = format!("{ip}:{WIRELESS_PORT}"); - // `adb connect` exits 0 even when it fails ("failed to connect to …"), so - // the stdout text is the only honest signal. "already connected to" counts. - let ok = Command::new("adb") - .args(["connect", &target]) - .output() - .is_ok_and(|o| String::from_utf8_lossy(&o.stdout).contains("connected to")); - if ok { - tracing::info!("mirror inject: wireless adb redialled at {target}"); - } else { - tracing::debug!("mirror inject: no answer on {target} (needs `adb tcpip 5555` once)"); + for port in redial_ports() { + let target = format!("{ip}:{port}"); + // `adb connect` exits 0 even when it fails ("failed to connect to …"), so + // the stdout text is the only honest signal. "already connected to" counts. + let ok = Command::new("adb") + .args(["connect", &target]) + .output() + .is_ok_and(|o| String::from_utf8_lossy(&o.stdout).contains("connected to")); + if ok { + // Also covers succeeding on the fallback: that port is now the one + // to try first next time. + remember_adb_port(port); + tracing::info!("mirror inject: wireless adb redialled at {target}"); + return true; + } + tracing::debug!("mirror inject: no answer on {target}"); } - ok + tracing::debug!( + "mirror inject: wireless adb unreachable — needs `adb tcpip 5555` once, or \ + Wireless debugging paired (Android 11+, works with USB debugging OFF)" + ); + false } /// Pick the phone to address, and pin every adb call to it with `-s`. @@ -647,7 +731,18 @@ pub fn stop() { #[cfg(test)] mod tests { - use super::PointerCurve; + use super::{redial_port_order, PointerCurve, WIRELESS_PORT}; + + #[test] + fn redial_tries_the_remembered_port_before_5555() { + // Wireless-debugging port: try it first, keep 5555 as the fallback for a + // phone that has since gone back to `adb tcpip`. + assert_eq!(redial_port_order(Some(37129)), vec![37129, WIRELESS_PORT]); + // Nothing remembered yet — the legacy port is the only sensible guess. + assert_eq!(redial_port_order(None), vec![WIRELESS_PORT]); + // Already 5555: don't dial the same port twice (each miss costs ~1 s). + assert_eq!(redial_port_order(Some(WIRELESS_PORT)), vec![WIRELESS_PORT]); + } /// Android's side of the bargain: what a delta of `sent` pixels, arriving /// `dt` after the last one, actually moves the cursor by.