Network churn (SSID jump/roam) makes adbd re-publish its advertisement repeatedly; each resolve re-arms the connection and posts a notification, so the resolve-once guard can't fully prevent spam on the OnePlus. Noted the alternatives (manual port, or Shizuku `ss` with no mDNS involved). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.7 KiB
Echolot — context for Claude Code
This repo is the capability prober for the Echolot project: an F/OSS Android app for detecting and debugging local network issues (target audience: network engineers). The prober validates the no-root feasibility matrix on real hardware before the production app is built.
Project facts
- Name: Echolot. Domain echo-lot.app. URI scheme
echolot://. Namespaceapp.echo_lot.*(hyphen→underscore; appIds/packages can't contain hyphens). Prober appId:app.echo_lot.prober. - Stack: native Kotlin + Jetpack Compose. No Flutter (all value is in platform APIs; Flutter would be a Dart skin over a full Kotlin app + F-Droid build friction).
- Tiers:
app(no root),shizuku(ADB-shell privileges via wireless pairing, shipped in v1),root(future module). Every result records which tier produced it. - License: code is GPL-3.0-or-later (
LICENSE), the specs indocs/are CC-BY-4.0 (docs/LICENSE). New source files get the two-line SPDX header the existing ones carry. The server, when it lands, is GPL too — not AGPL (seedocs/build-status.mdfor the reasoning).
Design docs (source of truth, in docs/)
docs/feature-catalog-and-feasibility.md— full feature list + no-root feasibility matrix.docs/measurement-schema.md— the archived/exportable measurement JSON format (observation vs finding separation, two-clock rule, columnar trains, anonymization types).docs/probe-protocol.md— client↔server wire protocol (pinned-TLS control plane, binary UDP data plane with anti-amplification HMAC, STUN, canary DNS reference records).docs/build-status.md— running log of decisions, what is delivered, and the next steps.
The three specs are draft-complete and user-reviewed; treat them as the contract. build-status.md
is the mutable one — update it as work lands.
Keep prober result IDs aligned with the measurement-schema test-type registry.
Versioning
Prefer patch bumps (server-v0.3.1) for additive/incremental work; reserve minor bumps
for real milestones. Don't burn through minor versions. Tags are namespaced: server-v* for the
Go server, v* for the app. Pushing a server-v* tag runs CI → binaries + Gitea release +
registry image; the server on fmr can --self-update from those releases.
Layout
Monorepo. The prober is one deliverable; the Go server and the production app land as siblings.
docs/ design docs (above) — apply repo-wide, not just to the prober
echolot-prober/ the capability prober (self-contained Gradle build)
app/src/main/java/app/echo_lot/prober/
MainActivity.kt Compose host, runs probes sequentially
probe/ Probe interface + all app-tier probes + OS ABI helper
shizuku/ AIDL UserService + ShizukuRunner (shell command exec)
export/Report.kt JSON report + share intent
ui/ProberScreen.kt result cards colored by verdict
Conventions
- Every probe returns a
ProbeResult— never throws to the caller (MainActivity wraps anyway). - Uncertain OS API paths (getsockoptInt, recvmsg, nat64Prefix) are reached via reflection/runCatching and reported, not assumed — the prober's purpose is to discover what exists.
- Hardcoded Linux sockopt ABI numbers live in
OsAbi.ktwith rationale; do not "fix" them to OsConstants names that don't exist.
Build
From echolot-prober/: ./gradlew :app:assembleDebug (needs local Android SDK; set sdk.dir in
echolot-prober/local.properties). The Gradle build is rooted there, not at the repo root.
First build downloads AGP/Compose/Shizuku from Google Maven + Maven Central.
Working with test devices (hard-won lessons)
- Prefer USB for adb. Wireless debugging dies constantly: probe runs churn the wifi the debug link rides on, the Lenovo tablet's ZUI power management kills the listener anyway, and the port rotates on every restart. Never drive a probe run over wireless adb.
- Collection loop over USB, fully driveable by Claude: install →
am start→ tap "Run all probes" → polluiautomator dumpuntil the button label returns to "Run all probes" → tap "Export JSON" → the report lands incache/reports/and comes back viaadb shell run-as app.echo_lot.prober cat …(no need to drive the share sheet; dismiss it with BACK). Archive underecholot-prober/reports/, record findings indocs/build-status.md. - Bash-tool
adb shellcalls with absolute device paths get mangled by Git Bash path conversion (/data/…→C:/Program Files/Git/data/…); use the PowerShell tool for those. - Bump
versionCodeon every deployed prober change — it shows on screen and asproberBuildin the report; that's how a report is matched to a build. - Known devices: OnePlus 15 (CPH2747, A16) — Shizuku UserService works;
Lenovo TB330FU (A15, multi-user) — UserService never binds, the
newProcessfallback carries it. Full history in build-status.md. - Wireless-adb beacon (
echolot-app/adb-beacon+tools/adb-beacon/receiver.py): the wireless-debug port rotates every couple of minutes on these devices; the beacon reads the live port from adbd's own mDNS (_adb-tls-connect._tcp, filtered to the device's own IP) and POSTs it to fmr:443 (cleartext allowed; over a validated net); a PC connector polls and reconnects. Bootstrap needs one manual adb connection to install it, then it self-heals. Existing adb connections survive port rotation — only fresh connects need the new port. Resolve adbd's mDNS advertisement exactly ONCE per service instance (re-resolving makes adbd re-arm the connection, which spams "wireless debugging connected" notifications); the periodic heartbeat only re-POSTs the cached port. Rotation is still caught: onServiceLost clears the guard, so the new advertisement is resolved once and reported within seconds (verified: 37089 -> 33667 reported 6 s after rotation). Known limitation — do not run the beacon on the OnePlus. On network churn (SSID jump, roam) adbd repeatedly drops and re-publishes its advertisement, so lost/found cycles keep clearing the resolve-guard; each resolve makes adbd re-arm and post a "wireless debugging connected" notification. Guarding reduces but cannot eliminate this — resolving adbd's own mDNS record is inherently noisy on that device. Use manualip:portthere (ask the user), or read the port via Shizuku (ss -tlnp | grep adbd, no mDNS) if the shell tier is available. - Shizuku start kills the adb bridge and the beacon can't auto-recover it. Shizuku's non-root start pairs over wireless debugging and runs its starter through adb, hijacking the channel: adb goes device→offline→refused, and adbd keeps advertising the now-dead port over mDNS (stale), so the beacon reports a port that no longer accepts connections. Workaround: after starting Shizuku, toggle Wireless debugging off/on — Shizuku keeps running (separate process), a fresh port + mDNS record appear, and the beacon/connector recover. Plan the Shizuku-tier dev loop around this (or USB, if ever available).
- As of the AGP 9.2.0 / Gradle 9.6.0 / Kotlin 2.2.10 bump, JDK 17+ (including 25) works —
AGP 9 requires Gradle 9.1.0+ and Kotlin 2.2.10+ as its minimum KGP version. On machines with
Android Studio, its
jbrdirectory works asJAVA_HOME.
Likely next steps
- Run on physical devices; collect JSON reports across Android versions/vendors.
C-over-JNI errqueue shim— not needed;trace.errqueue_reachableandtraceroute.udp4are SUPPORTED on both known devices viaOs.recvmsg+StructMsghdrreflection.- Fold the confirmed capabilities + Shizuku dump-format samples back into the production
core-probe/core-shizukumodules.