diff --git a/DataControl-0.1.0.apk b/DataControl-0.1.0.apk index ae229ae..583c0ca 100644 Binary files a/DataControl-0.1.0.apk and b/DataControl-0.1.0.apk differ diff --git a/app/src/main/java/com/jttools/datacontrol/MainActivity.kt b/app/src/main/java/com/jttools/datacontrol/MainActivity.kt index 13ef1dd..3fb8590 100644 --- a/app/src/main/java/com/jttools/datacontrol/MainActivity.kt +++ b/app/src/main/java/com/jttools/datacontrol/MainActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import com.jttools.datacontrol.service.DataMonitorService import com.jttools.datacontrol.ui.SystemActions import com.jttools.datacontrol.ui.navigation.DataControlNavHost import com.jttools.datacontrol.ui.theme.DataControlTheme @@ -18,6 +19,10 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) systemActions = SystemActions(this) enableEdgeToEdge() + // Démarre le suivi de consommation dès l'ouverture de l'app, indépendamment du contrôle + // (pare-feu) : sans ça, le tableau de bord/les réglages restent à 0 tant que le contrôle + // n'a jamais été activé, car c'est ce service qui alimente MonitorStateHolder. + DataMonitorService.start(this) setContent { DataControlTheme { DataControlNavHost(systemActions = systemActions) diff --git a/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt b/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt index 0033034..2b2e5d3 100644 --- a/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt +++ b/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt @@ -53,9 +53,8 @@ data class UsageState( } enum class DataState { - OK, // sous le forfait - WARNING, // proche du forfait (>= 80%) - OVERRUN_FILTER, // dépassé, liste blanche seule active - OVERRUN_CUT, // dépassé, coupure totale cyclique en cours - WIFI // Wi-Fi actif, aucun filtrage appliqué + OK, // sous le seuil d'alerte, liste blanche active, pas de coupure en cours + WARNING, // proche ou au-delà du forfait (>= 80%), liste blanche active, pas de coupure en cours + CUT, // coupure totale cyclique en cours (indépendant du dépassement de forfait) + WIFI // Wi-Fi actif, aucun filtrage (sauf si filtrage Wi-Fi activé séparément) } diff --git a/app/src/main/java/com/jttools/datacontrol/service/DataMonitorService.kt b/app/src/main/java/com/jttools/datacontrol/service/DataMonitorService.kt index 2b31aa7..2a84563 100644 --- a/app/src/main/java/com/jttools/datacontrol/service/DataMonitorService.kt +++ b/app/src/main/java/com/jttools/datacontrol/service/DataMonitorService.kt @@ -32,12 +32,11 @@ import kotlin.math.max * pilote le pare-feu [FirewallVpnService] et met à jour la notification permanente. * * Règles appliquées (voir README) : - * - Sur Wi-Fi : aucun filtrage, jamais (le pare-feu est arrêté). - * - Sur données mobiles, sous le forfait : liste blanche déjà active en permanence - * (seules les apps cochées ont accès aux données mobiles). - * - Sur données mobiles, forfait dépassé : la liste blanche reste active, ET une coupure - * totale (même liste blanche) se déclenche cycliquement toutes les X minutes pendant - * une courte pulsation avant de revenir en mode "liste blanche seule". + * - Sur Wi-Fi : aucun filtrage par défaut (le pare-feu est arrêté), sauf si le filtrage Wi-Fi + * est explicitement activé (Liste blanche Wi-Fi). + * - Sur données mobiles : liste blanche active en permanence (seules les apps cochées ont accès), + * ET une coupure totale cyclique (même liste blanche) se déclenche automatiquement toutes les + * X minutes pendant une courte pulsation, que le forfait soit dépassé ou non. */ @AndroidEntryPoint class DataMonitorService : LifecycleService() { @@ -125,7 +124,6 @@ class DataMonitorService : LifecycleService() { } val consumedMo = UsageRepository.bytesToMo(consumedBytes) val overageMo = max(0L, consumedMo - settings.planLimitMo) - val isOverPlan = consumedMo > settings.planLimitMo val estimatedTotal = settings.planPriceEuro + overageMo * settings.overagePricePerMoEuro val percent = if (settings.planLimitMo > 0) consumedMo.toFloat() / settings.planLimitMo else 0f @@ -162,39 +160,35 @@ class DataMonitorService : LifecycleService() { dataState = DataState.WIFI } latestTransport == NetworkTransport.CELLULAR && !vpnRevoked && FirewallVpnService.isPrepared(this) -> { - if (!isOverPlan) { - resetCutoffCycle() - applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) - dataState = if (percent >= WARNING_THRESHOLD) DataState.WARNING else DataState.OK - } else { - val now = System.currentTimeMillis() - val intervalMs = settings.cutoffIntervalMinutes * 60_000L - if (cutoffCycleAnchorMillis == null) { + // La coupure cyclique tourne en continu sur données mobiles, indépendamment du + // dépassement du forfait : elle ne doit pas attendre le hors-forfait pour se déclencher. + val now = System.currentTimeMillis() + val intervalMs = settings.cutoffIntervalMinutes * 60_000L + if (cutoffCycleAnchorMillis == null) { + cutoffCycleAnchorMillis = now + persistCutoffState() + } + val anchor = cutoffCycleAnchorMillis!! + + if (inPulse) { + if (now - pulseStartMillis >= PULSE_DURATION_MS) { + inPulse = false cutoffCycleAnchorMillis = now persistCutoffState() - } - val anchor = cutoffCycleAnchorMillis!! - - if (inPulse) { - if (now - pulseStartMillis >= PULSE_DURATION_MS) { - inPulse = false - cutoffCycleAnchorMillis = now - persistCutoffState() - applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) - } else { - applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) - } - } else if (now - anchor >= intervalMs) { - inPulse = true - pulseStartMillis = now - persistCutoffState() - applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) - } else { applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) + } else { + applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) } - nextCutoffAt = if (inPulse) pulseStartMillis + PULSE_DURATION_MS else anchor + intervalMs - dataState = if (inPulse) DataState.OVERRUN_CUT else DataState.OVERRUN_FILTER + } else if (now - anchor >= intervalMs) { + inPulse = true + pulseStartMillis = now + persistCutoffState() + applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) + } else { + applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) } + nextCutoffAt = if (inPulse) pulseStartMillis + PULSE_DURATION_MS else anchor + intervalMs + dataState = if (inPulse) DataState.CUT else if (percent >= WARNING_THRESHOLD) DataState.WARNING else DataState.OK } else -> { // Pas de réseau, transport inconnu, ou VPN non préparé/révoqué : rien à filtrer. @@ -271,8 +265,7 @@ class DataMonitorService : LifecycleService() { !snapshot.serviceRunning -> Triple("⚪", getString(R.string.dashboard_control_disabled), Color.parseColor("#757575")) snapshot.dataState == DataState.OK -> Triple("🟢", getString(R.string.dashboard_state_ok), Color.parseColor("#2E7D32")) snapshot.dataState == DataState.WARNING -> Triple("🟠", getString(R.string.dashboard_state_warning), Color.parseColor("#EF6C00")) - snapshot.dataState == DataState.OVERRUN_FILTER -> Triple("🔴", getString(R.string.dashboard_state_overrun_filtered), Color.parseColor("#C62828")) - snapshot.dataState == DataState.OVERRUN_CUT -> Triple("🔴", getString(R.string.dashboard_state_overrun_cut), Color.parseColor("#C62828")) + snapshot.dataState == DataState.CUT -> Triple("🔴", getString(R.string.dashboard_state_cut), Color.parseColor("#C62828")) else -> Triple("🔵", getString(R.string.dashboard_state_wifi), Color.parseColor("#1565C0")) } diff --git a/app/src/main/java/com/jttools/datacontrol/ui/dashboard/DashboardScreen.kt b/app/src/main/java/com/jttools/datacontrol/ui/dashboard/DashboardScreen.kt index 765ffd7..c88c9fd 100644 --- a/app/src/main/java/com/jttools/datacontrol/ui/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/jttools/datacontrol/ui/dashboard/DashboardScreen.kt @@ -72,8 +72,7 @@ fun DashboardScreen(systemActions: SystemActions) { !snapshot.serviceRunning -> stringResource(R.string.dashboard_control_disabled) snapshot.dataState == DataState.OK -> stringResource(R.string.dashboard_state_ok) snapshot.dataState == DataState.WARNING -> stringResource(R.string.dashboard_state_warning) - snapshot.dataState == DataState.OVERRUN_FILTER -> stringResource(R.string.dashboard_state_overrun_filtered) - snapshot.dataState == DataState.OVERRUN_CUT -> stringResource(R.string.dashboard_state_overrun_cut) + snapshot.dataState == DataState.CUT -> stringResource(R.string.dashboard_state_cut) else -> stringResource(R.string.dashboard_state_wifi) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a93f077..6843126 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,8 +17,7 @@ État des données Sous le forfait Proche du forfait - Hors-forfait — liste blanche seule - Hors-forfait — coupure en cours + Coupure totale en cours Wi-Fi actif — aucun filtrage Estimation du coût Prix du forfait @@ -45,7 +44,7 @@ Réinitialisation automatique mensuelle Coupure automatique Intervalle de coupure (minutes) - Une fois le forfait dépassé, une coupure totale (même pour la liste blanche) se déclenche brièvement à cet intervalle. + Une coupure totale (même pour la liste blanche) se déclenche cycliquement à cet intervalle, que le forfait soit dépassé ou non. Autorisations Accès aux statistiques d\'utilisation Nécessaire pour mesurer la consommation de données mobiles