Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -265,24 +262,12 @@ 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);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getAction() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
/**
* Reads the cost class off the ACTIVE DEFAULT network.
*
* <p>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.
* <p>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".
*
* <p>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
Expand All @@ -40,4 +39,25 @@ public static NetworkClass classify(@NonNull Context ctx) {
boolean notMetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
return NetworkClass.from(hasInternet, notMetered);
}

/** 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;
}

/** 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);
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)));
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -149,16 +149,4 @@ 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,24 +343,28 @@
// 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: 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);
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). */
Expand Down Expand Up @@ -657,7 +661,7 @@
* on purpose (not deleted): ADFA-4842 (reactivate proot modules via module management) may want
* to generalize a "install these proot modules now" path from here — 4842 decides whether to
* reuse/generalize or delete. Deleting now could remove something module management wants. */
public void openMapsPreparing(String[] levels) {

Check warning on line 664 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Build & Distribute

[dep-ann*** deprecated item is not annotated with @deprecated

Check warning on line 664 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Lint & Compile Check

[dep-ann] deprecated item is not annotated with @deprecated
getSupportFragmentManager().beginTransaction()
.replace(R.id.k2go_setup_host, MapsPreparingFragment.newInstance(levels))
.addToBackStack("maps_preparing")
Expand Down Expand Up @@ -706,7 +710,7 @@
* the per-layer selection). InstallService writes the full maps_* local_vars and runs runrole
* with the shared success/failure verdict, revert-on-fail and observable progress. {@code levels}
* is aligned to the Choose groups [base, satellite, terrain, search]; null = off. */
public void startMapsInstall(String[] levels) {

Check warning on line 713 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Build & Distribute

[dep-ann*** deprecated item is not annotated with @deprecated

Check warning on line 713 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Lint & Compile Check

[dep-ann] deprecated item is not annotated with @deprecated
String base = levels != null && levels.length > 0 && levels[0] != null ? levels[0] : "11";
String sat = levels != null && levels.length > 1 && levels[1] != null ? levels[1] : "none";
String ter = levels != null && levels.length > 2 && levels[2] != null ? levels[2] : "0-none";
Expand All @@ -728,7 +732,7 @@
* backToGetMoreHubZim, along with the ZIM live door that was the only thing calling it.)
* ADFA-4848: "Run in background" from Preparing -> drop the whole Maps flow off the back
* stack and return to the Get More hub; the build keeps running. */
public void backToGetMoreHub() {

Check warning on line 735 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Build & Distribute

[dep-ann*** deprecated item is not annotated with @deprecated

Check warning on line 735 in controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupLibraryActivity.java

View workflow job for this annotation

GitHub Actions / Lint & Compile Check

[dep-ann] deprecated item is not annotated with @deprecated
getSupportFragmentManager().popBackStack("getmore_maps",
androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,13 @@ 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: 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);
getUpdateViewModel().track(updateDownloadId);
showUpdateProgressDialog();
});
}
}

Expand Down
25 changes: 20 additions & 5 deletions controller/docs/ADR-395-network-cost-consent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down