diff --git a/controller/app/src/main/java/org/appdevforall/k2go/IIABApplication.java b/controller/app/src/main/java/org/appdevforall/k2go/IIABApplication.java
index 3dc3786eb..e067e3dbf 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/IIABApplication.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/IIABApplication.java
@@ -34,6 +34,10 @@ public void onCreate() {
// with NO foreground Activity. The tick stands down while an Activity is foregrounded (the Activity
// poll + bridge drive then); it only actuates OFF-UI when backgrounded.
org.appdevforall.k2go.env.ServerLifecycleReconciler.get().startBackgroundTick(this);
+ // K2GO-395 (ADR-395): one process-scoped watcher of the default-network cost class.
+ // Proactive alert on crossing into metered + clears the session metered-consent on leaving
+ // metered. Reuses the existing NetworkStateLiveData callback (one source of the change fact).
+ org.appdevforall.k2go.networkpolicy.presentation.MeteredNetworkObserver.start(this);
// We inject Conscrypt as the app's primary security provider
try {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
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
new file mode 100644
index 000000000..9d20ee7a6
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java
@@ -0,0 +1,43 @@
+package org.appdevforall.k2go.networkpolicy.data;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+
+import androidx.annotation.NonNull;
+
+import org.appdevforall.k2go.networkpolicy.domain.NetworkClass;
+
+/**
+ * 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.
+ *
+ *
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
+ * not (see ADR-395 device evidence). The pure mapping lives in
+ * {@link NetworkClass#from(boolean, boolean)}; this class only extracts the two
+ * facts from Android.
+ */
+public final class AndroidNetworkClassifier {
+
+ private AndroidNetworkClassifier() {}
+
+ @NonNull
+ public static NetworkClass classify(@NonNull Context ctx) {
+ ConnectivityManager cm =
+ (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) return NetworkClass.NONE;
+ Network net = cm.getActiveNetwork();
+ if (net == null) return NetworkClass.NONE;
+ NetworkCapabilities caps = cm.getNetworkCapabilities(net);
+ if (caps == null) return NetworkClass.NONE;
+ boolean hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ boolean notMetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ return NetworkClass.from(hasInternet, notMetered);
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/SessionMeteredConsentStore.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/SessionMeteredConsentStore.java
new file mode 100644
index 000000000..a2efb4f99
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/SessionMeteredConsentStore.java
@@ -0,0 +1,37 @@
+package org.appdevforall.k2go.networkpolicy.data;
+
+import org.appdevforall.k2go.networkpolicy.domain.MeteredConsentStore;
+
+/**
+ * In-memory, process-lifetime consent (ADR-395). Not persisted on purpose: the
+ * grant must not outlive the session, so cost awareness returns on the next
+ * launch. The metered-network observer clears it the moment the network returns
+ * to non-metered, so the grant never outlives the metered episode either.
+ */
+public final class SessionMeteredConsentStore implements MeteredConsentStore {
+
+ private static final SessionMeteredConsentStore INSTANCE = new SessionMeteredConsentStore();
+
+ public static SessionMeteredConsentStore get() {
+ return INSTANCE;
+ }
+
+ private SessionMeteredConsentStore() {}
+
+ private volatile boolean granted = false;
+
+ @Override
+ public boolean isGranted() {
+ return granted;
+ }
+
+ @Override
+ public void grant() {
+ granted = true;
+ }
+
+ @Override
+ public void clear() {
+ granted = false;
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/MeteredConsentStore.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/MeteredConsentStore.java
new file mode 100644
index 000000000..b500f4613
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/MeteredConsentStore.java
@@ -0,0 +1,21 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+/**
+ * Holds the one ephemeral fact the gate needs: did the user consent to spend
+ * metered data for this session?
+ *
+ *
Ephemeral by design (ADR-395): a persisted "always allow" would defeat the
+ * cost-awareness goal, and a persisted grant that nobody clears is the
+ * stuck-marker anti-pattern this project avoids. Lifecycle: the consent dialog
+ * calls {@link #grant()}; the metered-network observer calls {@link #clear()}
+ * when the default network returns to unmetered; process death clears it because
+ * the only implementation keeps it in memory.
+ */
+public interface MeteredConsentStore {
+
+ boolean isGranted();
+
+ void grant();
+
+ void clear();
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClass.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClass.java
new file mode 100644
index 000000000..0b8b2668a
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClass.java
@@ -0,0 +1,35 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+/**
+ * Cost class of the ACTIVE DEFAULT network, from the app point of view.
+ *
+ *
The split is by metered state, not by transport. On real hardware a carrier
+ * runs several cellular data networks at once: the IMS signaling network reports
+ * NOT_METERED, while the general-internet APN does not. Keying on
+ * TRANSPORT_CELLULAR would therefore misjudge cost. The one reliable signal is
+ * the active default network NET_CAPABILITY_NOT_METERED. See
+ * ADR-395 (device evidence appendix) for the measured values.
+ */
+public enum NetworkClass {
+
+ /** Has internet and is not metered (home Wi-Fi, unmetered ethernet). Free to use. */
+ UNMETERED,
+
+ /** Has internet but is metered (cellular internet APN, a metered Wi-Fi hotspot). Costs data. */
+ METERED,
+
+ /** No internet-capable default network. Nothing can be downloaded. */
+ NONE;
+
+ /**
+ * Pure mapping from the two facts the data layer reads off the active default
+ * network. Kept here so the rule is unit-tested without Android.
+ *
+ * @param hasInternet the default network has NET_CAPABILITY_INTERNET
+ * @param notMetered the default network has NET_CAPABILITY_NOT_METERED
+ */
+ public static NetworkClass from(boolean hasInternet, boolean notMetered) {
+ if (!hasInternet) return NONE;
+ return notMetered ? UNMETERED : METERED;
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicy.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicy.java
new file mode 100644
index 000000000..35bfa1c6f
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicy.java
@@ -0,0 +1,29 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+/**
+ * The single rule for "may a heavy transfer start now?". Pure, no Android.
+ *
+ *
Defensive by design (see ADR-395): the gate acts at the START of a new
+ * transfer. It does not micro-manage a transfer already in flight -- once bytes
+ * move on a link the app does not own (the in-proot server pulls content over
+ * the device default network), Android gives no fine control. So the contract
+ * is simple: do not START anything costly without consent.
+ */
+public final class NetworkPolicy {
+
+ /**
+ * @param net cost class of the active default network
+ * @param consented the user granted "spend metered data" for this session
+ */
+ public NetworkPolicyDecision decideHeavyStart(NetworkClass net, boolean consented) {
+ switch (net) {
+ case UNMETERED:
+ return NetworkPolicyDecision.ALLOW;
+ case METERED:
+ return consented ? NetworkPolicyDecision.ALLOW : NetworkPolicyDecision.NEEDS_CONSENT;
+ case NONE:
+ default:
+ return NetworkPolicyDecision.BLOCKED_NO_NETWORK;
+ }
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyDecision.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyDecision.java
new file mode 100644
index 000000000..a42b8c0e3
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyDecision.java
@@ -0,0 +1,14 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+/** What a caller must do before starting a heavy (costly) transfer. */
+public enum NetworkPolicyDecision {
+
+ /** Proceed now. The network is free, or the user already consented to spend data. */
+ ALLOW,
+
+ /** Ask the user to consent to spending metered data; proceed only on a yes. */
+ NEEDS_CONSENT,
+
+ /** No usable network. Do not start; tell the user they are offline. */
+ BLOCKED_NO_NETWORK
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransition.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransition.java
new file mode 100644
index 000000000..19b094989
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransition.java
@@ -0,0 +1,17 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+/** Pure rule for the proactive alert: warn when the default network becomes metered. */
+public final class NetworkTransition {
+
+ private NetworkTransition() {}
+
+ /**
+ * True when the default network just crossed INTO a metered state from a
+ * non-metered one -- the moment to warn the user that further activity spends
+ * data. A metered-to-metered change, or any change back to unmetered, never
+ * warns.
+ */
+ public static boolean shouldWarn(NetworkClass previous, NetworkClass next) {
+ return next == NetworkClass.METERED && previous != NetworkClass.METERED;
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/MeteredNetworkObserver.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/MeteredNetworkObserver.java
new file mode 100644
index 000000000..50d1f31c5
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/MeteredNetworkObserver.java
@@ -0,0 +1,96 @@
+package org.appdevforall.k2go.networkpolicy.presentation;
+
+import android.app.Application;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import org.appdevforall.k2go.R;
+import org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier;
+import org.appdevforall.k2go.networkpolicy.data.SessionMeteredConsentStore;
+import org.appdevforall.k2go.networkpolicy.domain.NetworkClass;
+import org.appdevforall.k2go.networkpolicy.domain.NetworkTransition;
+import org.appdevforall.k2go.sync.transport.NetworkStateLiveData;
+
+/**
+ * Process-wide watcher of the default-network cost class. Started once from
+ * IIABApplication, mirroring {@code ServerLifecycleReconciler} (one process-scoped
+ * owner). It REUSES the single existing default-network callback
+ * ({@link NetworkStateLiveData}) instead of registering a second one -- one
+ * source for the "network changed" fact (ADR-395).
+ *
+ *
Two jobs:
+ *
+ * - Proactive alert: when the network crosses into metered, post a
+ * notification so the user knows further activity spends data -- even with
+ * no download pending.
+ * - Consent lifecycle: clear the session metered-consent the moment the
+ * network leaves metered, so the next metered episode asks again (no stuck
+ * grant -- ADR-395).
+ *
+ */
+public final class MeteredNetworkObserver {
+
+ private static final String CHANNEL_ID = "network_cost";
+ private static final int NOTIF_ID = 0x4E50; // stable id: re-alert replaces, never stacks
+
+ private MeteredNetworkObserver() {}
+
+ private static NetworkClass last = null;
+
+ /** Idempotent; call once from Application.onCreate on the main thread. */
+ public static void start(@NonNull Application app) {
+ ensureChannel(app);
+ last = AndroidNetworkClassifier.classify(app);
+ // observeForever keeps NetworkStateLiveData active for the process lifetime,
+ // which is exactly the scope we want; no separate registration.
+ NetworkStateLiveData.get(app).observeForever(token -> onNetworkChanged(app));
+ }
+
+ private static void onNetworkChanged(@NonNull Application app) {
+ NetworkClass previous = last;
+ NetworkClass next = AndroidNetworkClassifier.classify(app);
+ if (next == previous) return;
+ last = next;
+ if (next != NetworkClass.METERED) {
+ SessionMeteredConsentStore.get().clear();
+ }
+ if (NetworkTransition.shouldWarn(previous, next)) {
+ notifyMetered(app);
+ }
+ }
+
+ private static void notifyMetered(@NonNull Context ctx) {
+ NotificationCompat.Builder b = new NotificationCompat.Builder(ctx, CHANNEL_ID)
+ .setSmallIcon(android.R.drawable.stat_sys_warning)
+ .setContentTitle(ctx.getString(R.string.k2go_netpolicy_switched_title))
+ .setContentText(ctx.getString(R.string.k2go_netpolicy_switched_msg))
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(ctx.getString(R.string.k2go_netpolicy_switched_msg)))
+ .setCategory(NotificationCompat.CATEGORY_STATUS)
+ .setAutoCancel(true)
+ .setOnlyAlertOnce(true)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+ try {
+ NotificationManagerCompat.from(ctx).notify(NOTIF_ID, b.build());
+ } catch (SecurityException ignored) {
+ // POST_NOTIFICATIONS not granted (Android 13+): the start-gate still protects cost.
+ }
+ }
+
+ private static void ensureChannel(@NonNull Context ctx) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel ch = new NotificationChannel(
+ CHANNEL_ID,
+ ctx.getString(R.string.k2go_netpolicy_channel),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationManager m = ctx.getSystemService(NotificationManager.class);
+ if (m != null) m.createNotificationChannel(ch);
+ }
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/NetworkPolicyGate.java b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/NetworkPolicyGate.java
new file mode 100644
index 000000000..76f6eda7b
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/NetworkPolicyGate.java
@@ -0,0 +1,68 @@
+package org.appdevforall.k2go.networkpolicy.presentation;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.appdevforall.k2go.R;
+import org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier;
+import org.appdevforall.k2go.networkpolicy.data.SessionMeteredConsentStore;
+import org.appdevforall.k2go.networkpolicy.domain.MeteredConsentStore;
+import org.appdevforall.k2go.networkpolicy.domain.NetworkClass;
+import org.appdevforall.k2go.networkpolicy.domain.NetworkPolicy;
+import org.appdevforall.k2go.networkpolicy.domain.NetworkPolicyDecision;
+import org.appdevforall.k2go.ui.dialog.BrandDialog;
+import org.appdevforall.k2go.util.Snackbars;
+
+/**
+ * The single consult point before any heavy download starts (ADR-395). A caller
+ * wraps its existing start call:
+ *
+ * NetworkPolicyGate.guardHeavyStart(activity, () -> a.startZimDownload());
+ *
+ * Stateless, like {@code OpReturnNavigator}: it owns no "is metered" flag. It
+ * reads the live class off {@link AndroidNetworkClassifier} and the session
+ * consent off {@link SessionMeteredConsentStore}, applies the pure
+ * {@link NetworkPolicy}, and either proceeds, asks, or blocks.
+ *
+ *
It gates the START only. It does NOT control a transfer already in flight --
+ * the content bytes are pulled by the in-proot server over the device default
+ * network, which Android gives the app no handle to throttle (ADR-395). Callers
+ * put this at the user's commit point (the Download button), never on the
+ * background drain that re-hands an already-authorized wishlist.
+ */
+public final class NetworkPolicyGate {
+
+ private NetworkPolicyGate() {}
+
+ private static final NetworkPolicy POLICY = new NetworkPolicy();
+
+ public static void guardHeavyStart(@NonNull Activity activity, @NonNull Runnable onProceed) {
+ MeteredConsentStore consent = SessionMeteredConsentStore.get();
+ NetworkClass net = AndroidNetworkClassifier.classify(activity);
+ NetworkPolicyDecision decision = POLICY.decideHeavyStart(net, consent.isGranted());
+ switch (decision) {
+ case ALLOW:
+ onProceed.run();
+ return;
+ case NEEDS_CONSENT:
+ new BrandDialog(activity)
+ .setTitle(R.string.k2go_netpolicy_metered_title)
+ .setMessage(R.string.k2go_netpolicy_metered_msg)
+ .setPositive(R.string.k2go_netpolicy_continue, () -> {
+ consent.grant();
+ onProceed.run();
+ })
+ .setNegative(R.string.k2go_netpolicy_not_now, null)
+ .show();
+ return;
+ case BLOCKED_NO_NETWORK:
+ default:
+ View root = activity.findViewById(android.R.id.content);
+ if (root != null) {
+ Snackbars.make(root, R.string.k2go_netpolicy_offline).show();
+ }
+ }
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimConfirmFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimConfirmFragment.java
index b26c80eb8..1d8f6c50e 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimConfirmFragment.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimConfirmFragment.java
@@ -106,7 +106,14 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
if (!(getActivity() instanceof SetupLibraryActivity)) return;
SetupLibraryActivity a = (SetupLibraryActivity) getActivity();
if (banks) a.zimWizardConfirm(); // no box yet: bank it
- else if (!DashboardRebuild.blockedByUpdate(v)) a.startZimDownload(); // ADFA-5074 / ADFA-5333
+ // ADFA-5074 / ADFA-5333: blocked while a dashboard update runs.
+ // K2GO-395 (ADR-395): gate a metered start behind explicit consent (cost awareness).
+ // The gate sits at the user commit point, not on ZimProvisioner.drain (that re-hands an
+ // already-authorized wishlist every ~2 s and would re-prompt).
+ else if (!DashboardRebuild.blockedByUpdate(v)) {
+ org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate
+ .guardHeavyStart(a, a::startZimDownload);
+ }
});
return root;
diff --git a/controller/app/src/main/res/values-ar/strings_networkpolicy.xml b/controller/app/src/main/res/values-ar/strings_networkpolicy.xml
new file mode 100644
index 000000000..70291527c
--- /dev/null
+++ b/controller/app/src/main/res/values-ar/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ أنت تستخدم بيانات الجوال
+ يستخدم هذا التنزيل بيانات الجوال. قد يترتب عليه تكلفة أو يستنفد باقتك الشهرية. هل تريد المتابعة؟
+ المتابعة عبر البيانات
+ ليس الآن
+ لا توجد شبكة. اتصل بشبكة Wi-Fi للتنزيل.
+ لقد انتقلت إلى بيانات الجوال
+ تستخدم التنزيلات الجديدة الآن بيانات الجوال. قد يترتب على ذلك تكلفة أو يستنفد باقتك الشهرية.
+ تنبيهات تكلفة البيانات
+
diff --git a/controller/app/src/main/res/values-az/strings_networkpolicy.xml b/controller/app/src/main/res/values-az/strings_networkpolicy.xml
new file mode 100644
index 000000000..a5398e08b
--- /dev/null
+++ b/controller/app/src/main/res/values-az/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Mobil internetdən istifadə edirsiniz
+ Bu yükləmə mobil internetdən istifadə edir. Bu, xərc yarada və ya aylıq paketinizi bitirə bilər. Davam etmək istəyirsiniz?
+ Mobil internetlə davam et
+ İndi yox
+ Şəbəkə yoxdur. Yükləmək üçün Wi-Fi-a qoşulun.
+ Mobil internetə keçdiniz
+ Yeni yükləmələr indi mobil internetdən istifadə edir. Bu, xərc yarada və ya aylıq paketinizi bitirə bilər.
+ Data xərci bildirişləri
+
diff --git a/controller/app/src/main/res/values-bg/strings_networkpolicy.xml b/controller/app/src/main/res/values-bg/strings_networkpolicy.xml
new file mode 100644
index 000000000..9cc33da70
--- /dev/null
+++ b/controller/app/src/main/res/values-bg/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Използвате мобилни данни
+ Това изтегляне използва мобилни данни. Може да има такса или да изчерпи месечния ви план. Искате ли да продължите?
+ Продължи с мобилни данни
+ Не сега
+ Няма мрежа. Свържете се с Wi-Fi, за да изтеглите.
+ Превключихте към мобилни данни
+ Новите изтегляния вече използват мобилни данни. Това може да има такса или да изчерпи месечния ви план.
+ Известия за разход на данни
+
diff --git a/controller/app/src/main/res/values-bn/strings_networkpolicy.xml b/controller/app/src/main/res/values-bn/strings_networkpolicy.xml
new file mode 100644
index 000000000..9b7545009
--- /dev/null
+++ b/controller/app/src/main/res/values-bn/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ আপনি মোবাইল ডেটা ব্যবহার করছেন
+ এই ডাউনলোডটি মোবাইল ডেটা ব্যবহার করে। এতে খরচ হতে পারে বা আপনার মাসিক ডেটা শেষ হতে পারে। আপনি কি চালিয়ে যেতে চান?
+ ডেটা দিয়ে চালিয়ে যান
+ এখন নয়
+ কোনো নেটওয়ার্ক নেই। ডাউনলোড করতে Wi-Fi এ সংযোগ করুন।
+ আপনি মোবাইল ডেটাতে স্যুইচ করেছেন
+ নতুন ডাউনলোডগুলি এখন মোবাইল ডেটা ব্যবহার করে। এতে খরচ হতে পারে বা আপনার মাসিক ডেটা শেষ হতে পারে।
+ ডেটা খরচের সতর্কতা
+
diff --git a/controller/app/src/main/res/values-cs/strings_networkpolicy.xml b/controller/app/src/main/res/values-cs/strings_networkpolicy.xml
new file mode 100644
index 000000000..dc3ff4053
--- /dev/null
+++ b/controller/app/src/main/res/values-cs/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Používáte mobilní data
+ Toto stahování používá mobilní data. Může být zpoplatněno nebo vyčerpat váš měsíční limit. Chcete pokračovat?
+ Pokračovat na mobilních datech
+ Teď ne
+ Žádná síť. Připojte se k Wi-Fi pro stažení.
+ Přepnuli jste na mobilní data
+ Nová stahování nyní používají mobilní data. Může být zpoplatněno nebo vyčerpat váš měsíční limit.
+ Upozornění na náklady za data
+
diff --git a/controller/app/src/main/res/values-de/strings_networkpolicy.xml b/controller/app/src/main/res/values-de/strings_networkpolicy.xml
new file mode 100644
index 000000000..a842c46bb
--- /dev/null
+++ b/controller/app/src/main/res/values-de/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Sie nutzen mobile Daten
+ Dieser Download nutzt mobile Daten. Das kann Kosten verursachen oder Ihr monatliches Datenvolumen aufbrauchen. Möchten Sie fortfahren?
+ Mit mobilen Daten fortfahren
+ Nicht jetzt
+ Kein Netzwerk. Verbinden Sie sich mit WLAN, um herunterzuladen.
+ Sie haben zu mobilen Daten gewechselt
+ Neue Downloads nutzen jetzt mobile Daten. Das kann Kosten verursachen oder Ihr monatliches Datenvolumen aufbrauchen.
+ Warnungen zu Datenkosten
+
diff --git a/controller/app/src/main/res/values-el/strings_networkpolicy.xml b/controller/app/src/main/res/values-el/strings_networkpolicy.xml
new file mode 100644
index 000000000..8827ddb8c
--- /dev/null
+++ b/controller/app/src/main/res/values-el/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Χρησιμοποιείτε δεδομένα κινητής
+ Αυτή η λήψη χρησιμοποιεί δεδομένα κινητής. Μπορεί να έχει κόστος ή να εξαντλήσει το μηνιαίο σας πακέτο. Θέλετε να συνεχίσετε;
+ Συνέχεια με δεδομένα
+ Όχι τώρα
+ Χωρίς δίκτυο. Συνδεθείτε σε Wi-Fi για λήψη.
+ Μεταβήκατε σε δεδομένα κινητής
+ Οι νέες λήψεις χρησιμοποιούν τώρα δεδομένα κινητής. Αυτό μπορεί να έχει κόστος ή να εξαντλήσει το μηνιαίο σας πακέτο.
+ Ειδοποιήσεις κόστους δεδομένων
+
diff --git a/controller/app/src/main/res/values-es/strings_networkpolicy.xml b/controller/app/src/main/res/values-es/strings_networkpolicy.xml
new file mode 100644
index 000000000..7b5d790c0
--- /dev/null
+++ b/controller/app/src/main/res/values-es/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Estás usando datos móviles
+ Esta descarga usa datos móviles. Puede tener costo o agotar tu plan mensual. ¿Deseas continuar?
+ Continuar con datos
+ Ahora no
+ Sin red. Conéctate a Wi-Fi para descargar.
+ Cambiaste a datos móviles
+ Las nuevas descargas ahora usan datos móviles. Esto puede tener costo o agotar tu plan mensual.
+ Alertas de costo de datos
+
diff --git a/controller/app/src/main/res/values-fa/strings_networkpolicy.xml b/controller/app/src/main/res/values-fa/strings_networkpolicy.xml
new file mode 100644
index 000000000..23f892f95
--- /dev/null
+++ b/controller/app/src/main/res/values-fa/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ شما در حال استفاده از داده تلفن همراه هستید
+ این دانلود از داده تلفن همراه استفاده میکند. ممکن است هزینه داشته باشد یا بسته ماهانه شما را تمام کند. آیا میخواهید ادامه دهید؟
+ ادامه با داده تلفن همراه
+ الان نه
+ شبکهای وجود ندارد. برای دانلود به Wi-Fi متصل شوید.
+ به داده تلفن همراه تغییر دادید
+ دانلودهای جدید اکنون از داده تلفن همراه استفاده میکنند. این ممکن است هزینه داشته باشد یا بسته ماهانه شما را تمام کند.
+ هشدارهای هزینه داده
+
diff --git a/controller/app/src/main/res/values-fr/strings_networkpolicy.xml b/controller/app/src/main/res/values-fr/strings_networkpolicy.xml
new file mode 100644
index 000000000..2a816ade2
--- /dev/null
+++ b/controller/app/src/main/res/values-fr/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Vous utilisez les données mobiles
+ Ce téléchargement utilise les données mobiles. Il peut coûter cher ou épuiser votre forfait mensuel. Voulez-vous continuer ?
+ Continuer avec les données
+ Pas maintenant
+ Aucun réseau. Connectez-vous au Wi-Fi pour télécharger.
+ Vous êtes passé aux données mobiles
+ Les nouveaux téléchargements utilisent désormais les données mobiles. Cela peut coûter cher ou épuiser votre forfait mensuel.
+ Alertes de coût des données
+
diff --git a/controller/app/src/main/res/values-gu/strings_networkpolicy.xml b/controller/app/src/main/res/values-gu/strings_networkpolicy.xml
new file mode 100644
index 000000000..2b692c002
--- /dev/null
+++ b/controller/app/src/main/res/values-gu/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ તમે મોબાઇલ ડેટા વાપરી રહ્યા છો
+ આ ડાઉનલોડ મોબાઇલ ડેટા વાપરે છે. તેનાથી ખર્ચ થઈ શકે અથવા તમારો માસિક ડેટા ખતમ થઈ શકે. શું તમે ચાલુ રાખવા માંગો છો?
+ ડેટા પર ચાલુ રાખો
+ હમણાં નહીં
+ કોઈ નેટવર્ક નથી. ડાઉનલોડ કરવા માટે Wi-Fi સાથે કનેક્ટ કરો.
+ તમે મોબાઇલ ડેટા પર સ્વિચ કર્યું
+ નવા ડાઉનલોડ હવે મોબાઇલ ડેટા વાપરે છે. તેનાથી ખર્ચ થઈ શકે અથવા તમારો માસિક ડેટા ખતમ થઈ શકે.
+ ડેટા ખર્ચ ચેતવણીઓ
+
diff --git a/controller/app/src/main/res/values-hi/strings_networkpolicy.xml b/controller/app/src/main/res/values-hi/strings_networkpolicy.xml
new file mode 100644
index 000000000..842be643f
--- /dev/null
+++ b/controller/app/src/main/res/values-hi/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ आप मोबाइल डेटा का उपयोग कर रहे हैं
+ यह डाउनलोड मोबाइल डेटा का उपयोग करता है. इससे शुल्क लग सकता है या आपका मासिक डेटा खत्म हो सकता है. क्या आप जारी रखना चाहते हैं?
+ डेटा पर जारी रखें
+ अभी नहीं
+ कोई नेटवर्क नहीं. डाउनलोड करने के लिए Wi-Fi से कनेक्ट करें.
+ आप मोबाइल डेटा पर स्विच हो गए
+ नए डाउनलोड अब मोबाइल डेटा का उपयोग करते हैं. इससे शुल्क लग सकता है या आपका मासिक डेटा खत्म हो सकता है.
+ डेटा लागत अलर्ट
+
diff --git a/controller/app/src/main/res/values-hu/strings_networkpolicy.xml b/controller/app/src/main/res/values-hu/strings_networkpolicy.xml
new file mode 100644
index 000000000..d00a79caa
--- /dev/null
+++ b/controller/app/src/main/res/values-hu/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Mobiladatot használ
+ Ez a letöltés mobiladatot használ. Ez költséggel járhat, vagy elfogyaszthatja a havi keretét. Folytatja?
+ Folytatás mobiladaton
+ Most nem
+ Nincs hálózat. Csatlakozzon Wi-Fi-hez a letöltéshez.
+ Átváltott mobiladatra
+ Az új letöltések most mobiladatot használnak. Ez költséggel járhat, vagy elfogyaszthatja a havi keretét.
+ Adatköltség-figyelmeztetések
+
diff --git a/controller/app/src/main/res/values-in/strings_networkpolicy.xml b/controller/app/src/main/res/values-in/strings_networkpolicy.xml
new file mode 100644
index 000000000..4956360ea
--- /dev/null
+++ b/controller/app/src/main/res/values-in/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Anda menggunakan data seluler
+ Unduhan ini menggunakan data seluler. Ini dapat menimbulkan biaya atau menghabiskan kuota bulanan Anda. Apakah Anda ingin melanjutkan?
+ Lanjutkan dengan data
+ Nanti saja
+ Tidak ada jaringan. Sambungkan ke Wi-Fi untuk mengunduh.
+ Anda beralih ke data seluler
+ Unduhan baru kini menggunakan data seluler. Ini dapat menimbulkan biaya atau menghabiskan kuota bulanan Anda.
+ Peringatan biaya data
+
diff --git a/controller/app/src/main/res/values-it/strings_networkpolicy.xml b/controller/app/src/main/res/values-it/strings_networkpolicy.xml
new file mode 100644
index 000000000..865401da4
--- /dev/null
+++ b/controller/app/src/main/res/values-it/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Stai usando i dati mobili
+ Questo download usa i dati mobili. Può comportare costi o esaurire il tuo piano mensile. Vuoi continuare?
+ Continua con i dati
+ Non ora
+ Nessuna rete. Connettiti al Wi-Fi per scaricare.
+ Sei passato ai dati mobili
+ I nuovi download ora usano i dati mobili. Questo può comportare costi o esaurire il tuo piano mensile.
+ Avvisi sui costi dei dati
+
diff --git a/controller/app/src/main/res/values-ja/strings_networkpolicy.xml b/controller/app/src/main/res/values-ja/strings_networkpolicy.xml
new file mode 100644
index 000000000..f528cf56c
--- /dev/null
+++ b/controller/app/src/main/res/values-ja/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ モバイルデータを使用しています
+ このダウンロードはモバイルデータを使用します。料金が発生したり、月間データを使い切ったりする可能性があります。続行しますか?
+ モバイルデータで続行
+ 今はしない
+ ネットワークがありません。ダウンロードするには Wi-Fi に接続してください。
+ モバイルデータに切り替わりました
+ 新しいダウンロードはモバイルデータを使用します。料金が発生したり、月間データを使い切ったりする可能性があります。
+ データ料金の通知
+
diff --git a/controller/app/src/main/res/values-ko/strings_networkpolicy.xml b/controller/app/src/main/res/values-ko/strings_networkpolicy.xml
new file mode 100644
index 000000000..4d9dfc327
--- /dev/null
+++ b/controller/app/src/main/res/values-ko/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ 모바일 데이터를 사용 중입니다
+ 이 다운로드는 모바일 데이터를 사용합니다. 요금이 발생하거나 월 데이터를 모두 사용할 수 있습니다. 계속하시겠습니까?
+ 데이터로 계속
+ 나중에
+ 네트워크가 없습니다. 다운로드하려면 Wi-Fi에 연결하세요.
+ 모바일 데이터로 전환되었습니다
+ 새 다운로드가 이제 모바일 데이터를 사용합니다. 요금이 발생하거나 월 데이터를 모두 사용할 수 있습니다.
+ 데이터 요금 알림
+
diff --git a/controller/app/src/main/res/values-lt/strings_networkpolicy.xml b/controller/app/src/main/res/values-lt/strings_networkpolicy.xml
new file mode 100644
index 000000000..22f137617
--- /dev/null
+++ b/controller/app/src/main/res/values-lt/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Naudojate mobiliuosius duomenis
+ Šis atsisiuntimas naudoja mobiliuosius duomenis. Tai gali kainuoti arba išnaudoti mėnesio planą. Ar norite tęsti?
+ Tęsti su mobiliaisiais duomenimis
+ Ne dabar
+ Nėra tinklo. Prisijunkite prie Wi-Fi, kad atsisiųstumėte.
+ Perjungėte į mobiliuosius duomenis
+ Nauji atsisiuntimai dabar naudoja mobiliuosius duomenis. Tai gali kainuoti arba išnaudoti mėnesio planą.
+ Duomenų sąnaudų įspėjimai
+
diff --git a/controller/app/src/main/res/values-nl/strings_networkpolicy.xml b/controller/app/src/main/res/values-nl/strings_networkpolicy.xml
new file mode 100644
index 000000000..1b713b9fd
--- /dev/null
+++ b/controller/app/src/main/res/values-nl/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Je gebruikt mobiele data
+ Deze download gebruikt mobiele data. Dit kan kosten opleveren of je maandbundel opmaken. Wil je doorgaan?
+ Doorgaan met mobiele data
+ Niet nu
+ Geen netwerk. Maak verbinding met wifi om te downloaden.
+ Je bent overgeschakeld naar mobiele data
+ Nieuwe downloads gebruiken nu mobiele data. Dit kan kosten opleveren of je maandbundel opmaken.
+ Waarschuwingen datakosten
+
diff --git a/controller/app/src/main/res/values-no/strings_networkpolicy.xml b/controller/app/src/main/res/values-no/strings_networkpolicy.xml
new file mode 100644
index 000000000..721442fa8
--- /dev/null
+++ b/controller/app/src/main/res/values-no/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Du bruker mobildata
+ Denne nedlastingen bruker mobildata. Det kan koste penger eller bruke opp den månedlige datapakken. Vil du fortsette?
+ Fortsett med mobildata
+ Ikke nå
+ Ingen nettverk. Koble til Wi-Fi for å laste ned.
+ Du byttet til mobildata
+ Nye nedlastinger bruker nå mobildata. Det kan koste penger eller bruke opp den månedlige datapakken.
+ Varsler om datakostnad
+
diff --git a/controller/app/src/main/res/values-pl/strings_networkpolicy.xml b/controller/app/src/main/res/values-pl/strings_networkpolicy.xml
new file mode 100644
index 000000000..60fdeec8a
--- /dev/null
+++ b/controller/app/src/main/res/values-pl/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Używasz danych komórkowych
+ To pobieranie używa danych komórkowych. Może wiązać się z kosztami lub wyczerpać miesięczny pakiet. Czy chcesz kontynuować?
+ Kontynuuj na danych
+ Nie teraz
+ Brak sieci. Połącz się z Wi-Fi, aby pobrać.
+ Przełączono na dane komórkowe
+ Nowe pobierania używają teraz danych komórkowych. Może to wiązać się z kosztami lub wyczerpać miesięczny pakiet.
+ Alerty o kosztach danych
+
diff --git a/controller/app/src/main/res/values-pt/strings_networkpolicy.xml b/controller/app/src/main/res/values-pt/strings_networkpolicy.xml
new file mode 100644
index 000000000..b9fac679e
--- /dev/null
+++ b/controller/app/src/main/res/values-pt/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Você está usando dados móveis
+ Este download usa dados móveis. Pode gerar custos ou esgotar seu plano mensal. Deseja continuar?
+ Continuar com dados
+ Agora não
+ Sem rede. Conecte-se ao Wi-Fi para baixar.
+ Você mudou para dados móveis
+ Os novos downloads agora usam dados móveis. Isso pode gerar custos ou esgotar seu plano mensal.
+ Alertas de custo de dados
+
diff --git a/controller/app/src/main/res/values-ro/strings_networkpolicy.xml b/controller/app/src/main/res/values-ro/strings_networkpolicy.xml
new file mode 100644
index 000000000..c9ec3c08a
--- /dev/null
+++ b/controller/app/src/main/res/values-ro/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Folosești date mobile
+ Această descărcare folosește date mobile. Poate genera costuri sau îți poate epuiza planul lunar. Vrei să continui?
+ Continuă pe date mobile
+ Nu acum
+ Fără rețea. Conectează-te la Wi-Fi pentru a descărca.
+ Ai trecut la date mobile
+ Descărcările noi folosesc acum date mobile. Acest lucru poate genera costuri sau îți poate epuiza planul lunar.
+ Alerte privind costul datelor
+
diff --git a/controller/app/src/main/res/values-ru-rRU/strings_networkpolicy.xml b/controller/app/src/main/res/values-ru-rRU/strings_networkpolicy.xml
new file mode 100644
index 000000000..cb3002e76
--- /dev/null
+++ b/controller/app/src/main/res/values-ru-rRU/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Вы используете мобильные данные
+ Эта загрузка использует мобильные данные. Это может повлечь расходы или исчерпать месячный пакет. Продолжить?
+ Продолжить на мобильных данных
+ Не сейчас
+ Нет сети. Подключитесь к Wi-Fi для загрузки.
+ Вы переключились на мобильные данные
+ Новые загрузки теперь используют мобильные данные. Это может повлечь расходы или исчерпать месячный пакет.
+ Оповещения о расходе данных
+
diff --git a/controller/app/src/main/res/values-sk/strings_networkpolicy.xml b/controller/app/src/main/res/values-sk/strings_networkpolicy.xml
new file mode 100644
index 000000000..282d50359
--- /dev/null
+++ b/controller/app/src/main/res/values-sk/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Používate mobilné dáta
+ Toto sťahovanie používa mobilné dáta. Môže byť spoplatnené alebo vyčerpať váš mesačný limit. Chcete pokračovať?
+ Pokračovať na mobilných dátach
+ Teraz nie
+ Žiadna sieť. Pripojte sa k Wi-Fi na stiahnutie.
+ Prepli ste sa na mobilné dáta
+ Nové sťahovania teraz používajú mobilné dáta. Môže byť spoplatnené alebo vyčerpať váš mesačný limit.
+ Upozornenia na náklady za dáta
+
diff --git a/controller/app/src/main/res/values-sr/strings_networkpolicy.xml b/controller/app/src/main/res/values-sr/strings_networkpolicy.xml
new file mode 100644
index 000000000..7a4e8564f
--- /dev/null
+++ b/controller/app/src/main/res/values-sr/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Користите мобилне податке
+ Ово преузимање користи мобилне податке. Може да има трошак или да потроши ваш месечни план. Желите ли да наставите?
+ Настави на мобилним подацима
+ Не сада
+ Нема мреже. Повежите се на Wi-Fi да бисте преузели.
+ Прешли сте на мобилне податке
+ Нова преузимања сада користе мобилне податке. Ово може да има трошак или да потроши ваш месечни план.
+ Обавештења о трошку података
+
diff --git a/controller/app/src/main/res/values-sw/strings_networkpolicy.xml b/controller/app/src/main/res/values-sw/strings_networkpolicy.xml
new file mode 100644
index 000000000..740e138f4
--- /dev/null
+++ b/controller/app/src/main/res/values-sw/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Unatumia data ya simu
+ Upakuaji huu unatumia data ya simu. Inaweza kugharimu au kumaliza kifurushi chako cha mwezi. Ungependa kuendelea?
+ Endelea kwa data
+ Si sasa
+ Hakuna mtandao. Unganisha kwa Wi-Fi ili kupakua.
+ Umebadili hadi data ya simu
+ Vipakuliwa vipya sasa vinatumia data ya simu. Hii inaweza kugharimu au kumaliza kifurushi chako cha mwezi.
+ Arifa za gharama ya data
+
diff --git a/controller/app/src/main/res/values-ta/strings_networkpolicy.xml b/controller/app/src/main/res/values-ta/strings_networkpolicy.xml
new file mode 100644
index 000000000..25382c5a1
--- /dev/null
+++ b/controller/app/src/main/res/values-ta/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ நீங்கள் மொபைல் டேட்டாவைப் பயன்படுத்துகிறீர்கள்
+ இந்தப் பதிவிறக்கம் மொபைல் டேட்டாவைப் பயன்படுத்துகிறது. இதனால் கட்டணம் ஏற்படலாம் அல்லது உங்கள் மாதத் திட்டம் தீர்ந்துபோகலாம். தொடர விரும்புகிறீர்களா?
+ டேட்டாவில் தொடரவும்
+ இப்போது வேண்டாம்
+ நெட்வொர்க் இல்லை. பதிவிறக்க Wi-Fi உடன் இணைக்கவும்.
+ நீங்கள் மொபைல் டேட்டாவிற்கு மாறினீர்கள்
+ புதிய பதிவிறக்கங்கள் இப்போது மொபைல் டேட்டாவைப் பயன்படுத்துகின்றன. இதனால் கட்டணம் ஏற்படலாம் அல்லது உங்கள் மாதத் திட்டம் தீர்ந்துபோகலாம்.
+ டேட்டா செலவு எச்சரிக்கைகள்
+
diff --git a/controller/app/src/main/res/values-tr/strings_networkpolicy.xml b/controller/app/src/main/res/values-tr/strings_networkpolicy.xml
new file mode 100644
index 000000000..c8f2ef75e
--- /dev/null
+++ b/controller/app/src/main/res/values-tr/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Mobil veri kullanıyorsunuz
+ Bu indirme mobil veri kullanıyor. Ücretlendirilebilir veya aylık paketinizi tüketebilir. Devam etmek istiyor musunuz?
+ Mobil veriyle devam et
+ Şimdi değil
+ Ağ yok. İndirmek için Wi-Fi\'ye bağlanın.
+ Mobil veriye geçtiniz
+ Yeni indirmeler artık mobil veri kullanıyor. Bu, ücretlendirilebilir veya aylık paketinizi tüketebilir.
+ Veri maliyeti uyarıları
+
diff --git a/controller/app/src/main/res/values-uk/strings_networkpolicy.xml b/controller/app/src/main/res/values-uk/strings_networkpolicy.xml
new file mode 100644
index 000000000..27dcb008e
--- /dev/null
+++ b/controller/app/src/main/res/values-uk/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Ви використовуєте мобільні дані
+ Це завантаження використовує мобільні дані. Це може призвести до витрат або вичерпати місячний пакет. Продовжити?
+ Продовжити на мобільних даних
+ Не зараз
+ Немає мережі. Підключіться до Wi-Fi, щоб завантажити.
+ Ви перейшли на мобільні дані
+ Нові завантаження тепер використовують мобільні дані. Це може призвести до витрат або вичерпати місячний пакет.
+ Сповіщення про витрати даних
+
diff --git a/controller/app/src/main/res/values-vi/strings_networkpolicy.xml b/controller/app/src/main/res/values-vi/strings_networkpolicy.xml
new file mode 100644
index 000000000..35367cf1d
--- /dev/null
+++ b/controller/app/src/main/res/values-vi/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ Bạn đang dùng dữ liệu di động
+ Bản tải xuống này dùng dữ liệu di động. Có thể phát sinh chi phí hoặc dùng hết gói hàng tháng của bạn. Bạn có muốn tiếp tục không?
+ Tiếp tục bằng dữ liệu
+ Để sau
+ Không có mạng. Kết nối Wi-Fi để tải xuống.
+ Bạn đã chuyển sang dữ liệu di động
+ Các bản tải xuống mới hiện dùng dữ liệu di động. Điều này có thể phát sinh chi phí hoặc dùng hết gói hàng tháng của bạn.
+ Cảnh báo chi phí dữ liệu
+
diff --git a/controller/app/src/main/res/values-yo/strings_networkpolicy.xml b/controller/app/src/main/res/values-yo/strings_networkpolicy.xml
new file mode 100644
index 000000000..11bdb0461
--- /dev/null
+++ b/controller/app/src/main/res/values-yo/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ O n lo data alagbeka
+ Igbasilẹ yii n lo data alagbeka. O le san owo tabi lo package oṣooṣu rẹ tan. Ṣe o fẹ tẹsiwaju?
+ Tẹsiwaju pẹlu data
+ Kii ṣe bayii
+ Ko si nẹtiwọọki. Sopọ si Wi-Fi lati gbaa wọle.
+ O ti yipada si data alagbeka
+ Awọn igbasilẹ tuntun n lo data alagbeka bayii. Eyi le san owo tabi lo package oṣooṣu rẹ tan.
+ Awọn ikilọ iye owo data
+
diff --git a/controller/app/src/main/res/values-zh-rCN/strings_networkpolicy.xml b/controller/app/src/main/res/values-zh-rCN/strings_networkpolicy.xml
new file mode 100644
index 000000000..92acaec2c
--- /dev/null
+++ b/controller/app/src/main/res/values-zh-rCN/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ 您正在使用移动数据
+ 此下载会使用移动数据。可能产生费用或用尽您的每月流量。是否继续?
+ 使用数据继续
+ 暂不
+ 无网络。请连接 Wi-Fi 以下载。
+ 您已切换到移动数据
+ 新的下载现在会使用移动数据。这可能产生费用或用尽您的每月流量。
+ 数据费用提醒
+
diff --git a/controller/app/src/main/res/values/strings_networkpolicy.xml b/controller/app/src/main/res/values/strings_networkpolicy.xml
new file mode 100644
index 000000000..a5e3bc840
--- /dev/null
+++ b/controller/app/src/main/res/values/strings_networkpolicy.xml
@@ -0,0 +1,12 @@
+
+
+
+ You are on mobile data
+ This download uses mobile data. It can add cost or use up your monthly data. Do you want to continue?
+ Continue on data
+ Not now
+ No network. Connect to Wi-Fi to download.
+ You switched to mobile data
+ New downloads now use mobile data. This can add cost or use up your monthly data.
+ Data cost alerts
+
diff --git a/controller/app/src/main/res/values/strings_untranslated.xml b/controller/app/src/main/res/values/strings_untranslated.xml
index 85de9d5d7..0470a548a 100644
--- a/controller/app/src/main/res/values/strings_untranslated.xml
+++ b/controller/app/src/main/res/values/strings_untranslated.xml
@@ -10,7 +10,7 @@
-->
-
-
+
diff --git a/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClassTest.java b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClassTest.java
new file mode 100644
index 000000000..4b058eda6
--- /dev/null
+++ b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClassTest.java
@@ -0,0 +1,30 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Pure-JVM tests for the cost-class mapping. The cases mirror the device
+ * evidence in ADR-395: Wi-Fi "HIKVISION" carried NOT_METERED; the "Bienestar"
+ * LTE internet APN did not; the cellular IMS PDN carried NOT_METERED but no
+ * INTERNET, so it never becomes the internet-bearing default that gets classified.
+ */
+public class NetworkClassTest {
+
+ @Test
+ public void wifiUnmetered_isUnmetered() {
+ assertEquals(NetworkClass.UNMETERED, NetworkClass.from(true, true));
+ }
+
+ @Test
+ public void cellularInternetApn_isMetered() {
+ assertEquals(NetworkClass.METERED, NetworkClass.from(true, false));
+ }
+
+ @Test
+ public void noInternet_isNone_whateverTheMeteredFlag() {
+ assertEquals(NetworkClass.NONE, NetworkClass.from(false, false));
+ assertEquals(NetworkClass.NONE, NetworkClass.from(false, true));
+ }
+}
diff --git a/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyTest.java b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyTest.java
new file mode 100644
index 000000000..fe8bb76c4
--- /dev/null
+++ b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyTest.java
@@ -0,0 +1,33 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/** Pure-JVM tests for the heavy-start decision table. No Android, no network. */
+public class NetworkPolicyTest {
+
+ private final NetworkPolicy policy = new NetworkPolicy();
+
+ @Test
+ public void unmetered_alwaysAllows() {
+ assertEquals(NetworkPolicyDecision.ALLOW, policy.decideHeavyStart(NetworkClass.UNMETERED, false));
+ assertEquals(NetworkPolicyDecision.ALLOW, policy.decideHeavyStart(NetworkClass.UNMETERED, true));
+ }
+
+ @Test
+ public void metered_withoutConsent_needsConsent() {
+ assertEquals(NetworkPolicyDecision.NEEDS_CONSENT, policy.decideHeavyStart(NetworkClass.METERED, false));
+ }
+
+ @Test
+ public void metered_withConsent_allows() {
+ assertEquals(NetworkPolicyDecision.ALLOW, policy.decideHeavyStart(NetworkClass.METERED, true));
+ }
+
+ @Test
+ public void noNetwork_blocks_regardlessOfConsent() {
+ assertEquals(NetworkPolicyDecision.BLOCKED_NO_NETWORK, policy.decideHeavyStart(NetworkClass.NONE, false));
+ assertEquals(NetworkPolicyDecision.BLOCKED_NO_NETWORK, policy.decideHeavyStart(NetworkClass.NONE, true));
+ }
+}
diff --git a/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransitionTest.java b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransitionTest.java
new file mode 100644
index 000000000..13cbeaaa3
--- /dev/null
+++ b/controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransitionTest.java
@@ -0,0 +1,30 @@
+package org.appdevforall.k2go.networkpolicy.domain;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/** Pure-JVM tests for the "warn on becoming metered" edge rule. */
+public class NetworkTransitionTest {
+
+ @Test
+ public void unmeteredToMetered_warns() {
+ assertTrue(NetworkTransition.shouldWarn(NetworkClass.UNMETERED, NetworkClass.METERED));
+ }
+
+ @Test
+ public void noneToMetered_warns() {
+ assertTrue(NetworkTransition.shouldWarn(NetworkClass.NONE, NetworkClass.METERED));
+ }
+
+ @Test
+ public void meteredToMetered_doesNotWarn() {
+ assertFalse(NetworkTransition.shouldWarn(NetworkClass.METERED, NetworkClass.METERED));
+ }
+
+ @Test
+ public void meteredToUnmetered_doesNotWarn() {
+ assertFalse(NetworkTransition.shouldWarn(NetworkClass.METERED, NetworkClass.UNMETERED));
+ }
+}
diff --git a/controller/docs/ADR-395-network-cost-consent.md b/controller/docs/ADR-395-network-cost-consent.md
new file mode 100644
index 000000000..d4d01bc95
--- /dev/null
+++ b/controller/docs/ADR-395-network-cost-consent.md
@@ -0,0 +1,283 @@
+# ADR-395 -- Network cost consent (metered-data gate)
+
+- Status: Proposed
+- Date: 2026-09-06
+- Ticket: K2GO-395 (Relates to K2GO-4 "Resilient download contract")
+- Author: AppDevForAll
+
+## 1. Context
+
+K2Go downloads very heavy content: the rootfs image is ~2-4 GB, ZIM files reach
+tens to hundreds of GB, maps reach tens of GB. A user on a metered link (mobile
+data, or a metered Wi-Fi hotspot) can spend real money or burn a monthly data
+plan with a single tap, with no warning today.
+
+K2GO-4 already makes downloads **resilient** -- they survive a network change
+mid-download (Wi-Fi to mobile) by pause/resume/reconnect. Resilience is not the
+same fact as **cost consent**. Surviving a switch to mobile data is not the same
+as asking permission to spend it. This ADR defines the cost-consent mechanism.
+There is no ticket and no code for it today (verified: a repo-wide search for
+`isActiveNetworkMetered`, `NOT_METERED`, `setAllowedOverMetered` returns zero
+hits).
+
+### 1.1 The three-process reality (why this is not a socket problem)
+
+Egress does not come from one place. It comes from three, and only some are
+under the app's control:
+
+| What is downloaded | Which process pulls the bytes | App can throttle the socket? |
+|---|---|---|
+| Rootfs tarball + proot Debian base (GB) | on-device `aria2` (`libaria2c.so`) | Yes (own process) |
+| ZIM / Books / Maps / Kolibri content (GB) | the in-proot server (dash-node); device only POSTs + polls | **No** -- not the app's socket |
+| OTA APK, portal APK/PDF | Android system `DownloadManager` | Via its own API |
+| Manifests, `.meta4` size, catalog ETags (KB) | app process (`HttpURLConnection`) | Yes |
+
+The key consequence: for content (the biggest cost driver) the app **cannot**
+make the transfer "Wi-Fi only" at the network layer, because the in-proot server
+holds the socket, not the app. The only honest lever is to **not authorize the
+job to start** (gate before the POST), and to cancel/pause it through the
+existing REST cancel if the user asks.
+
+### 1.2 The primitive is metered, not "cellular vs Wi-Fi"
+
+The user goal is "do not spend costed data without asking". The correct signal
+is therefore **metered vs not-metered**, not transport. Two facts force this:
+
+- A phone hotspot is Wi-Fi but costs data. "Wi-Fi" does not mean "free".
+- On real hardware "cellular" is not one network. See the device evidence
+ (Section 6): the carrier's IMS PDN reports NOT_METERED while its internet APN
+ does not. Keying on `TRANSPORT_CELLULAR` would misjudge cost.
+
+So the rule keys on the **active default network's** `NET_CAPABILITY_NOT_METERED`.
+
+## 2. Decision
+
+Add one **cost-consent gate** consulted at the START of every heavy download,
+plus one process-wide **metered observer** for the proactive alert. Defensive by
+design: do not start anything costly on a metered link without consent; do not
+attempt fine control of a transfer already in flight (the app cannot).
+
+### 2.1 Behavior
+
+1. **Start gate.** Before a heavy download starts, classify the active default
+ network. If unmetered -> proceed. If metered and the user has not consented
+ this session -> ask ("You are on mobile data ... continue?"). If they decline,
+ do not start. If no network -> tell them they are offline. Once consent is
+ given, it holds for the session: 1 KB or 5 GB, it does not ask again.
+2. **Proactive alert.** If the default network crosses INTO metered while the app
+ runs -- even with nothing pending -- post a notification so the user knows
+ further activity spends data.
+3. **In flight = best effort only.** A transfer already running on a link the app
+ does not own is left alone. Resilience (K2GO-4) means it can pause/resume, but
+ this ADR does not add fine control. Offering "cancel or continue" on such a
+ transfer is a possible follow-up, not part of this contract.
+
+### 2.2 One source per fact (anti-duplication ledger)
+
+| Fact | Existing owner | This design |
+|---|---|---|
+| "network changed" | `sync/transport/NetworkStateLiveData` (the only default-network callback) | REUSED via `observeForever`; no second registration |
+| "what is the network" | `DashboardRebuild.hasInternet`, `InstallService.hasValidatedInternet` (two readers today) | New `AndroidNetworkClassifier` becomes the one reader; fold the two existing ones into it as a follow-up |
+| "is a heavy transfer running" | `ContentDownloadSession`, `InstallProgressRepository` | READ if needed; never duplicated |
+| "did the user consent to spend data" | none (new fact) | `SessionMeteredConsentStore` (in-memory) |
+
+### 2.3 Lifecycle of the consent grant
+
+- **Standing preference** (a future "Wi-Fi only" toggle) would be persisted,
+ default on. Not built yet; the gate already behaves as if it is on.
+- **Session grant** is ephemeral, in memory. Set by the consent dialog. Cleared
+ by the observer when the network leaves metered, and by process death (the only
+ store keeps it in memory). A persisted "always allow" is deliberately NOT
+ offered: it would defeat cost awareness and would be the stuck-marker
+ anti-pattern (a persisted flag nobody clears).
+- **If the process dies mid-metered-download**: on restart the grant is gone; if
+ still metered and a transfer wants to resume, the gate asks again. No stuck
+ state.
+
+## 3. Design (layered feature `networkpolicy`)
+
+New self-contained feature package `org.appdevforall.k2go.networkpolicy`, wired
+by hand (no DI), placed beside the existing `network` (DNS) feature.
+
+```
+networkpolicy/
+ domain/ NetworkClass, NetworkPolicyDecision, NetworkPolicy,
+ NetworkTransition, MeteredConsentStore (pure JVM, unit-tested)
+ data/ AndroidNetworkClassifier (the one ConnectivityManager reader),
+ SessionMeteredConsentStore (in-memory grant)
+ presentation/ NetworkPolicyGate (stateless start gate + consent dialog),
+ MeteredNetworkObserver (process-wide alert + grant lifecycle)
+```
+
+- `NetworkPolicyGate` is stateless, in the style of `OpReturnNavigator`: it owns
+ no "is metered" flag; it reads the live class and the session grant and returns
+ a decision.
+- `MeteredNetworkObserver` is one process-scoped owner started from
+ `IIABApplication`, in the style of `ServerLifecycleReconciler`.
+
+## 4. Seams (where the gate is consulted)
+
+Each content family has a `*ConfirmFragment` with a Start/Add button whose click
+is the user commit point. The gate wraps THAT click, never the background
+`*Provisioner.drain` (it runs every ~2 s to re-hand an already-authorized
+wishlist and would re-prompt). Every confirm fragment has the same shape as the
+reference (`ZimConfirmFragment`), so each remaining seam is a one-line wrap.
+
+| Family | Commit-point seam (exact) | Wrap |
+|---|---|---|
+| ZIM | `ZimConfirmFragment.java:105-110` -> `a.startZimDownload()` | LANDED (reference) |
+| Books | `BooksConfirmFragment.java:82` -> `a.startBooksDownload()` | `guardHeavyStart(a, a::startBooksDownload)` |
+| Kolibri | `KolibriConfirmFragment.java:227` -> `startLive(chosen)` | `guardHeavyStart(requireActivity(), () -> startLive(chosen))` |
+| Maps | `MapsConfirmFragment.java:90` -- the live `else` branch (~:99), NOT the `wizard` branch | wrap the live-download body |
+| Rootfs/modules install | `InstallService` started at `SetupProgressActivity.java:1401`; commit point is the wizard "Install" confirmation | wrap that confirm before starting InstallService |
+
+DownloadManager seams differ -- no `Service.start`; the app enqueues and the
+system transfers. Consult the gate first, then honor the decision (proceed on
+consent, or set `setAllowedOverMetered(false)` as the fallback):
+
+| Path | Enqueue site |
+|---|---|
+| OTA APK | `UpdateController.java:210-223` |
+| Portal APK | `PortalActivity.java:464-477` |
+| Portal PDF / other box file | `PortalActivity.java:502-513` |
+
+### 4.1 Recipe (content seam)
+
+Replace `X.startYDownload()` at the commit click with
+`NetworkPolicyGate.guardHeavyStart(activity, activity::startYDownload)`, where
+`activity` is the hosting Activity (the consent dialog needs an Activity context).
+A seam with no Activity (a pure background start) cannot show the dialog -- but
+those are post-authorization drains, correctly left ungated.
+
+### 4.2 Recipe (DownloadManager seam)
+
+Before `dm.enqueue(request)`: classify with `AndroidNetworkClassifier`. If metered
+and not consented, either ask via the gate or set
+`request.setAllowedOverMetered(false)` so the system holds it for Wi-Fi. If
+unmetered or already consented, enqueue as today.
+
+## 5. Reference implementation status (this change)
+
+Landed as a compiling, tested starting point for the implementer:
+
+- Domain, pure JVM, unit-tested: `NetworkClass`, `NetworkPolicy`,
+ `NetworkTransition`, `NetworkPolicyDecision`, `MeteredConsentStore`
+ (`NetworkPolicyTest`, `NetworkClassTest`, `NetworkTransitionTest` -- green).
+- Data: `AndroidNetworkClassifier`, `SessionMeteredConsentStore`.
+- Presentation: `NetworkPolicyGate` (+ `BrandDialog` consent), `MeteredNetworkObserver`.
+- Wiring: observer started in `IIABApplication`; gate wired at the ZIM commit
+ point (`ZimConfirmFragment`).
+- Strings translated to all 33 locales (machine-generated, pending human review)
+ in `values*/strings_networkpolicy.xml`; `strings_untranslated.xml` is clear.
+
+Remaining to finish the contract: the other four seams and the two-way fold of the
+existing `hasInternet` readers into the classifier. (l10n is done pending review.)
+
+## 6. Device evidence appendix (dark surfaces flattened)
+
+Measured on Samsung SM-A165M (`RF8Y80CE2DA`), Android 15, SIM "Bienestar" LTE
+(25 GB plan), via `adb shell svc wifi disable/enable` + `dumpsys connectivity`.
+
+| Network | Transport | Has NOT_METERED? | Has INTERNET? | Classifier verdict |
+|---|---|---|---|---|
+| Wi-Fi "HIKVISION_B7FD" (home router) | WIFI | Yes (Metered hint: false) | Yes | UNMETERED |
+| Cellular internet APN (rmnet1, default when Wi-Fi off) | CELLULAR | **No** | Yes | **METERED** |
+| Cellular IMS PDN | CELLULAR | Yes | No (IMS only) | never the internet default |
+| Phone hotspot "Galaxy A16 4AEC" (Samsung tether), seen by an OPPO CPH2557 client | WIFI | **No** (Metered hint: true) | Yes | **METERED** |
+
+Conclusions, now empirical, not assumed:
+
+1. Wi-Fi here is unmetered; the LTE internet APN is metered. The classifier rule
+ (`NOT_METERED` on the active default) produces the right verdict for both.
+2. "Cellular" is not monolithic: the IMS PDN carries NOT_METERED. A
+ transport-based rule would have called the whole radio unmetered and leaked
+ the plan. This is the concrete reason the rule keys on metered, not transport.
+3. The active default flips correctly on Wi-Fi toggle (Wi-Fi network id when on;
+ cellular id when off), so `getActiveNetwork()` is the right anchor.
+4. A phone hotspot CAN be flagged metered natively: the Samsung A16 tether
+ advertised the metered bit and the OPPO client's Wi-Fi network dropped
+ NOT_METERED (Metered hint: true). The classifier read METERED with no special
+ case -- the "Wi-Fi is not always free" case is caught by the same rule. Caveat:
+ this is OEM/AP-dependent. A router or hotspot that does not advertise the bit,
+ or a user who marks the Wi-Fi "unmetered", will read UNMETERED; the manual
+ override and the proactive alert exist for exactly that residual gap.
+
+## 7. Test protocol (remaining dark surfaces)
+
+Run before shipping the full contract:
+
+1. **Phone-hotspot metered detection (MEASURED -- Section 6, row 4).** Samsung A16
+ tether -> OPPO CPH2557 client: the client's Wi-Fi network had Metered hint:
+ true and no NOT_METERED, so the classifier read METERED with no special case.
+ The residual case to keep in mind: an AP/router that does not advertise the
+ metered bit, or a user who marks the Wi-Fi "unmetered", reads UNMETERED --
+ covered by the manual override (future toggle) and the proactive alert, not by
+ auto-detection. Re-run with other AP brands as they appear.
+2. **Premise: server content download consumes the SIM.** With the box up and the
+ device on cellular, start a small ZIM and watch `/proc/net/dev` `rmnet` bytes
+ climb. Architecturally certain (rmnet is the only uplink; proot has no
+ independent radio) -- measure once to confirm.
+3. **DownloadManager over metered.** Enqueue with `setAllowedOverMetered(false)`
+ on cellular; confirm it holds until Wi-Fi (and that the gate asks first, so the
+ hold is a chosen fallback, not a silent stall).
+4. **Callback latency.** Time `NetworkStateLiveData` firing after a transport flip
+ to confirm the proactive alert is prompt.
+
+## 8. Consequences
+
+- Positive: one owner for cost policy; reuses the existing change callback;
+ reduces (does not add) `hasInternet` duplication; empirically grounded rule.
+- Cost: four seams still to wire; device verification per the protocol. (The UI
+ strings are already translated to 33 locales, machine-generated, pending human
+ review.)
+- Deployment detail: the proactive alert posts a notification, so on Android 13+
+ it needs the POST_NOTIFICATIONS runtime permission. The observer swallows the
+ SecurityException when it is not granted, so the start gate (the primary cost
+ protection) still works with no notification permission. If the app does not
+ already request POST_NOTIFICATIONS elsewhere, the alert is silent until it does.
+- Out of scope: fine control of in-flight transfers; a persisted "always allow";
+ P2P (rsync clone -- LAN, no cost).
+
+## 9. Alternatives rejected
+
+- **Key on `TRANSPORT_CELLULAR`.** Rejected: the IMS PDN evidence shows cellular
+ is not uniformly metered, and a metered Wi-Fi hotspot would be missed.
+- **A per-transfer byte threshold ("ask only above N MB").** Rejected: a global
+ threshold across every seam adds complexity for little gain. The model is
+ binary consent. A small-payload exemption (< ~1 MB) may arrive later, per seam,
+ not as a global rule.
+- **Block at the socket / bind the process to Wi-Fi.** Rejected: cannot cover the
+ in-proot server's egress, and would fight `WifiNetworkBinder` (LAN sync).
+
+## 10. Open design questions (from the code-review second pass)
+
+The reference wiring gates the immediate commit point. The two-pass review found
+that this alone is not fully defensive, because the wishlist is a durable queue
+drained by an UNGATED background pass. These must be resolved before the feature
+is complete:
+
+1. **The commit point both banks and drains.** `SetupLibraryActivity.startZimDownload()`
+ calls `ZimWishlist.add(cart)` (the durable queue) and then `ZimProvisioner.drain()`.
+ Wrapping the whole method means a BLOCKED (offline) or declined start also skips
+ the banking, so the selection is lost instead of queued. Offline is not a cost
+ decision -- it should still bank for a later drain. Fix direction: bank
+ unconditionally; gate only the drain.
+
+2. **Banked items drain ungated.** `ZimProvisioner.drain` runs every ~2 s from
+ Home/SetupProgress and starts the real download with no gate (by design, to
+ avoid re-prompting). So any banked item downloads on whatever network is
+ active. The wizard-bank path (`zimWizardConfirm`, `banks == true`) banks
+ without ever passing the gate, so those ZIMs can download on metered data with
+ no consent. Gating the UI commit point does not cover them.
+
+3. **Consequence:** to be truly defensive the provisioner drain must be
+ consent-aware -- HOLD a banked item on metered-without-consent instead of
+ downloading, and surface the consent prompt when an Activity is next
+ foreground (the drain itself has no UI). The proactive alert only informs; it
+ does not hold the transfer. This is the real depth of the feature and should
+ be designed before wiring the remaining seams, not after.
+
+Decision needed (consult, do not default): keep the commit-point gate as a first
+layer and add drain-level consent enforcement, or move enforcement entirely into
+the provisioner/session start. The reference ZIM seam is left as-is pending this
+decision so the trade-off is visible, not silently patched.