2 Commits
Author SHA1 Message Date
tuxgyverandClaude Sonnet 5 eecad06eea release: v0.1.7 — dissociation automatique quand le parent supprime l'appareil
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 21:43:40 +02:00
tuxgyverandClaude Sonnet 5 d7e55dbb91 release: v0.1.6 — retire le bouton de test VPN, README à jour
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 18:48:53 +02:00
6 changed files with 66 additions and 43 deletions

No files matched your search

+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
## v0.1.7 – 2026-09-20
- Contrôle parental : si le parent supprime l'appareil depuis FamilyControl, DataControl se dissocie automatiquement (règles et envois arrêtés) et le note dans le journal.
## v0.1.6 – 2026-09-20
- Onglet Journal : le bouton « Tester le VPN » est retiré (le VPN se monte correctement).
- README mis à jour (journal, contrôle parental, permissions, architecture).
## v0.1.5 – 2026-09-20 ## v0.1.5 – 2026-09-20
- Onglet Journal : bouton **Tester le VPN** (10 s) qui monte le tunnel quel que soit le réseau, pour savoir si le VPN peut se monter sur l'appareil ; les erreurs de démarrage du service VPN sont désormais tracées au lieu d'être silencieuses. - Onglet Journal : bouton **Tester le VPN** (10 s) qui monte le tunnel quel que soit le réseau, pour savoir si le VPN peut se monter sur l'appareil ; les erreurs de démarrage du service VPN sont désormais tracées au lieu d'être silencieuses.
+26 -3
View File
@@ -2,7 +2,7 @@
**DataControl** est une application Android qui surveille en arrière-plan la consommation de données mobiles et coupe automatiquement l'accès à Internet en cas de dépassement du forfait — sans jamais toucher au Wi-Fi. **DataControl** est une application Android qui surveille en arrière-plan la consommation de données mobiles et coupe automatiquement l'accès à Internet en cas de dépassement du forfait — sans jamais toucher au Wi-Fi.
> JT-Tools by Johnny — v0.1.0 > JT-Tools by Johnny — version courante et historique : voir [CHANGELOG.md](CHANGELOG.md) (aussi lisible dans l'onglet *À propos* de l'application).
--- ---
@@ -16,7 +16,23 @@
- **Coupure cyclique en cas de dépassement** — Une fois le forfait dépassé, une coupure totale (même pour la liste blanche) se déclenche automatiquement toutes les X minutes (paramétrable), pendant une courte pulsation, avant de revenir en mode "liste blanche seule". - **Coupure cyclique en cas de dépassement** — Une fois le forfait dépassé, une coupure totale (même pour la liste blanche) se déclenche automatiquement toutes les X minutes (paramétrable), pendant une courte pulsation, avant de revenir en mode "liste blanche seule".
- **Notification permanente** — Affiche la consommation (Mo / forfait / %), l'état des données avec un code couleur (🟢 sous le forfait, 🟠 proche du forfait, 🔴 hors-forfait/coupure, 🔵 Wi-Fi actif), et le coût estimé. Rafraîchie toutes les 20 secondes et à chaque changement de réseau. - **Notification permanente** — Affiche la consommation (Mo / forfait / %), l'état des données avec un code couleur (🟢 sous le forfait, 🟠 proche du forfait, 🔴 hors-forfait/coupure, 🔵 Wi-Fi actif), et le coût estimé. Rafraîchie toutes les 20 secondes et à chaque changement de réseau.
- **Démarrage au boot** — Le service redémarre automatiquement après un redémarrage de l'appareil si le contrôle était actif. - **Démarrage au boot** — Le service redémarre automatiquement après un redémarrage de l'appareil si le contrôle était actif.
- **Interface Material 3** — Thème clair/sombre avec couleurs dynamiques (Android 12+), 3 écrans : Tableau de bord, Réglages, Liste blanche. - **Alerte pare-feu inactif** — Si le contrôle est actif sur données mobiles mais que le tunnel VPN n'est pas monté, la notification passe en rouge (« PARE-FEU INACTIF ») : les données ne sont alors pas filtrées.
- **Journal de diagnostic** — Onglet *Journal* : état du VPN en direct, décisions du pare-feu, et, avec le service d'accessibilité, chaque application utilisée avec son verdict réseau/lancement. Conservé 15 jours.
- **Mise à jour intégrée** — Recherche et installation de la dernière Release Gitea depuis l'onglet *À propos*.
- **Mot de passe de verrouillage** — Protège les Réglages et la désactivation du contrôle.
- **Interface Material 3** — Thème clair/sombre avec couleurs dynamiques (Android 12+), 5 onglets : Tableau de bord, Réglages, Liste blanche, Journal, À propos.
---
## Contrôle parental (avec FamilyControl)
DataControl peut être associé à **FamilyControl** (application parent en TypeScript/Expo, dépôt séparé) via Firebase (Auth + Firestore). Sans fichier `firebase.properties` à la compilation (voir `firebase.properties.example`), ces fonctions sont simplement désactivées.
- **Appairage** par code à usage court, saisi dans *Réglages → Contrôle parental*.
- **Remontées** : batterie, position, état du pare-feu, consommation, trafic quotidien mobile/Wi-Fi, temps d'écran, événements (VPN révoqué, protection retirée…).
- **Consignes du parent** : contrôle activé/désactivé, applications autorisées (mobile et Wi-Fi), filtrage Wi-Fi.
- **Règles d'usage** (service d'accessibilité) : plages horaires (avec coupure optionnelle des données mobiles hors plage), temps d'écran quotidien global ou par application, ajout de temps, applications interdites. Le lanceur, le téléphone, le clavier et DataControl ne sont jamais bloqués.
- **Anti-désinstallation** par administrateur d'appareil (retirable depuis les Réglages Android : le parent est alors alerté ; le mot de passe protège le retrait depuis l'application).
--- ---
@@ -44,6 +60,9 @@ Le pare-feu est démarré/arrêté automatiquement selon le transport réseau ac
| Ignorer l'optimisation de la batterie | Éviter que le système ne tue le service de surveillance | | Ignorer l'optimisation de la batterie | Éviter que le système ne tue le service de surveillance |
| `QUERY_ALL_PACKAGES` | Lister toutes les applications installées dans l'écran Liste blanche | | `QUERY_ALL_PACKAGES` | Lister toutes les applications installées dans l'écran Liste blanche |
| `RECEIVE_BOOT_COMPLETED` | Relancer la surveillance après redémarrage si le contrôle était actif | | `RECEIVE_BOOT_COMPLETED` | Relancer la surveillance après redémarrage si le contrôle était actif |
| Position (dont arrière-plan) | Facultatif — transmise au parent si l'appareil est associé |
| Administrateur d'appareil | Facultatif — anti-désinstallation |
| Service d'accessibilité | Facultatif — détecte l'application au premier plan (sans lire les écrans) pour les règles d'usage et le journal |
--- ---
@@ -60,7 +79,10 @@ Le pare-feu est démarré/arrêté automatiquement selon le transport réseau ac
- **Kotlin + Jetpack Compose (Material 3)**, Hilt pour l'injection de dépendances. - **Kotlin + Jetpack Compose (Material 3)**, Hilt pour l'injection de dépendances.
- `data/` — `SettingsRepository` (DataStore Preferences), `UsageRepository` (TrafficStats), `InstalledAppsRepository` (PackageManager). - `data/` — `SettingsRepository` (DataStore Preferences), `UsageRepository` (TrafficStats), `InstalledAppsRepository` (PackageManager).
- `service/` — `FirewallVpnService` (VpnService), `ConnectivityObserver` (détection Wi-Fi/cellulaire), `DataMonitorService` (orchestration + notification), `MonitorStateHolder` (pont service ↔ UI), `BootReceiver`. - `service/` — `FirewallVpnService` (VpnService), `ConnectivityObserver` (détection Wi-Fi/cellulaire), `DataMonitorService` (orchestration + notification), `MonitorStateHolder` (pont service ↔ UI), `BootReceiver`.
- `ui/` — écrans Compose (`dashboard`, `settings`, `whitelist`), navigation par `NavigationBar`, thème dynamique. - `remote/` — `FirebaseGateway`, `RemoteSync` (appairage, consignes, remontées), `DeviceReporter` (batterie, position, trafic, état).
- `guard/` — `GuardEngine` (règles d'usage, temps d'écran), `AppGuardService` (accessibilité), `BlockedActivity`.
- `admin/` — administrateur d'appareil (anti-désinstallation). `log/` — journal de diagnostic.
- `ui/` — écrans Compose (`dashboard`, `settings`, `whitelist`, `journal`, `about`, `family`), navigation par `NavigationBar`, thème dynamique.
--- ---
@@ -70,6 +92,7 @@ Le pare-feu est démarré/arrêté automatiquement selon le transport réseau ac
make build # APK debug make build # APK debug
make release # APK release signé, copié en DataControl-<version>.apk make release # APK release signé, copié en DataControl-<version>.apk
make install # Installe le debug sur un appareil connecté (adb) make install # Installe le debug sur un appareil connecté (adb)
make deploy # APK release + publication en Release Gitea (voir `make help`)
``` ```
Prérequis : Android SDK (API 34), JDK 17. Le wrapper Gradle télécharge Gradle 8.13 au premier lancement. Prérequis : Android SDK (API 34), JDK 17. Le wrapper Gradle télécharge Gradle 8.13 au premier lancement.
@@ -8,6 +8,7 @@ import com.jttools.datacontrol.data.AppSettings
import com.jttools.datacontrol.data.InstalledAppsRepository import com.jttools.datacontrol.data.InstalledAppsRepository
import com.jttools.datacontrol.data.SettingsRepository import com.jttools.datacontrol.data.SettingsRepository
import com.jttools.datacontrol.data.TrafficDay import com.jttools.datacontrol.data.TrafficDay
import com.jttools.datacontrol.log.AppLog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -94,6 +95,20 @@ class RemoteSync @Inject constructor(
} }
.collect { cfg -> if (cfg != null) apply(cfg) } .collect { cfg -> if (cfg != null) apply(cfg) }
} }
scope.launch {
// Si le parent supprime l'appareil de la famille, on se dissocie localement (plus de règles,
// plus d'envois qui échoueraient en silence).
settingsRepository.settingsFlow
.map { it.familyId to it.deviceId }
.distinctUntilChanged()
.flatMapLatest { (fid, did) -> if (fid == null || did == null) flowOf(null) else existenceFlow(fid, did) }
.collect { exists ->
if (exists == false) {
AppLog.w("FamilyControl", "Appareil supprimé par le parent : dissociation locale")
settingsRepository.setPairing(null, null)
}
}
}
scope.launch { scope.launch {
// Liste des applications installées, pour que le parent puisse choisir lesquelles autoriser. // Liste des applications installées, pour que le parent puisse choisir lesquelles autoriser.
settingsRepository.settingsFlow.map { it.isPaired }.distinctUntilChanged().collect { paired -> settingsRepository.settingsFlow.map { it.isPaired }.distinctUntilChanged().collect { paired ->
@@ -102,6 +117,18 @@ class RemoteSync @Inject constructor(
} }
} }
/** Vrai/faux selon que le document de l'appareil existe côté serveur (ignore le cache local). */
private fun existenceFlow(fid: String, did: String) = callbackFlow<Boolean?> {
val db = gateway.db
if (db == null || gateway.ensureSignedIn() == null) { trySend(null); close(); return@callbackFlow }
val reg = db.collection("families").document(fid).collection("devices").document(did)
.addSnapshotListener { snap, err ->
if (err != null || snap == null || snap.metadata.isFromCache) return@addSnapshotListener
trySend(snap.exists())
}
awaitClose { reg.remove() }
}
private class Config(val map: Map<String, Any?>) private class Config(val map: Map<String, Any?>)
private fun configFlow(fid: String, did: String) = callbackFlow<Config?> { private fun configFlow(fid: String, did: String) = callbackFlow<Config?> {
@@ -23,7 +23,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -34,14 +33,11 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.jttools.datacontrol.log.AppLog import com.jttools.datacontrol.log.AppLog
import com.jttools.datacontrol.service.FirewallVpnService import com.jttools.datacontrol.service.FirewallVpnService
import com.jttools.datacontrol.ui.SystemActions
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable @Composable
fun JournalScreen(systemActions: SystemActions) { fun JournalScreen() {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val entries by AppLog.entries.collectAsStateWithLifecycle() val entries by AppLog.entries.collectAsStateWithLifecycle()
// État en direct du pare-feu, rafraîchi chaque seconde : première chose à regarder si le VPN n'apparaît pas. // État en direct du pare-feu, rafraîchi chaque seconde : première chose à regarder si le VPN n'apparaît pas.
@@ -67,18 +63,6 @@ fun JournalScreen(systemActions: SystemActions) {
}) { Text("Partager") } }) { Text("Partager") }
OutlinedButton(onClick = { AppLog.clear() }) { Text("Effacer") } OutlinedButton(onClick = { AppLog.clear() }) { Text("Effacer") }
} }
Button(onClick = {
scope.launch {
if (!FirewallVpnService.isPrepared(context)) {
AppLog.i("Test VPN", "autorisation VPN absente : demande à l'utilisateur")
systemActions.requestVpnPermission { granted ->
AppLog.i("Test VPN", "autorisation VPN ${if (granted) "accordée" else "REFUSÉE"} — relancez le test")
}
return@launch
}
runVpnTest(context)
}
}, modifier = Modifier.fillMaxWidth()) { Text("Tester le VPN (10 s, coupe les autres applications)") }
LazyColumn(Modifier.fillMaxWidth().weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { LazyColumn(Modifier.fillMaxWidth().weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
items(entries.asReversed()) { line -> items(entries.asReversed()) { line ->
val color = when { val color = when {
@@ -99,23 +83,3 @@ private fun liveStatus(context: Context): String = buildString {
appendLine("Conservation du journal : ${AppLog.RETENTION_DAYS} jours") appendLine("Conservation du journal : ${AppLog.RETENTION_DAYS} jours")
append("Attendu : le tunnel n'est monté QUE sur données mobiles, contrôle activé.") append("Attendu : le tunnel n'est monté QUE sur données mobiles, contrôle activé.")
} }
/** Monte le tunnel 10 s quel que soit le réseau : prouve (ou infirme) que le VPN peut se monter sur cet appareil. */
private suspend fun runVpnTest(context: Context) {
val app = context.applicationContext
AppLog.i("Test VPN", "démarrage : demande de montage du tunnel (mode blocage total)")
try {
androidx.core.content.ContextCompat.startForegroundService(app, FirewallVpnService.startBlockAllIntent(app))
} catch (e: Exception) {
AppLog.e("Test VPN", "le service VPN n'a pas pu démarrer", e)
return
}
delay(3_000)
if (FirewallVpnService.tunnelActive) AppLog.i("Test VPN", "SUCCÈS : tunnel monté — l'icône clé doit être visible")
else AppLog.e("Test VPN", "ÉCHEC : tunnel non monté après 3 s (voir les lignes FirewallVpnService ci-dessus)")
delay(7_000)
runCatching {
androidx.core.content.ContextCompat.startForegroundService(app, FirewallVpnService.stopIntent(app))
}
AppLog.i("Test VPN", "fin du test : tunnel arrêté")
}
@@ -75,7 +75,7 @@ fun DataControlNavHost(systemActions: SystemActions) {
composable(Destination.Dashboard.route) { DashboardScreen(systemActions) } composable(Destination.Dashboard.route) { DashboardScreen(systemActions) }
composable(Destination.Settings.route) { SettingsScreen(systemActions) } composable(Destination.Settings.route) { SettingsScreen(systemActions) }
composable(Destination.Whitelist.route) { WhitelistScreen() } composable(Destination.Whitelist.route) { WhitelistScreen() }
composable(Destination.Journal.route) { JournalScreen(systemActions) } composable(Destination.Journal.route) { JournalScreen() }
composable(Destination.About.route) { AboutScreen() } composable(Destination.About.route) { AboutScreen() }
} }
} }
+2 -2
View File
@@ -1,2 +1,2 @@
versionName=0.1.5 versionName=0.1.7
versionCode=6 versionCode=8