From a5b173003dd871ee1f7a0a11e34b0f49040d5271 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sun, 6 Sep 2026 21:37:23 -0600 Subject: [PATCH 1/4] K2GO-395 feat(networkpolicy): metered-data cost-consent gate + proactive alert Add a defensive gate that asks for consent before starting a heavy download on a metered network, plus a process-wide observer that warns on switching to metered data and clears the session consent on leaving metered. The rule keys on the active default network NET_CAPABILITY_NOT_METERED, not on transport -- grounded in device evidence (ADR-395): a cellular IMS PDN reports NOT_METERED while the internet APN does not, and a phone hotspot can arrive metered on the client. Pure-JVM domain with unit tests; data reads the one ConnectivityManager source; presentation is a stateless gate (BrandDialog) plus the observer started from IIABApplication. Reference wiring: the ZIM confirm commit point (ZimConfirmFragment), not the ~2s provisioner drain. Other seams (Books, Kolibri, rootfs install, DownloadManager) and the l10n migration are follow-ups per ADR-395. Strings parked in strings_untranslated.xml pending 33-locale translation. --- .../appdevforall/k2go/IIABApplication.java | 4 + .../data/AndroidNetworkClassifier.java | 43 ++++ .../data/SessionMeteredConsentStore.java | 37 +++ .../domain/MeteredConsentStore.java | 21 ++ .../networkpolicy/domain/NetworkClass.java | 35 +++ .../networkpolicy/domain/NetworkPolicy.java | 29 +++ .../domain/NetworkPolicyDecision.java | 14 ++ .../domain/NetworkTransition.java | 17 ++ .../presentation/MeteredNetworkObserver.java | 96 ++++++++ .../presentation/NetworkPolicyGate.java | 68 ++++++ .../k2go/redesign/ZimConfirmFragment.java | 9 +- .../main/res/values/strings_untranslated.xml | 12 +- .../domain/NetworkClassTest.java | 30 +++ .../domain/NetworkPolicyTest.java | 33 +++ .../domain/NetworkTransitionTest.java | 30 +++ .../docs/ADR-395-network-cost-consent.md | 217 ++++++++++++++++++ 16 files changed, 692 insertions(+), 3 deletions(-) create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/AndroidNetworkClassifier.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/data/SessionMeteredConsentStore.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/MeteredConsentStore.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClass.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicy.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyDecision.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransition.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/MeteredNetworkObserver.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/networkpolicy/presentation/NetworkPolicyGate.java create mode 100644 controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkClassTest.java create mode 100644 controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkPolicyTest.java create mode 100644 controller/app/src/test/java/org/appdevforall/k2go/networkpolicy/domain/NetworkTransitionTest.java create mode 100644 controller/docs/ADR-395-network-cost-consent.md 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: + *

