tools: beacon fixes — cleartext, multi-device, validated-net POST, status URL

Debugging against a real restricted LAN surfaced three fixes:
- Android blocks app cleartext HTTP by default (targetSdk 36) — the port
  discovery + reachability were fine (phone curl reached fmr), only the
  app POST was denied. Added usesCleartextTraffic for this dev tool.
- Multi-device: report + receiver are keyed by device (Build.MODEL) so a
  phone and tablet don't clobber each other; connector connects each.
- POST over a VALIDATED internet network (prefer cellular) since the
  wireless-debug wifi is often a restricted LAN.
- Status/notification now show the target beacon URL + which network, per
  the request to surface what it's connecting to.
Verified live: beacon tracks the (frequently rotating) port via mDNS and
self-reports the current endpoint within seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-07-31 22:07:35 +02:00
co-authored by Claude Opus 5
parent 52748ac853
commit 38d4440412
4 changed files with 75 additions and 25 deletions
@@ -13,6 +13,7 @@
<application
android:allowBackup="false"
android:label="Echolot ADB Beacon"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.Material.Light">
<activity android:name=".MainActivity" android:exported="true">
@@ -90,9 +90,16 @@ class BeaconService : Service() {
val ip = wifiIpv4() ?: run { Status.set("no wlan0 IPv4 (is wifi up?)"); return }
val port = currentPort
if (port <= 0) { Status.set("$ip — waiting for wireless-debug port"); return }
val body = """{"ip":"$ip","port":$port}"""
val device = android.os.Build.MODEL.replace(Regex("[^A-Za-z0-9_.-]"), "_")
val body = """{"device":"$device","ip":"$ip","port":$port}"""
// Send over a VALIDATED internet network — the wireless-debug wifi is often a restricted
// LAN with no real internet TCP egress, so bind the report to cellular/whatever actually
// reaches the beacon.
val net = internetNetwork()
val ok = runCatching {
(URL(BuildConfig.BEACON_URL).openConnection() as HttpURLConnection).run {
val url = URL(BuildConfig.BEACON_URL)
val conn = (net?.openConnection(url) ?: url.openConnection()) as HttpURLConnection
conn.run {
requestMethod = "POST"
connectTimeout = 5000; readTimeout = 5000
doOutput = true
@@ -102,9 +109,40 @@ class BeaconService : Service() {
responseCode == 200
}
}.getOrDefault(false)
val line = if (ok) "reported $ip:$port" else "report failed for $ip:$port (beacon unreachable?)"
val via = if (net != null) "via ${netLabel(net)}" else "via default net"
val line = if (ok) {
"reported [$device] $ip:$port$via\n${BuildConfig.BEACON_URL}"
} else {
"report FAILED for [$device] $ip:$port $via\n→ POST ${BuildConfig.BEACON_URL}"
}
Status.set(line)
updateNotification(line)
updateNotification(if (ok) "reported $ip:$port" else "report failed for $ip:$port")
}
/** A network with validated internet access, preferring cellular (the wireless-debug wifi is
* frequently a restricted LAN that can't reach the beacon over TCP). */
private fun internetNetwork(): android.net.Network? {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
var fallback: android.net.Network? = null
for (n in cm.allNetworks) {
val c = cm.getNetworkCapabilities(n) ?: continue
if (!c.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET)) continue
if (!c.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED)) continue
if (c.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR)) return n
fallback = n
}
return fallback
}
private fun netLabel(n: android.net.Network): String {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
val c = cm.getNetworkCapabilities(n) ?: return "net"
return when {
c.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
c.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
c.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
else -> "net"
}
}
/** The wlan0 (or first private) IPv4 the PC routes to. */