app: put the relay switch where it is looked for, and fix why it found nothing
The relay was buried at the bottom of settings. It is now a switch on the home screen - not a third chip beside Quick and Long, because those choose how the next run measures while this starts a service that outlives it and produces no document at all. Debug builds only: it publishes where this device can be reached over adb, which is scaffolding for driving a test device and a footgun in a release build. And it never worked. localIp() read WifiManager.connectionInfo.ipAddress, deprecated and returning 0 to ordinary apps on current Android, so the own-device check compared every advertisement against null and rejected all of them - the service ran, reported healthy, and relayed nothing. Now read from LinkProperties, and when this device genuinely cannot say what its own address is the endpoint is relayed anyway, labelled unverified: silence was the worse answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c414534e03
commit
97515f7090
@@ -4,9 +4,9 @@
|
||||
package app.echo_lot.app
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import java.net.Inet4Address
|
||||
|
||||
/**
|
||||
@@ -102,12 +102,18 @@ class AdbRelay(
|
||||
// reachable one from a developer's subnet is the routable v4 address, and it is
|
||||
// also the only one worth relaying — a link-local address means nothing off-link.
|
||||
if (i.host !is Inet4Address) return
|
||||
// Only this device's own advertisement: on a shared network several phones may
|
||||
// Prefer this device's own advertisement: on a shared network several phones may
|
||||
// have wireless debugging on, and relaying a neighbour's port would send a
|
||||
// developer to the wrong device.
|
||||
if (host != localIp()) return
|
||||
// developer to the wrong device. But when this device cannot say what its own
|
||||
// address is, that is no reason to relay nothing — an unverified endpoint beats
|
||||
// silence, and it is labelled so it is never mistaken for a confirmed one.
|
||||
val mine = localIp()
|
||||
if (mine != null && host != mine) return
|
||||
lastEndpoint = Endpoint(host, i.port, i.serviceName ?: "adb")
|
||||
onEvent("found adbd at $host:${i.port}")
|
||||
onEvent(
|
||||
if (mine == null) "found adbd at $host:${i.port} (own address unknown)"
|
||||
else "found adbd at $host:${i.port}"
|
||||
)
|
||||
}
|
||||
}
|
||||
runCatching { nsd?.resolveService(info, cb) }
|
||||
@@ -117,17 +123,21 @@ class AdbRelay(
|
||||
}
|
||||
}
|
||||
|
||||
/** This device's own IPv4 address on the wifi it is relaying from. */
|
||||
/**
|
||||
* This device's own IPv4 address on the network it is relaying from, or null if it cannot be
|
||||
* determined.
|
||||
*
|
||||
* Read from LinkProperties rather than `WifiManager.connectionInfo.ipAddress`, which is
|
||||
* deprecated and returns 0 to ordinary apps on current Android — a null that silently made the
|
||||
* ownership check reject every advertisement, so the relay found nothing and said nothing.
|
||||
*/
|
||||
private fun localIp(): String? = runCatching {
|
||||
val wifi = ctx.getSystemService(WifiManager::class.java) ?: return null
|
||||
@Suppress("DEPRECATION")
|
||||
val ip = wifi.connectionInfo.ipAddress
|
||||
if (ip == 0) return null
|
||||
@Suppress("DEPRECATION")
|
||||
String.format(
|
||||
"%d.%d.%d.%d",
|
||||
ip and 0xff, ip shr 8 and 0xff, ip shr 16 and 0xff, ip shr 24 and 0xff,
|
||||
)
|
||||
val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return null
|
||||
val lp = cm.getLinkProperties(cm.activeNetwork) ?: return null
|
||||
lp.linkAddresses.map { it.address }
|
||||
.filterIsInstance<Inet4Address>()
|
||||
.firstOrNull { !it.isLoopbackAddress }
|
||||
?.hostAddress
|
||||
}.getOrNull()
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -73,6 +73,9 @@ class MainActivity : ComponentActivity() {
|
||||
// there is no back stack to model beyond "return to the run screen".
|
||||
var screen by remember { mutableStateOf(Screen.RUN) }
|
||||
var preview by remember { mutableStateOf<String?>(null) }
|
||||
// Hoisted so the home-screen switch and the settings toggle cannot disagree
|
||||
// about whether the relay is on.
|
||||
var relayOn by remember { mutableStateOf(vm.settings.adbRelayEnabled) }
|
||||
// Automation entry point:
|
||||
// adb shell am start -n app.echo_lot.app/.MainActivity --ez autorun true
|
||||
// starts a run immediately and uploads the report, so an unattended
|
||||
@@ -283,6 +286,19 @@ class MainActivity : ComponentActivity() {
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
relayOn = relayOn,
|
||||
// Dev builds only. The relay publishes where this device can be reached
|
||||
// over adb; that is scaffolding for driving a test device, and a release
|
||||
// build has no business offering it — it would be useless without
|
||||
// wireless debugging and a footgun for anyone who switched it on without
|
||||
// knowing what it announces.
|
||||
relayAvailable = BuildConfig.DEBUG && vm.settings.serverConfigured,
|
||||
onRelayToggle = { on ->
|
||||
relayOn = on
|
||||
vm.settings.adbRelayEnabled = on
|
||||
if (on) AdbRelayService.start(this@MainActivity)
|
||||
else AdbRelayService.stop(this@MainActivity)
|
||||
},
|
||||
onExport = { doc -> startActivity(Intent.createChooser(Report.share(this, doc), "Export Echolot run")) },
|
||||
onOpenSettings = { screen = Screen.SETTINGS },
|
||||
onOpenHistory = { vm.refreshHistory(); screen = Screen.HISTORY },
|
||||
@@ -347,6 +363,10 @@ private fun EcholotScreen(
|
||||
/** Which discovery listeners are selected, and how to change one. */
|
||||
discoveryEnabled: (String) -> Boolean,
|
||||
onDiscoveryChange: (String, Boolean) -> Unit,
|
||||
/** The relay is not a run mode, but it is switched on from here because that is where it is looked for. */
|
||||
relayOn: Boolean,
|
||||
relayAvailable: Boolean,
|
||||
onRelayToggle: (Boolean) -> Unit,
|
||||
onRun: (RunMode) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onShizukuAction: () -> Unit,
|
||||
@@ -473,6 +493,42 @@ private fun EcholotScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Deliberately NOT a third chip beside Quick and Long. Those choose how the next run
|
||||
// measures; this starts a service that keeps running afterwards and produces no document
|
||||
// at all, so putting it in the same row would promise that "Run measurement" starts it.
|
||||
// It lives here anyway because here is where it gets looked for.
|
||||
var relay by remember { mutableStateOf(relayOn) }
|
||||
if (relayAvailable || relay) Card(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
"Relay adb endpoint" + if (relay) " — on" else "",
|
||||
fontSize = 13.sp, fontWeight = FontWeight.Medium,
|
||||
color = LocalContentColor.current.copy(alpha = if (relayAvailable) 1f else 0.5f),
|
||||
)
|
||||
Text(
|
||||
if (!relayAvailable) {
|
||||
"Needs an enrolled server."
|
||||
} else if (relay) {
|
||||
"Reporting this device's wireless-debug host:port to the server."
|
||||
} else {
|
||||
"Not a measurement: lets a developer on another network find this " +
|
||||
"device when the port rotates."
|
||||
},
|
||||
fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = relay,
|
||||
enabled = relayAvailable,
|
||||
onCheckedChange = { on -> relay = on; onRelayToggle(on) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Button(onClick = { onRun(mode) }, enabled = !state.running) {
|
||||
Text(if (state.running) "Running…" else "Run measurement")
|
||||
|
||||
@@ -388,10 +388,11 @@ fun SettingsScreen(
|
||||
|
||||
// ---- dev relay ------------------------------------------------------------------
|
||||
//
|
||||
// Last, and deliberately plain: this is scaffolding for driving a test device, not a
|
||||
// measurement. It publishes where this device can be reached over adb, which is why it
|
||||
// is off until someone decides otherwise.
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
// Debug builds only, and duplicated as a switch on the home screen because that is where
|
||||
// it is reached for. It publishes where this device can be reached over adb: scaffolding
|
||||
// for driving a test device, useless without wireless debugging, and not something a
|
||||
// release build should offer at all.
|
||||
if (BuildConfig.DEBUG) Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Developer relay", style = MaterialTheme.typography.titleMedium)
|
||||
Toggle(
|
||||
|
||||
Reference in New Issue
Block a user