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