app: an IMS network is not a blocked measurement

rmnet_data1 on the OnePlus is the carrier's IMS/VoLTE network: it carries
IMS&MMTEL but neither INTERNET nor NOT_RESTRICTED, so binding needs
CONNECTIVITY_USE_RESTRICTED_NETWORKS - signature-level, unobtainable.
EPERM there is permanent and says nothing about any network's health, but
the constraint detector counted it, so a phone with VoLTE reported
'measurement blocked' on every run and every verdict came out
INCONCLUSIVE. Capabilities now decide: networks an app may never bind are
recorded as app_usable:false in networks[] and skipped, rather than
reported as something the run failed to do.

Also retires the 'while it tears down' wording, which was a guess that
turned out to be wrong - the refusal outlasts the VPN indefinitely, so
the text now names the VPN as the usual cause without asserting a
timeline it cannot know.

App version 0.2.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 14:11:08 +02:00
co-authored by Claude Opus 5
parent d420a71a01
commit ab6e278272
7 changed files with 54 additions and 9 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ plugins {
//
// major*1_000_000 + minor*10_000 + patch*10 leaves room for 9 patch-level rebuilds (the trailing
// digit) without disturbing the mapping, and stays inside the 2_100_000_000 ceiling until major 2100.
val appVersionName = "0.2.3"
val appVersionName = "0.2.4"
fun versionCodeOf(semver: String): Int {
val (major, minor, patch) = semver.substringBefore('-').split(".").map(String::toInt)
@@ -432,9 +432,9 @@ private fun Results(doc: MeasurementDocument) {
"Disconnect the VPN and run again to measure the networks themselves.")
constraints.perNetworkBlocked ->
"Some networks could not be measured" to
("Android refused sends on $blocked — the restriction a VPN leaves in place " +
"while it tears down. These networks went unmeasured; wait a few " +
"seconds and run again.")
("Android refused this app permission to send on $blocked, so they went " +
"unmeasured. A connected VPN is the usual cause; the refusal can also " +
"outlast one. Everything else in this run is unaffected.")
else ->
"A VPN holds the default route" to
("Default-route results describe the tunnel; per-network measurements " +
@@ -594,10 +594,11 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
runConstraints.perNetworkBlocked ->
"The OS refused sends on $blocked" to
("Android denied this app permission to send on $blocked (EPERM on " +
"bind). That is the restriction a VPN imposes on the networks " +
"beneath it — a tunnel disconnected moments ago can still leave it " +
"in place while it tears down. Nothing in this run says anything " +
"about $blocked; wait a few seconds and run again.")
"bind), so nothing in this run says anything about it. The usual " +
"cause is a VPN — Android walls off the networks beneath a tunnel — " +
"but the refusal can also outlast one, and some networks are " +
"reserved for the system entirely. If no VPN is connected, this is " +
"a restriction on the app rather than a fault in the network.")
else ->
"A VPN holds the default route" to
("Everything using the default route in this run describes the tunnel, " +