From 17edf34a2547835c487e214d71f78d7def4f7e07 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 16 Sep 2026 21:31:20 -0600 Subject: [PATCH 1/5] K2GO-404 feat(networkpolicy): gate the rootfs install on metered cost The wizard install downloads the rootfs image and the proot-distro base over aria2 (internet). It started with no cost prompt. Wrap SetupLibraryActivity.startWizardInstall in NetworkPolicyGate.guardHeavyStart: the gate covers the whole commit -- the InstallGuard marker, the service start and the navigation -- so a decline plants no marker and does not navigate to the boot gate (a marker with no install behind it would send the next launch into recovery). Decline/offline resets the installStarting debounce so the user can retry after moving to Wi-Fi. In-flight resume on a returning network is handled headless by InstallService.onValidatedNetworkReturned, not re-prompted; a gate at the headless aria2 start would have no Activity for the dialog, so the UI commit is the seam (mirrors startZimDownload / openMapsIndex). --- .../k2go/redesign/SetupLibraryActivity.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java index b301fbb1..ef029591 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java @@ -343,24 +343,33 @@ public void startWizardInstall() { // The trade-off, stated: if the service never starts at all, the marker is left set with no // install behind it, and the next launch enters recovery. That is a state with a dialog and a // way out (ADFA-5119) rather than a silent dead end, which is the right side to fail on. - org.appdevforall.k2go.InstallGuard.begin(this); - Intent i = new Intent(this, InstallService.class); - i.setAction(InstallService.ACTION_START); - i.putExtra(InstallService.EXTRA_TIER, getSelectedTier().name()); - i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(this)); - // ADFA-5023: reinstall wipes the existing rootfs first. Stopping a LIVE server before the wipe is - // done by the SERVICE (InstallService.runPipeline) — NOT here — so this navigation stays instant: - // one tap goes straight to the boot gate instead of the wizard sitting there during stopEnvironment. - i.putExtra(InstallService.EXTRA_REINSTALL, reinstallMode); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(i); - else startService(i); - // ADFA-5023: plain startActivity so a FRESH LibraryActivity is created and reads EXTRA_INSTALLING - // in onCreate → the boot gate. (An earlier CLEAR_TOP reused the existing Library sitting on the - // Settings tab, which doesn't re-read the extra via onNewIntent, and dumped the user back on - // Settings.) Backing out mid-install is prevented by LibraryActivity.onBackPressed, not by - // clearing the stack. - startActivity(new Intent(this, LibraryActivity.class).putExtra(LibraryActivity.EXTRA_INSTALLING, true)); - finish(); + // K2GO-404 (ADR-395): the install downloads the rootfs image and the proot-distro base over + // aria2 (internet), so ask before spending metered data. The gate wraps the WHOLE commit -- + // the InstallGuard marker, the service start and the navigation -- so a decline plants no + // marker and does not navigate to the boot gate (a marker with no install behind it would send + // the next launch into recovery). On decline/offline, reset the debounce so the user can retry + // after moving to Wi-Fi. In-flight resume on a returning network is handled headless by + // InstallService.onValidatedNetworkReturned, not re-prompted. + org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate.guardHeavyStart(this, () -> { + org.appdevforall.k2go.InstallGuard.begin(this); + Intent i = new Intent(this, InstallService.class); + i.setAction(InstallService.ACTION_START); + i.putExtra(InstallService.EXTRA_TIER, getSelectedTier().name()); + i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(this)); + // ADFA-5023: reinstall wipes the existing rootfs first. Stopping a LIVE server before the wipe is + // done by the SERVICE (InstallService.runPipeline) — NOT here — so this navigation stays instant: + // one tap goes straight to the boot gate instead of the wizard sitting there during stopEnvironment. + i.putExtra(InstallService.EXTRA_REINSTALL, reinstallMode); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(i); + else startService(i); + // ADFA-5023: plain startActivity so a FRESH LibraryActivity is created and reads EXTRA_INSTALLING + // in onCreate → the boot gate. (An earlier CLEAR_TOP reused the existing Library sitting on the + // Settings tab, which doesn't re-read the extra via onNewIntent, and dumped the user back on + // Settings.) Backing out mid-install is prevented by LibraryActivity.onBackPressed, not by + // clearing the stack. + startActivity(new Intent(this, LibraryActivity.class).putExtra(LibraryActivity.EXTRA_INSTALLING, true)); + finish(); + }, () -> installStarting = false); } /** ADFA-4853: the wizard content step — the Get More hub in pre-install mode (tier-gated). */ From ea07ebcb93faf33732c871436a3ea02192b01709 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 16 Sep 2026 21:31:20 -0600 Subject: [PATCH 2/5] K2GO-404 feat(networkpolicy): gate the OTA update download on metered cost The OTA APK is an internet download enqueued through DownloadManager with no cost check. An Activity is present in UpdateController, so prompt via NetworkPolicyGate.guardHeavyStart at the enqueue (consistent with the other seams) rather than a silent setAllowedOverMetered(false): on consent (or Wi-Fi) it enqueues, on decline it does not start and the user keeps the existing build. Portal APK/PDF downloads are NOT gated -- the WebView DownloadListener only serves local box files (internal host), never metered egress. --- .../k2go/update/presentation/UpdateController.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java index 360bdffc..999c1a7d 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java @@ -219,10 +219,16 @@ private void startDownload(String downloadUrl) { android.app.DownloadManager manager = (android.app.DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE); if (manager != null) { - downloadCompletionHandled = false; - updateDownloadId = manager.enqueue(request); - getUpdateViewModel().track(updateDownloadId); - showUpdateProgressDialog(); + // K2GO-404 (ADR-395): the OTA APK is an internet download, so ask before spending metered + // data. An Activity is present here, so we prompt (like the other seams) rather than fall + // back to setAllowedOverMetered(false); on consent (or Wi-Fi) it enqueues, on decline it + // does not start and the user keeps the existing build. + org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate.guardHeavyStart(activity, () -> { + downloadCompletionHandled = false; + updateDownloadId = manager.enqueue(request); + getUpdateViewModel().track(updateDownloadId); + showUpdateProgressDialog(); + }); } } From 3e5b305a8af9f91d19ded3be609ada78d9607ac9 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 16 Sep 2026 21:31:33 -0600 Subject: [PATCH 3/5] K2GO-404 refactor(networkpolicy): fold the two internet readers into AndroidNetworkClassifier There were three ConnectivityManager readers for "what is the network". Make AndroidNetworkClassifier the single reader: add hasInternet (classify != NONE) and hasValidatedInternet (adds NET_CAPABILITY_VALIDATED, load-bearing for the install resume's captive-portal avoidance). Remove DashboardRebuild.hasInternet and InstallService.hasValidatedInternet and route their callers here. One behavior delta: a null ConnectivityManager now reads as no internet (was "unknown -> true" in DashboardRebuild), an edge effectively never hit -- failing closed is safe. Removes the now-unused android.net imports from InstallService. --- .../install/presentation/InstallService.java | 17 +-------- .../data/AndroidNetworkClassifier.java | 38 +++++++++++++++++-- .../k2go/redesign/DashboardCardStatus.java | 4 +- .../k2go/redesign/DashboardRebuild.java | 14 +------ 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java b/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java index 4d8b98f9..11cc1b00 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java @@ -29,9 +29,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Handler; @@ -265,23 +262,13 @@ private void onValidatedNetworkReturned() { // still drives it (a user decision, not a radio event); the rootfs-download resume below is // unrelated to maps. if (!InstallProgressRepository.get().current().isSoftFailed()) return; - if (!hasValidatedInternet()) return; + if (!org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier.hasValidatedInternet(this)) return; Log.i(TAG, "ADFA-4895: a validated network returned while the download was held — resuming"); log("[Download] validated network returned — resuming the held download"); doResume(); } - /** ADFA-4895: true only when the active default network both offers internet and has been validated. */ - private boolean hasValidatedInternet() { - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm == null) return false; - Network active = cm.getActiveNetwork(); - if (active == null) return false; - NetworkCapabilities caps = cm.getNetworkCapabilities(active); - return caps != null - && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - } + // K2GO-404: hasValidatedInternet moved to AndroidNetworkClassifier.hasValidatedInternet (one reader). @Override public int onStartCommand(Intent intent, int flags, int startId) { diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java index 9d20ee7a..bcabba2c 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java @@ -12,10 +12,11 @@ /** * Reads the cost class off the ACTIVE DEFAULT network. * - *

This is the single reader of ConnectivityManager for cost decisions - * (ADR-395). The two existing internet checks -- DashboardRebuild.hasInternet and - * InstallService.hasValidatedInternet -- should route through here as a follow-up - * so there is one source of the "what is the network" fact, not three. + *

This is the single reader of ConnectivityManager (ADR-395 / K2GO-404). The + * two former internet checks -- DashboardRebuild.hasInternet and + * InstallService.hasValidatedInternet -- now route through {@link #hasInternet} + * and {@link #hasValidatedInternet} here, so there is one source of the "what is + * the network" fact, not three. * *

The rule is by NET_CAPABILITY_NOT_METERED, never by transport: on real * hardware the cellular IMS PDN reports NOT_METERED while the internet APN does @@ -40,4 +41,33 @@ public static NetworkClass classify(@NonNull Context ctx) { boolean notMetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); return NetworkClass.from(hasInternet, notMetered); } + + /** + * True when the active default network is internet-capable. The one plain "is there internet" + * reader (K2GO-404: folds {@code DashboardRebuild.hasInternet}). Unlike the former reader, an + * absent ConnectivityManager reads as no internet (via {@link #classify} returning NONE) rather + * than "unknown -> true"; that edge is effectively never hit and failing closed on it is safe. + */ + public static boolean hasInternet(@NonNull Context ctx) { + return classify(ctx) != NetworkClass.NONE; + } + + /** + * True when the active default network is internet-capable AND Android has VALIDATED real + * connectivity. Distinct from {@link #hasInternet}: a captive-portal association reports + * INTERNET but not VALIDATED, and resuming a download onto it just soft-fails. The install + * resume path requires this stricter check (K2GO-404: folds + * {@code InstallService.hasValidatedInternet}, preserving the VALIDATED requirement). + */ + public static boolean hasValidatedInternet(@NonNull Context ctx) { + ConnectivityManager cm = + (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + Network net = cm.getActiveNetwork(); + if (net == null) return false; + NetworkCapabilities caps = cm.getNetworkCapabilities(net); + return caps != null + && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } } diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardCardStatus.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardCardStatus.java index 3ca04935..9f1131c5 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardCardStatus.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardCardStatus.java @@ -33,7 +33,7 @@ public interface Listener { void onState(DashboardCardState state); } */ public static void fetch(Context context, Listener l) { final Context ctx = context.getApplicationContext(); - final boolean online = DashboardRebuild.hasInternet(ctx); + final boolean online = org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier.hasInternet(ctx); // Cached-first (ADFA-5026) so the UI isn't blank while the live check runs; the cache holds // only the boolean, so no versions -> no arrow from a cached state. @@ -55,7 +55,7 @@ public static void fetch(Context context, Listener l) { // Online but the check failed (box stopped): fall back to the cached state, or Checking // when nothing is cached. Re-read connectivity in case the network just dropped. l.onState(DashboardCardState.resolve( - DashboardRebuild.hasInternet(ctx), false, false, null, null, + org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier.hasInternet(ctx), false, false, null, null, UpdateStatusCache.has(ctx), UpdateStatusCache.updateAvailable(ctx))); } }); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java index e5ae0449..87f666ae 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java @@ -52,7 +52,7 @@ public static void confirmAndStart(@NonNull Fragment host, @NonNull View anchor, Snackbars.make(anchor, org.appdevforall.k2go.util.BusyMessage.resFor(ctx)).show(); return; } - if (!hasInternet(ctx)) { + if (!org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier.hasInternet(ctx)) { Snackbars.make(anchor, R.string.k2go_dash_needs_internet).show(); return; } @@ -150,15 +150,5 @@ public static boolean blockedByUpdate(@NonNull View anchor) { return false; } - /** True when the device reports an internet-capable active network. Unknown -> true (let the - * preflight decide), matching the previous inline check in ModuleHubFragment. */ - public static boolean hasInternet(@NonNull Context ctx) { - android.net.ConnectivityManager cm = (android.net.ConnectivityManager) - ctx.getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm == null) return true; - android.net.Network n = cm.getActiveNetwork(); - if (n == null) return false; - android.net.NetworkCapabilities caps = cm.getNetworkCapabilities(n); - return caps != null && caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET); - } + // K2GO-404: hasInternet moved to AndroidNetworkClassifier.hasInternet (one reader). } From 1226c099ce0c0bbc3abe2809be469ede11102353 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 16 Sep 2026 21:31:33 -0600 Subject: [PATCH 4/5] K2GO-404 docs(networkpolicy): record the non-REST egress seams in ADR-395 Record what K2GO-404's first PR gates (rootfs aria2 at startWizardInstall; OTA at the enqueue), why portal APK/PDF are exempt (local box files, never metered), and the hasInternet fold with its null-ConnectivityManager delta. Note the remaining second PR: the wizard/system-install maps path, which needs an orchestrator "waiting for network" state because a MapsProvisioner.drain refusal is terminal (mapsStartFailed) today. --- .../docs/ADR-395-network-cost-consent.md | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/controller/docs/ADR-395-network-cost-consent.md b/controller/docs/ADR-395-network-cost-consent.md index 1ec8a63e..e5ee480b 100644 --- a/controller/docs/ADR-395-network-cost-consent.md +++ b/controller/docs/ADR-395-network-cost-consent.md @@ -212,11 +212,26 @@ except the last two (pending a device pass). Every REST-heavy egress the user ca trigger on a live box now routes through the gate: ZIM, Books, Kolibri (Get More + native), FQR regions, dashboard live update, and Get-More base maps. -Remaining, DEFERRED to the install/rootfs PR ("el install va aparte con el rootfs"): -the wizard/system-install maps path (`mapsWizardConfirm`) and its headless hold with -an orchestrator "waiting for network" state; the rootfs-image install (aria2, not -REST). Separate follow-ups: the DownloadManager seam (OTA / portal, -`setAllowedOverMetered`) and the two-`hasInternet` fold into `AndroidNetworkClassifier`. +K2GO-404 (first PR) extends the gate to the non-REST egress: +- Rootfs install (aria2, rootfs image + proot-distro base): gated at the UI commit + `SetupLibraryActivity.startWizardInstall`, wrapping the whole commit (marker + service + + navigation) so a decline plants no InstallGuard marker. Auto-retry / resume stay + headless via `InstallService.onValidatedNetworkReturned`, not re-prompted. +- OTA APK (`UpdateController.startDownload`): prompt at the enqueue (an Activity is + present, so a prompt beats a silent `setAllowedOverMetered(false)`). +- Portal APK / PDF (`PortalActivity`): NOT gated -- the WebView `DownloadListener` only + serves LOCAL box files (internal host; external downloads are ignored), so they are + never metered internet egress. Gating them would false-alarm or block a local download. +- `hasInternet` fold: `AndroidNetworkClassifier.hasInternet` and `hasValidatedInternet` + (the latter preserves `NET_CAPABILITY_VALIDATED`) are now the single reader; + `DashboardRebuild.hasInternet` and `InstallService.hasValidatedInternet` are removed and + their callers routed. One behavior delta: a null ConnectivityManager now reads as no + internet (was "unknown -> true"), an edge effectively never hit; failing closed is safe. + +Remaining (K2GO-404 second PR): the wizard/system-install maps path (`mapsWizardConfirm`) +and its headless hold -- it needs an orchestrator "waiting for network" state because +`SetupProgressActivity` treats a `MapsProvisioner.drain` refusal as terminal +(`mapsStartFailed`), so a plain cost-hold there reads as a hard failure, not a deferral. (l10n done pending human review.) ## 6. Device evidence appendix (dark surfaces flattened) From 72c97cd805d081654a4942c536f822d6943eca28 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 16 Sep 2026 22:04:09 -0600 Subject: [PATCH 5/5] K2GO-404 style(networkpolicy): trim code comments to be proportional The seam comments over-explained. Keep the non-obvious why (the InstallGuard marker -> recovery reason, the NET_CAPABILITY_VALIDATED reason, the cm==null delta) and drop the narrative; remove the "moved to ..." markers left where the folded methods were (the fold is documented on AndroidNetworkClassifier). --- .../install/presentation/InstallService.java | 2 -- .../data/AndroidNetworkClassifier.java | 26 ++++++------------- .../k2go/redesign/DashboardRebuild.java | 2 -- .../k2go/redesign/SetupLibraryActivity.java | 9 ++----- .../update/presentation/UpdateController.java | 5 +--- 5 files changed, 11 insertions(+), 33 deletions(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java b/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java index 11cc1b00..a96f9f35 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/install/presentation/InstallService.java @@ -268,8 +268,6 @@ private void onValidatedNetworkReturned() { doResume(); } - // K2GO-404: hasValidatedInternet moved to AndroidNetworkClassifier.hasValidatedInternet (one reader). - @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : null; diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java index bcabba2c..b2582263 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java @@ -12,11 +12,9 @@ /** * Reads the cost class off the ACTIVE DEFAULT network. * - *

This is the single reader of ConnectivityManager (ADR-395 / K2GO-404). The - * two former internet checks -- DashboardRebuild.hasInternet and - * InstallService.hasValidatedInternet -- now route through {@link #hasInternet} - * and {@link #hasValidatedInternet} here, so there is one source of the "what is - * the network" fact, not three. + *

The single reader of ConnectivityManager (ADR-395 / K2GO-404): {@link #classify} for cost, + * {@link #hasInternet} / {@link #hasValidatedInternet} for the checks that used to live in + * DashboardRebuild and InstallService. One source for "what is the network". * *

The rule is by NET_CAPABILITY_NOT_METERED, never by transport: on real * hardware the cellular IMS PDN reports NOT_METERED while the internet APN does @@ -42,23 +40,15 @@ public static NetworkClass classify(@NonNull Context ctx) { return NetworkClass.from(hasInternet, notMetered); } - /** - * True when the active default network is internet-capable. The one plain "is there internet" - * reader (K2GO-404: folds {@code DashboardRebuild.hasInternet}). Unlike the former reader, an - * absent ConnectivityManager reads as no internet (via {@link #classify} returning NONE) rather - * than "unknown -> true"; that edge is effectively never hit and failing closed on it is safe. - */ + /** The single "is there internet" reader (K2GO-404: folds DashboardRebuild.hasInternet). A null + * ConnectivityManager reads as no internet (was "unknown -> true"); that edge is never hit. */ public static boolean hasInternet(@NonNull Context ctx) { return classify(ctx) != NetworkClass.NONE; } - /** - * True when the active default network is internet-capable AND Android has VALIDATED real - * connectivity. Distinct from {@link #hasInternet}: a captive-portal association reports - * INTERNET but not VALIDATED, and resuming a download onto it just soft-fails. The install - * resume path requires this stricter check (K2GO-404: folds - * {@code InstallService.hasValidatedInternet}, preserving the VALIDATED requirement). - */ + /** Like {@link #hasInternet} but also requires NET_CAPABILITY_VALIDATED: a captive-portal + * association has INTERNET but not VALIDATED, and resuming a download onto it soft-fails + * (K2GO-404: folds InstallService.hasValidatedInternet). */ public static boolean hasValidatedInternet(@NonNull Context ctx) { ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java index 87f666ae..e0c469db 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardRebuild.java @@ -149,6 +149,4 @@ public static boolean blockedByUpdate(@NonNull View anchor) { } return false; } - - // K2GO-404: hasInternet moved to AndroidNetworkClassifier.hasInternet (one reader). } diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java index ef029591..756655c2 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java @@ -343,13 +343,8 @@ public void startWizardInstall() { // The trade-off, stated: if the service never starts at all, the marker is left set with no // install behind it, and the next launch enters recovery. That is a state with a dialog and a // way out (ADFA-5119) rather than a silent dead end, which is the right side to fail on. - // K2GO-404 (ADR-395): the install downloads the rootfs image and the proot-distro base over - // aria2 (internet), so ask before spending metered data. The gate wraps the WHOLE commit -- - // the InstallGuard marker, the service start and the navigation -- so a decline plants no - // marker and does not navigate to the boot gate (a marker with no install behind it would send - // the next launch into recovery). On decline/offline, reset the debounce so the user can retry - // after moving to Wi-Fi. In-flight resume on a returning network is handled headless by - // InstallService.onValidatedNetworkReturned, not re-prompted. + // K2GO-404: gate the rootfs (aria2) download on metered cost. Wrap the whole commit so a + // decline plants no InstallGuard marker (a marker with no install behind it forces recovery). org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate.guardHeavyStart(this, () -> { org.appdevforall.k2go.InstallGuard.begin(this); Intent i = new Intent(this, InstallService.class); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java index 999c1a7d..06c1b7d8 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java @@ -219,10 +219,7 @@ private void startDownload(String downloadUrl) { android.app.DownloadManager manager = (android.app.DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE); if (manager != null) { - // K2GO-404 (ADR-395): the OTA APK is an internet download, so ask before spending metered - // data. An Activity is present here, so we prompt (like the other seams) rather than fall - // back to setAllowedOverMetered(false); on consent (or Wi-Fi) it enqueues, on decline it - // does not start and the user keeps the existing build. + // K2GO-404: gate the OTA (internet) download on metered cost; on decline it does not enqueue. org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate.guardHeavyStart(activity, () -> { downloadCompletionHandled = false; updateDownloadId = manager.enqueue(request);