app: progress bar with ETA, cancel button, and cutout-safe layout

- Probe.estimatedMs (measured per probe; timeout-bound ones dominate)
  drives a determinate progress bar and "test N of M · ~Xs left",
  including the Shizuku battery in the total.
- Cancel stops the run and shows the partial results as a normal document
  (findings + verdict over what was collected) but never uploads them.
- safeDrawingPadding() on the root column: Android 15 is edge-to-edge by
  default and the title was colliding with the status-bar clock and the
  camera cutout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 09:48:18 +02:00
co-authored by Claude Opus 5
parent 2e34463c0a
commit 217818f7b3
10 changed files with 127 additions and 24 deletions
@@ -36,6 +36,7 @@ import java.net.URL
class CaptivePortalProbe(private val entries: List<NetworkInventory.Entry>) : Probe {
override val type = TestType.NET_CAPTIVE_PORTAL
override val tier = Tier.APP
override val estimatedMs = 9_000L // two HTTP probes per network, 4s timeouts each
private val httpsUrl = "https://www.google.com/generate_204"
private val httpUrl = "http://connectivitycheck.gstatic.com/generate_204"
@@ -37,6 +37,7 @@ class DnsCanaryProbe(
) : Probe {
override val type = TestType.DNS_CANARY
override val tier = Tier.APP
override val estimatedMs = 3_000L // five resolutions through the platform resolver
/** Frozen ground truth from probe-protocol.md §6.1 — must match the server's dns_reference.go. */
private val references = listOf(
@@ -35,6 +35,8 @@ class IcmpProbe(
) : Probe {
override val type = if (v6) TestType.ICMP_PING6 else TestType.ICMP_PING4
override val tier = Tier.APP
// v6 on a v4-only network waits out a 3s timeout per network; v4 answers in ms.
override val estimatedMs = if (v6) 7_000L else 800L
override suspend fun run(ctx: Context, ids: ProbeIds): Test = withContext(Dispatchers.IO) {
val b = TestBuilder(type, tier, ids)
@@ -23,6 +23,7 @@ import kotlinx.serialization.json.addJsonObject
class LinkSnapshotProbe(private val entries: List<NetworkInventory.Entry>) : Probe {
override val type = TestType.LINK_SNAPSHOT
override val tier = Tier.APP
override val estimatedMs = 200L // reads LinkProperties, no I/O
override suspend fun run(ctx: Context, ids: ProbeIds): Test {
val b = TestBuilder(type, tier, ids)
@@ -19,6 +19,14 @@ interface Probe {
val type: String
val tier: Tier
/**
* Rough wall-clock estimate in ms, used only to drive the progress bar / ETA. Probes that
* wait on timeouts (ICMPv6 on a v4-only net, SSDP, STUN) dominate a run, so they override
* this with realistic values measured on device — a bad estimate misleads the user, it does
* not break the run.
*/
val estimatedMs: Long get() = 2_000
/** Runs the probe. [ctx] gives platform access; [ids] supplies UUIDs + the monotonic clock so
* results are attributable and use the two-clock rule. Must never throw. */
suspend fun run(ctx: Context, ids: ProbeIds): Test
@@ -47,6 +47,7 @@ import java.net.URL
class RouterIdentityProbe(private val entries: List<NetworkInventory.Entry>) : Probe {
override val type = TestType.LINK_RA_SOURCE
override val tier = Tier.APP
override val estimatedMs = 7_000L // SSDP M-SEARCH window + description fetches + rDNS
override suspend fun run(ctx: Context, ids: ProbeIds): Test = withContext(Dispatchers.IO) {
val b = TestBuilder(type, tier, ids)
@@ -43,6 +43,7 @@ class StunProbe(
) : Probe {
override val type = TestType.NAT_STUN_5780
override val tier = Tier.APP
override val estimatedMs = 6_000L // three binding requests; the change-request usually times out (3s)
private companion object {
const val MAGIC_COOKIE = 0x2112A442.toInt()