+ */ +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/strings_untranslated.xml b/controller/app/src/main/res/values/strings_untranslated.xml index 85de9d5d7..8ef921124 100644 --- a/controller/app/src/main/res/values/strings_untranslated.xml +++ b/controller/app/src/main/res/values/strings_untranslated.xml @@ -10,7 +10,15 @@ --> - + + 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/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..96eb843e9 --- /dev/null +++ b/controller/docs/ADR-395-network-cost-consent.md @@ -0,0 +1,217 @@ +# 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 download family has a single static start entry point. The gate wraps the +**user commit point**, never the background drain (`ZimProvisioner.drain` runs +every ~2 s and would re-prompt). + +| Family | Start entry point | Commit-point seam | +|---|---|---| +| ZIM | `ZimDownloadService.start` | `ZimConfirmFragment` "Start" click (REFERENCE, landed) | +| Books | `BooksDownloadService.start` | Books confirm click (follow-up) | +| Kolibri | `KolibriSeedService.start` | Kolibri seed confirm (follow-up) | +| Rootfs/modules | `InstallService` via `SetupProgressActivity`/`ModuleProvisioner`/`MapsProvisioner` | install confirm (follow-up) | +| Portal APK/PDF + OTA | `DownloadManager` enqueue in `PortalActivity`, `UpdateController` | gate + `setAllowedOverMetered(false)` (follow-up) | + +## 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 parked in `strings_untranslated.xml` (WIP; translate to 33 locales and + move to a feature file before merge -- l10n policy). + +Remaining to finish the contract: the other four seams, the two-way fold of the +existing `hasInternet` readers into the classifier, and the l10n migration. + +## 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: new UI strings need 33-locale translation before merge; five seams to + wire; device verification per the protocol. +- 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). From 5fdbad77847e753b081c3ac56bad80bbf1dbafb0 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sun, 6 Sep 2026 21:53:46 -0600 Subject: [PATCH 2/4] K2GO-395 docs(networkpolicy): pinpoint remaining seams + recipes in ADR-395 Add exact file:line commit points for the Books/Kolibri/Maps/install/DownloadManager seams, a one-line wrap recipe per seam, and the POST_NOTIFICATIONS deployment note, so the remaining wiring is copy-paste for the next implementer. --- .../docs/ADR-395-network-cost-consent.md | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/controller/docs/ADR-395-network-cost-consent.md b/controller/docs/ADR-395-network-cost-consent.md index 96eb843e9..4d19b1889 100644 --- a/controller/docs/ADR-395-network-cost-consent.md +++ b/controller/docs/ADR-395-network-cost-consent.md @@ -117,17 +117,44 @@ networkpolicy/ ## 4. Seams (where the gate is consulted) -Each download family has a single static start entry point. The gate wraps the -**user commit point**, never the background drain (`ZimProvisioner.drain` runs -every ~2 s and would re-prompt). +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 | Start entry point | Commit-point seam | +| Family | Commit-point seam (exact) | Wrap | |---|---|---| -| ZIM | `ZimDownloadService.start` | `ZimConfirmFragment` "Start" click (REFERENCE, landed) | -| Books | `BooksDownloadService.start` | Books confirm click (follow-up) | -| Kolibri | `KolibriSeedService.start` | Kolibri seed confirm (follow-up) | -| Rootfs/modules | `InstallService` via `SetupProgressActivity`/`ModuleProvisioner`/`MapsProvisioner` | install confirm (follow-up) | -| Portal APK/PDF + OTA | `DownloadManager` enqueue in `PortalActivity`, `UpdateController` | gate + `setAllowedOverMetered(false)` (follow-up) | +| 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) @@ -202,6 +229,11 @@ Run before shipping the full contract: reduces (does not add) `hasInternet` duplication; empirically grounded rule. - Cost: new UI strings need 33-locale translation before merge; five seams to wire; device verification per the protocol. +- 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). From bc9ab6be2167c1f1678a0406a66de214befe4f7c Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sun, 6 Sep 2026 22:06:40 -0600 Subject: [PATCH 3/4] K2GO-395 l10n(networkpolicy): translate consent strings to 33 locales Move the 8 network-cost consent strings out of strings_untranslated.xml into values*/strings_networkpolicy.xml with all 33 locale values (machine-generated, pending human review), per the l10n policy. strings_untranslated.xml is clear again. ADR-395 updated: l10n done pending review. --- .../main/res/values-ar/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-az/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-bg/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-bn/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-cs/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-de/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-el/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-es/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-fa/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-fr/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-gu/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-hi/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-hu/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-in/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-it/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-ja/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-ko/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-lt/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-nl/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-no/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-pl/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-pt/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-ro/strings_networkpolicy.xml | 12 ++++++++++++ .../res/values-ru-rRU/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-sk/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-sr/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-sw/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-ta/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-tr/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-uk/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-vi/strings_networkpolicy.xml | 12 ++++++++++++ .../main/res/values-yo/strings_networkpolicy.xml | 12 ++++++++++++ .../res/values-zh-rCN/strings_networkpolicy.xml | 12 ++++++++++++ .../src/main/res/values/strings_networkpolicy.xml | 12 ++++++++++++ .../src/main/res/values/strings_untranslated.xml | 14 +++----------- controller/docs/ADR-395-network-cost-consent.md | 13 +++++++------ 36 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 controller/app/src/main/res/values-ar/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-az/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-bg/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-bn/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-cs/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-de/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-el/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-es/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-fa/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-fr/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-gu/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-hi/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-hu/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-in/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-it/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-ja/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-ko/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-lt/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-nl/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-no/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-pl/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-pt/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-ro/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-ru-rRU/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-sk/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-sr/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-sw/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-ta/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-tr/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-uk/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-vi/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-yo/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values-zh-rCN/strings_networkpolicy.xml create mode 100644 controller/app/src/main/res/values/strings_networkpolicy.xml 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 8ef921124..0470a548a 100644 --- a/controller/app/src/main/res/values/strings_untranslated.xml +++ b/controller/app/src/main/res/values/strings_untranslated.xml @@ -10,15 +10,7 @@ --> - - 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/docs/ADR-395-network-cost-consent.md b/controller/docs/ADR-395-network-cost-consent.md index 4d19b1889..8ba7117b7 100644 --- a/controller/docs/ADR-395-network-cost-consent.md +++ b/controller/docs/ADR-395-network-cost-consent.md @@ -167,11 +167,11 @@ Landed as a compiling, tested starting point for the implementer: - Presentation: `NetworkPolicyGate` (+ `BrandDialog` consent), `MeteredNetworkObserver`. - Wiring: observer started in `IIABApplication`; gate wired at the ZIM commit point (`ZimConfirmFragment`). -- Strings parked in `strings_untranslated.xml` (WIP; translate to 33 locales and - move to a feature file before merge -- l10n policy). +- 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, the two-way fold of the -existing `hasInternet` readers into the classifier, and the l10n migration. +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) @@ -227,8 +227,9 @@ Run before shipping the full contract: - Positive: one owner for cost policy; reuses the existing change callback; reduces (does not add) `hasInternet` duplication; empirically grounded rule. -- Cost: new UI strings need 33-locale translation before merge; five seams to - wire; device verification per the protocol. +- 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 From 0362edee0b561751b28ea70f04eb171cceadb2c6 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Sun, 6 Sep 2026 23:11:03 -0600 Subject: [PATCH 4/4] K2GO-395 docs(networkpolicy): record code-review second-pass design questions The two-pass review found that gating only the UI commit point is not fully defensive: the wishlist is a durable queue drained by an ungated background pass, so banked items (incl. the wizard-bank path) download on any network without consent, and wrapping startZimDownload() also discards the cart when offline. ADR sec.10 records these open design questions to resolve before wiring the rest. --- .../docs/ADR-395-network-cost-consent.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/controller/docs/ADR-395-network-cost-consent.md b/controller/docs/ADR-395-network-cost-consent.md index 8ba7117b7..d4d01bc95 100644 --- a/controller/docs/ADR-395-network-cost-consent.md +++ b/controller/docs/ADR-395-network-cost-consent.md @@ -248,3 +248,36 @@ Run before shipping the full contract: 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.