From 30ee8be82e44d1a8250d1f6e27e71f7d5f24aec5 Mon Sep 17 00:00:00 2001 From: Lena Date: Wed, 1 Jul 2026 00:00:00 +0000 Subject: app: enter the device address as one host:port field The form asked for a host, a pairing port and a connection port as three separate fields. Nothing on screen says which port is which, and the two are only distinguishable if the reader already knows how adb pair works on Android 11+. Take the connection endpoint the way the target prints it, as a single "ip:port" string copied off the Wireless debugging screen, and leave the pairing dialog with just its port and code. Devices.parseAddress() does the split and rejects anything malformed rather than guessing: an unbracketed IPv6 literal would otherwise be split at the wrong colon. Reported on the F-Droid submission (fdroiddata!41394). --- app/src/main/java/invalid/lena/scrcpy/Devices.java | 44 +++++++++++++++- app/src/main/java/invalid/lena/scrcpy/Main.java | 58 ++++++++++------------ 2 files changed, 70 insertions(+), 32 deletions(-) (limited to 'app/src/main/java/invalid/lena/scrcpy') diff --git a/app/src/main/java/invalid/lena/scrcpy/Devices.java b/app/src/main/java/invalid/lena/scrcpy/Devices.java index c3e5141..0ba61a3 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Devices.java +++ b/app/src/main/java/invalid/lena/scrcpy/Devices.java @@ -30,9 +30,13 @@ public final class Devices { this.port = port; } + // Bracket IPv6 literals so the port stays unambiguous. Round-trips + // through parseAddress(). @Override public String toString() { - return host + ":" + port; + return host.indexOf(':') < 0 + ? host + ":" + port + : "[" + host + "]:" + port; } @Override @@ -49,6 +53,44 @@ public final class Devices { private Devices() {} + // Parse "host:port" exactly as the target's Wireless debugging screen + // prints it. IPv6 literals must be bracketed ("[fe80::1]:5555") because + // the address itself contains colons; an unbracketed one is rejected + // rather than silently split at the wrong colon. Returns null on + // anything malformed - the only caller is a text field. + public static Device parseAddress(String s) { + if (s == null) return null; + s = s.trim(); + String host, port; + if (s.startsWith("[")) { + int end = s.indexOf(']'); + if (end < 0 || !s.startsWith("]:", end)) return null; + host = s.substring(1, end); + port = s.substring(end + 2); + } else { + int colon = s.indexOf(':'); + if (colon < 0 || colon != s.lastIndexOf(':')) return null; + host = s.substring(0, colon); + port = s.substring(colon + 1); + } + int p = parsePort(port); + if (host.isEmpty() || p < 0) return null; + return new Device(host, p); + } + + // Digits only, 1-65535. Returns -1 if it is not a usable port. + // Integer.parseInt() alone would accept "+5555" and " 5555". + public static int parsePort(String s) { + if (s == null) return -1; + s = s.trim(); + if (s.isEmpty() || s.length() > 5) return -1; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) < '0' || s.charAt(i) > '9') return -1; + } + int p = Integer.parseInt(s); + return p >= 1 && p <= 65535 ? p : -1; + } + // Pure-java parse: returns whatever rows are well-formed; logs and // skips anything malformed instead of nuking the list. public static List parse(String json) { diff --git a/app/src/main/java/invalid/lena/scrcpy/Main.java b/app/src/main/java/invalid/lena/scrcpy/Main.java index eb8d400..28a0870 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Main.java +++ b/app/src/main/java/invalid/lena/scrcpy/Main.java @@ -18,12 +18,14 @@ import java.util.List; // Pairing form + saved-device list. // -// Layout: host, pair-port, pair-code, connect-port + a single "Pair and save" -// button. After a successful pair() against the daemon, the row is appended -// to devices.json with the *connect* port (different from the pair port on -// Android 11+). Tapping a saved row launches the Mirror activity with the -// target host/port; Mirror owns its own Adb instance loaded from the same -// on-disk keypair. +// Layout: device address ("ip:port"), pair-port, pair-code + a single +// "Pair and save" button. The address is the connect endpoint printed on +// the target's Wireless debugging screen; pairing happens on a different, +// short-lived port from the pairing dialog, against the same host. After a +// successful pair() against the daemon, the row is appended to devices.json +// with the *connect* port. Tapping a saved row launches the Mirror activity +// with the target host/port; Mirror owns its own Adb instance loaded from +// the same on-disk keypair. public final class Main extends Activity { private volatile Adb adb; @@ -41,10 +43,9 @@ public final class Main extends Activity { Ui.padForInsets(findViewById(R.id.root), WindowInsets.Type.systemBars() | WindowInsets.Type.ime()); - EditText host = findViewById(R.id.host); + EditText address = findViewById(R.id.device_address); EditText pairPort = findViewById(R.id.pair_port); EditText pairCode = findViewById(R.id.pair_code); - EditText connectPort = findViewById(R.id.connect_port); pairButton = findViewById(R.id.pair); View settingsBtn = findViewById(R.id.settings); ListView devices = findViewById(R.id.devices); @@ -89,7 +90,7 @@ public final class Main extends Activity { Devices.Device d = adapter.getItem(pos); new AlertDialog.Builder(this) .setTitle(R.string.forget_device) - .setMessage(d.host + ":" + d.port) + .setMessage(d.toString()) .setPositiveButton(android.R.string.ok, (dlg, w) -> { Log.i("forget device: %s", d); try { @@ -110,41 +111,36 @@ public final class Main extends Activity { pairButton.setOnClickListener(v -> { if (adb == null) return; // still initialising - String h = host.getText().toString().trim(); - String pp = pairPort.getText().toString().trim(); - String pc = pairCode.getText().toString().trim(); - String cp = connectPort.getText().toString().trim(); - if (TextUtils.isEmpty(h) || TextUtils.isEmpty(pp) - || TextUtils.isEmpty(pc) || TextUtils.isEmpty(cp)) { - Toast.makeText(this, "fill all four fields", Toast.LENGTH_SHORT).show(); + // The saved endpoint is the address field verbatim; pairing + // reuses its host with the pairing port. + Devices.Device target = Devices.parseAddress(address.getText().toString()); + if (target == null) { + Toast.makeText(this, R.string.bad_address, Toast.LENGTH_LONG).show(); return; } - int pairP, connP; - try { - pairP = Integer.parseInt(pp); - connP = Integer.parseInt(cp); - } catch (NumberFormatException e) { - Toast.makeText(this, "ports must be numeric", Toast.LENGTH_SHORT).show(); + int pairP = Devices.parsePort(pairPort.getText().toString()); + if (pairP < 0) { + Toast.makeText(this, R.string.bad_pair_port, Toast.LENGTH_LONG).show(); return; } - if (pairP < 1 || pairP > 65535 || connP < 1 || connP > 65535) { - Toast.makeText(this, "ports must be 1-65535", Toast.LENGTH_SHORT).show(); + String pc = pairCode.getText().toString().trim(); + if (TextUtils.isEmpty(pc)) { + Toast.makeText(this, R.string.bad_pair_code, Toast.LENGTH_LONG).show(); return; } pairButton.setEnabled(false); Toast.makeText(this, R.string.pairing, Toast.LENGTH_SHORT).show(); - new Thread(() -> pairAndSave(h, pairP, pc, connP, pairButton), "pair").start(); + new Thread(() -> pairAndSave(target, pairP, pc, pairButton), "pair").start(); }); } - private void pairAndSave(String host, int pairPort, String code, int connPort, Button btn) { + private void pairAndSave(Devices.Device target, int pairPort, String code, Button btn) { try { - Log.i("pair: %s:%d", host, pairPort); - boolean ok = adb.pair(host, pairPort, code); + Log.i("pair: %s:%d", target.host, pairPort); + boolean ok = adb.pair(target.host, pairPort, code); if (!ok) throw new IllegalStateException("pair returned false"); - Log.i("pair ok host=%s pair_port=%d", host, pairPort); - Devices.Device d = new Devices.Device(host, connPort); - List updated = Devices.upsert(this, d); + Log.i("pair ok host=%s pair_port=%d", target.host, pairPort); + List updated = Devices.upsert(this, target); runOnUiThread(() -> { adapter.clear(); adapter.addAll(updated); -- cgit v1.2.3