app: link.ra_source router identification + brand icons + DEV build variant
link.ra_source answers "who advertises IPv6 here, and which box is it": RA source per network, MAC recovered from the modified-EUI-64 link-local (privacy addresses reported as such, not guessed), vendor via a curated OUI table, UPnP/SSDP M-SEARCH for the gateway's server banner + device description (manufacturer/model/friendly name), and reverse DNS. All SSDP responders are recorded so a rogue RA sender that isn't the gateway can still be matched; the MAC accompanies every identity source as the hook for future LLDP/mDNS cross-matching. UI gains a "Router / IPv6 advertiser" panel. Icons: branding adaptive icon converted to vector drawables (+ PNG mipmaps, monochrome layer). The debug build is now a separate app — applicationIdSuffix .dev, label "Echolot DEV", DEV-badged icon — so it installs alongside a production build and can't be confused with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -25,6 +25,12 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
release { isMinifyEnabled = false }
|
||||
debug {
|
||||
// The dev build is a separate app: own package (installs alongside a real
|
||||
// Echolot), own label and a DEV-badged icon (src/debug/res).
|
||||
applicationIdSuffix = ".dev"
|
||||
versionNameSuffix = "-dev"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 948 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
Debug foreground: the production mark with a DEV ribbon so the dev build is
|
||||
unmistakable on the launcher next to a real install. -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<item android:drawable="@drawable/ic_dev_badge"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground_dev"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground_dev"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Echolot DEV</string>
|
||||
</resources>
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="Echolot"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.Echolot">
|
||||
|
||||
@@ -138,6 +138,8 @@ private fun Results(doc: MeasurementDocument) {
|
||||
FlowCategories(summary.categories)
|
||||
}
|
||||
|
||||
RouterPanel(doc)
|
||||
|
||||
SectionTitle("Networks (${doc.networks.size})")
|
||||
for (n in doc.networks) {
|
||||
Text("• ${n.transport.name.lowercase()} ${n.iface ?: ""} — " +
|
||||
@@ -175,6 +177,53 @@ private fun Results(doc: MeasurementDocument) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Who advertises IPv6 here, and what box is it? Pulled from the link.ra_source evidence and shown
|
||||
* up front — a rogue/misconfigured RA sender is a top cause of broken IPv6, and "some router" is
|
||||
* not actionable without an identity.
|
||||
*/
|
||||
@Composable
|
||||
private fun RouterPanel(doc: MeasurementDocument) {
|
||||
val test = doc.tests.firstOrNull { it.type == TestType.LINK_RA_SOURCE } ?: return
|
||||
val nets = (test.evidence?.get("networks") as? kotlinx.serialization.json.JsonArray) ?: return
|
||||
if (nets.isEmpty()) return
|
||||
|
||||
SectionTitle("Router / IPv6 advertiser")
|
||||
for (el in nets) {
|
||||
val o = el as? kotlinx.serialization.json.JsonObject ?: continue
|
||||
fun f(k: String): String? =
|
||||
(o[k] as? kotlinx.serialization.json.JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
|
||||
val ra = f("ra_source")
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(f("network") ?: "network", fontWeight = FontWeight.Medium)
|
||||
// The identity line: vendor/model if we could pin it down.
|
||||
val identity = listOfNotNull(
|
||||
f("upnp_manufacturer"), f("upnp_model"), f("ra_source_vendor"),
|
||||
).distinct().joinToString(" · ").ifBlank { null }
|
||||
identity?.let {
|
||||
Text(it, fontWeight = FontWeight.SemiBold, color = Color(0xFF35E0C4), fontSize = 15.sp)
|
||||
}
|
||||
f("upnp_friendly_name")?.let { Row0("name", it) }
|
||||
ra?.let { Row0("RA source", it) }
|
||||
f("ra_source_mac")?.let { Row0("RA source MAC", it) }
|
||||
f("ra_source_reverse_dns")?.takeIf { it != "(none)" }?.let { Row0("reverse DNS", it) }
|
||||
f("v4_gateway")?.let { Row0("IPv4 gateway", it) }
|
||||
f("v4_gateway_reverse_dns")?.takeIf { it != "(none)" }?.let { Row0("gateway rDNS", it) }
|
||||
f("upnp_server")?.let { Row0("UPnP server", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Row0(label: String, value: String) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("$label:", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(value, fontSize = 12.sp, fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FlowCategories(categories: Map<String, CategorySummary>) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import app.echo_lot.probe.StunProbe
|
||||
import app.echo_lot.probe.NetworkInventory
|
||||
import app.echo_lot.probe.Probe
|
||||
import app.echo_lot.probe.ProbeIds
|
||||
import app.echo_lot.probe.RouterIdentityProbe
|
||||
import app.echo_lot.shizuku.ShizukuProbe
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -82,6 +83,7 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
val probes: List<Probe> = listOf(
|
||||
LinkSnapshotProbe(entries),
|
||||
RouterIdentityProbe(entries),
|
||||
IcmpProbe(entries, v6 = false),
|
||||
IcmpProbe(entries, v6 = true),
|
||||
CaptivePortalProbe(entries),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
Adaptive-icon background: the brand "tile" gradient (assets/branding). -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp" android:height="108dp"
|
||||
android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:pathData="M0,0h108v108h-108z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:startX="54" android:startY="0" android:endX="54" android:endY="108"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF0E2433"/>
|
||||
<item android:offset="1" android:color="#FF071522"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
Adaptive-icon foreground: the Focus mark (assets/branding/icon-adaptive-foreground.svg),
|
||||
SVG transform baked in so the art sits inside the 66dp safe circle. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp" android:height="108dp"
|
||||
android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M32.72,34.8 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M51.92,34.8 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M71.12,34.8 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M32.72,54.0 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M71.12,54.0 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M32.72,73.2 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M51.92,73.2 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:fillColor="#FF1E4A5C" android:pathData="M71.12,73.2 a2.08,2.08 0 1,0 4.16,0 a2.08,2.08 0 1,0 -4.16,0"/>
|
||||
<path android:strokeColor="#FFB454" android:strokeAlpha="0.3" android:strokeWidth="1.6" android:fillColor="#00000000" android:pathData="M47.6,54.0 a6.4,6.4 0 1,0 12.8,0 a6.4,6.4 0 1,0 -12.8,0"/>
|
||||
<path android:fillColor="#FFFFB454" android:pathData="M50.4,54.0 a3.6,3.6 0 1,0 7.2,0 a3.6,3.6 0 1,0 -7.2,0"/>
|
||||
<path android:strokeColor="#FF35E0C4" android:strokeWidth="2.8" android:strokeLineCap="round" android:strokeLineJoin="round" android:fillColor="#00000000" android:pathData="M42.0,48.4 V42.0 H48.4"/>
|
||||
<path android:strokeColor="#FF35E0C4" android:strokeWidth="2.8" android:strokeLineCap="round" android:strokeLineJoin="round" android:fillColor="#00000000" android:pathData="M59.6,42.0 H66.0 V48.4"/>
|
||||
<path android:strokeColor="#FF35E0C4" android:strokeWidth="2.8" android:strokeLineCap="round" android:strokeLineJoin="round" android:fillColor="#00000000" android:pathData="M66.0,59.6 V66.0 H59.6"/>
|
||||
<path android:strokeColor="#FF35E0C4" android:strokeWidth="2.8" android:strokeLineCap="round" android:strokeLineJoin="round" android:fillColor="#00000000" android:pathData="M48.4,66.0 H42.0 V59.6"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Echolot</string>
|
||||
</resources>
|
||||