diff --git a/CHANGELOG.md b/CHANGELOG.md index 574dd71..a83beda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.1.8 – 2026-09-21 + +- Contrôle parental : la configuration Firebase se **saisit dans l'application** (Réglages → Contrôle parental → Configuration Firebase), en collant la chaîne « fc1:… » partagée par FamilyControl ou le bloc « firebaseConfig » de la console. Plus besoin de recompiler ni de fichier firebase.properties (qui reste possible comme valeur par défaut). +- La liaison Firebase se relance automatiquement quand la configuration change. + ## 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. diff --git a/README.md b/README.md index 79207b4..53cac5f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ ## 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. +DataControl peut être associé à **FamilyControl** (application parent en TypeScript/Expo, dépôt séparé) via Firebase (Auth + Firestore). La configuration du projet Firebase se **colle dans l'application** (*Réglages → Contrôle parental → Configuration Firebase*), sans recompiler ; sans elle, ces fonctions sont simplement désactivées (un `firebase.properties` à la compilation reste possible comme valeur par défaut). - **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…). diff --git a/app/src/main/java/com/jttools/datacontrol/DataControlApp.kt b/app/src/main/java/com/jttools/datacontrol/DataControlApp.kt index 0f1cf17..9a13eff 100644 --- a/app/src/main/java/com/jttools/datacontrol/DataControlApp.kt +++ b/app/src/main/java/com/jttools/datacontrol/DataControlApp.kt @@ -9,6 +9,8 @@ import com.jttools.datacontrol.service.DataMonitorRestartWorker import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -17,11 +19,16 @@ import javax.inject.Inject class DataControlApp : Application() { @Inject lateinit var settingsRepository: SettingsRepository + @Inject lateinit var firebaseGateway: com.jttools.datacontrol.remote.FirebaseGateway override fun onCreate() { super.onCreate() com.jttools.datacontrol.log.AppLog.init(this) CoroutineScope(Dispatchers.IO).launch { settingsRepository.resetPasswordOnce() } + // La config Firebase se saisit dans l'appli : on l'applique au démarrage et à chaque modification. + CoroutineScope(Dispatchers.IO).launch { + settingsRepository.settingsFlow.map { it.firebase }.distinctUntilChanged().collect { firebaseGateway.apply(it) } + } // Filet de sécurité : relance périodiquement DataMonitorService s'il a été tué en arrière-plan. // enqueueUniquePeriodicWork + KEEP : sans effet si déjà planifié, donc sûr à appeler à chaque démarrage. val request = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).build() 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 c9ce05a..c4ce109 100644 --- a/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt +++ b/app/src/main/java/com/jttools/datacontrol/data/AppSettings.kt @@ -29,7 +29,9 @@ data class AppSettings( /** Horodatage (ms) de la dernière configuration distante appliquée. */ val remoteConfigVersion: Long = 0L, /** Règles d'usage (plages horaires, limites de temps, applications interdites), au format JSON. */ - val rulesJson: String? = null + val rulesJson: String? = null, + /** Configuration Firebase saisie dans l'application (null : voir firebase.properties de compilation). */ + val firebase: com.jttools.datacontrol.remote.FirebaseConfig? = null ) { val isPaired: Boolean get() = !familyId.isNullOrEmpty() && !deviceId.isNullOrEmpty() diff --git a/app/src/main/java/com/jttools/datacontrol/data/SettingsRepository.kt b/app/src/main/java/com/jttools/datacontrol/data/SettingsRepository.kt index 9814e58..088eb30 100644 --- a/app/src/main/java/com/jttools/datacontrol/data/SettingsRepository.kt +++ b/app/src/main/java/com/jttools/datacontrol/data/SettingsRepository.kt @@ -46,6 +46,9 @@ class SettingsRepository @Inject constructor( val DEVICE_ID = stringPreferencesKey("device_id") val REMOTE_CONFIG_VERSION = longPreferencesKey("remote_config_version") val RULES_JSON = stringPreferencesKey("rules_json") + val FIREBASE_API_KEY = stringPreferencesKey("firebase_api_key") + val FIREBASE_APP_ID = stringPreferencesKey("firebase_app_id") + val FIREBASE_PROJECT_ID = stringPreferencesKey("firebase_project_id") val USAGE_DAY = stringPreferencesKey("usage_day") val USAGE_JSON = stringPreferencesKey("usage_json") val TRAFFIC_DAY = stringPreferencesKey("traffic_day") @@ -78,7 +81,12 @@ class SettingsRepository @Inject constructor( familyId = prefs[Keys.FAMILY_ID], deviceId = prefs[Keys.DEVICE_ID], remoteConfigVersion = prefs[Keys.REMOTE_CONFIG_VERSION] ?: defaults.remoteConfigVersion, - rulesJson = prefs[Keys.RULES_JSON] + rulesJson = prefs[Keys.RULES_JSON], + firebase = run { + val k = prefs[Keys.FIREBASE_API_KEY]; val a = prefs[Keys.FIREBASE_APP_ID]; val p = prefs[Keys.FIREBASE_PROJECT_ID] + if (k.isNullOrBlank() || a.isNullOrBlank() || p.isNullOrBlank()) null + else com.jttools.datacontrol.remote.FirebaseConfig(k, a, p) + } ) } @@ -286,4 +294,17 @@ class SettingsRepository @Inject constructor( it[Keys.USAGE_JSON] = json } } + + /** Enregistre (ou efface, si null) la configuration Firebase saisie dans l'application. */ + suspend fun setFirebaseConfig(config: com.jttools.datacontrol.remote.FirebaseConfig?) { + context.dataStore.edit { + if (config == null) { + it.remove(Keys.FIREBASE_API_KEY); it.remove(Keys.FIREBASE_APP_ID); it.remove(Keys.FIREBASE_PROJECT_ID) + } else { + it[Keys.FIREBASE_API_KEY] = config.apiKey + it[Keys.FIREBASE_APP_ID] = config.appId + it[Keys.FIREBASE_PROJECT_ID] = config.projectId + } + } + } } diff --git a/app/src/main/java/com/jttools/datacontrol/remote/DeviceReporter.kt b/app/src/main/java/com/jttools/datacontrol/remote/DeviceReporter.kt index df2649a..2323b95 100644 --- a/app/src/main/java/com/jttools/datacontrol/remote/DeviceReporter.kt +++ b/app/src/main/java/com/jttools/datacontrol/remote/DeviceReporter.kt @@ -53,7 +53,7 @@ class DeviceReporter @Inject constructor( private var lastUsageSentTotal = -1L fun start(scope: CoroutineScope) { - if (started || !remote.available) return + if (started) return started = true remote.start(scope) diff --git a/app/src/main/java/com/jttools/datacontrol/remote/FirebaseConfig.kt b/app/src/main/java/com/jttools/datacontrol/remote/FirebaseConfig.kt new file mode 100644 index 0000000..9218fda --- /dev/null +++ b/app/src/main/java/com/jttools/datacontrol/remote/FirebaseConfig.kt @@ -0,0 +1,30 @@ +package com.jttools.datacontrol.remote + +import android.util.Base64 +import org.json.JSONObject + +/** Identifiants d'un projet Firebase (ceux du bloc `firebaseConfig` de la console). */ +data class FirebaseConfig(val apiKey: String, val appId: String, val projectId: String) { + companion object { + /** + * Lit ce que l'utilisateur colle : le bloc `firebaseConfig` de la console (JavaScript), un JSON, + * ou une chaîne « fc1:… » partagée par FamilyControl. Renvoie null si apiKey, appId ou projectId manque. + */ + fun parse(input: String): FirebaseConfig? { + val text = input.trim() + if (text.startsWith("fc1:")) { + return runCatching { + val json = JSONObject(String(Base64.decode(text.removePrefix("fc1:"), Base64.DEFAULT), Charsets.UTF_8)) + build(json.optString("apiKey"), json.optString("appId"), json.optString("projectId")) + }.getOrNull() + } + fun pick(name: String) = + Regex("""["']?$name["']?\s*[:=]\s*["']([^"']+)["']""").find(text)?.groupValues?.get(1) + return build(pick("apiKey"), pick("appId"), pick("projectId")) + } + + private fun build(apiKey: String?, appId: String?, projectId: String?): FirebaseConfig? = + if (apiKey.isNullOrBlank() || appId.isNullOrBlank() || projectId.isNullOrBlank()) null + else FirebaseConfig(apiKey.trim(), appId.trim(), projectId.trim()) + } +} diff --git a/app/src/main/java/com/jttools/datacontrol/remote/FirebaseGateway.kt b/app/src/main/java/com/jttools/datacontrol/remote/FirebaseGateway.kt index 4e4c590..6af040e 100644 --- a/app/src/main/java/com/jttools/datacontrol/remote/FirebaseGateway.kt +++ b/app/src/main/java/com/jttools/datacontrol/remote/FirebaseGateway.kt @@ -6,37 +6,65 @@ import com.google.firebase.FirebaseOptions import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import com.jttools.datacontrol.BuildConfig +import com.jttools.datacontrol.log.AppLog import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton +/** État du lien Firebase : [generation] change à chaque (ré)initialisation, pour relancer les écoutes. */ +data class GatewayState(val generation: Int = 0, val enabled: Boolean = false, val projectId: String? = null) + /** - * Accès à Firebase (Auth anonyme + Firestore) initialisé depuis `firebase.properties` - * (voir build.gradle.kts), sans plugin google-services : si la config est absente, - * [enabled] est faux et toutes les fonctions distantes restent inertes. + * Accès à Firebase (Auth anonyme + Firestore). La configuration se saisit dans l'application + * (Réglages → Contrôle parental) et peut changer à tout moment ; à défaut, on retombe sur + * `firebase.properties` (compilation). Sans configuration, [enabled] est faux et tout reste inerte. */ @Singleton class FirebaseGateway @Inject constructor( @ApplicationContext private val context: Context ) { - val enabled: Boolean = BuildConfig.FIREBASE_API_KEY.isNotBlank() && - BuildConfig.FIREBASE_APP_ID.isNotBlank() && - BuildConfig.FIREBASE_PROJECT_ID.isNotBlank() + private val _state = MutableStateFlow(GatewayState()) + val state: StateFlow = _state.asStateFlow() + val enabled: Boolean get() = _state.value.enabled - private val app: FirebaseApp? by lazy { - if (!enabled) return@lazy null - runCatching { - FirebaseApp.getApps(context).firstOrNull() - ?: FirebaseApp.initializeApp( + @Volatile private var app: FirebaseApp? = null + private var currentKey: String? = null + + private fun buildConfigFallback(): FirebaseConfig? = + if (BuildConfig.FIREBASE_API_KEY.isNotBlank() && BuildConfig.FIREBASE_APP_ID.isNotBlank() && + BuildConfig.FIREBASE_PROJECT_ID.isNotBlank() + ) FirebaseConfig(BuildConfig.FIREBASE_API_KEY, BuildConfig.FIREBASE_APP_ID, BuildConfig.FIREBASE_PROJECT_ID) + else null + + /** Applique la configuration saisie [saved] (ou, à défaut, celle de compilation). Sans effet si elle n'a pas changé. */ + @Synchronized + fun apply(saved: FirebaseConfig?) { + val cfg = saved ?: buildConfigFallback() + val key = cfg?.let { "${it.apiKey}|${it.appId}|${it.projectId}" } + if (key == currentKey && (key == null || app != null)) return + + app?.let { runCatching { it.delete() } } + app = null + currentKey = key + if (cfg != null) { + app = runCatching { + FirebaseApp.initializeApp( context, FirebaseOptions.Builder() - .setApiKey(BuildConfig.FIREBASE_API_KEY) - .setApplicationId(BuildConfig.FIREBASE_APP_ID) - .setProjectId(BuildConfig.FIREBASE_PROJECT_ID) + .setApiKey(cfg.apiKey) + .setApplicationId(cfg.appId) + .setProjectId(cfg.projectId) .build() ) - }.getOrNull() + }.onFailure { AppLog.e("Firebase", "Initialisation impossible", it) }.getOrNull() + } + val ok = app != null + AppLog.i("Firebase", if (ok) "Configuré (projet ${cfg?.projectId})" else "Non configuré") + _state.value = GatewayState(_state.value.generation + 1, ok, cfg?.projectId?.takeIf { ok }) } val db: FirebaseFirestore? get() = app?.let { FirebaseFirestore.getInstance(it) } @@ -46,6 +74,8 @@ class FirebaseGateway @Inject constructor( suspend fun ensureSignedIn(): String? { val a = auth ?: return null a.currentUser?.let { return it.uid } - return runCatching { a.signInAnonymously().await().user?.uid }.getOrNull() + return runCatching { a.signInAnonymously().await().user?.uid } + .onFailure { AppLog.w("Firebase", "Connexion anonyme impossible : ${it.message}") } + .getOrNull() } } diff --git a/app/src/main/java/com/jttools/datacontrol/remote/RemoteSync.kt b/app/src/main/java/com/jttools/datacontrol/remote/RemoteSync.kt index ca26e1b..754ce68 100644 --- a/app/src/main/java/com/jttools/datacontrol/remote/RemoteSync.kt +++ b/app/src/main/java/com/jttools/datacontrol/remote/RemoteSync.kt @@ -10,6 +10,8 @@ import com.jttools.datacontrol.data.SettingsRepository import com.jttools.datacontrol.data.TrafficDay import com.jttools.datacontrol.log.AppLog import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -44,6 +46,7 @@ class RemoteSync @Inject constructor( private val installedApps: InstalledAppsRepository ) { val available: Boolean get() = gateway.enabled + val gatewayState = gateway.state private fun deviceDoc(s: AppSettings): DocumentReference? { val db = gateway.db ?: return null @@ -82,37 +85,43 @@ class RemoteSync @Inject constructor( settingsRepository.setPairing(null, null) } - /** Écoute les consignes du parent et les applique aux réglages locaux. */ + /** Écoute les consignes du parent et les applique ; relancé à chaque (ré)initialisation de Firebase. */ @OptIn(ExperimentalCoroutinesApi::class) fun start(scope: CoroutineScope) { - if (!gateway.enabled) return scope.launch { - settingsRepository.settingsFlow - .map { it.familyId to it.deviceId } - .distinctUntilChanged() - .flatMapLatest { (fid, did) -> - if (fid == null || did == null) flowOf(null) else configFlow(fid, did) - } - .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) + gateway.state.collectLatest { st -> + if (!st.enabled) return@collectLatest + coroutineScope { + launch { + settingsRepository.settingsFlow + .map { it.familyId to it.deviceId } + .distinctUntilChanged() + .flatMapLatest { (fid, did) -> + if (fid == null || did == null) flowOf(null) else configFlow(fid, did) + } + .collect { cfg -> if (cfg != null) apply(cfg) } + } + 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) + } + } + } + launch { + // Liste des applications installées, pour que le parent puisse choisir lesquelles autoriser. + settingsRepository.settingsFlow.map { it.isPaired }.distinctUntilChanged().collect { paired -> + if (paired) pushApps() + } } } - } - scope.launch { - // Liste des applications installées, pour que le parent puisse choisir lesquelles autoriser. - settingsRepository.settingsFlow.map { it.isPaired }.distinctUntilChanged().collect { paired -> - if (paired) pushApps() } } } diff --git a/app/src/main/java/com/jttools/datacontrol/ui/about/AboutContent.kt b/app/src/main/java/com/jttools/datacontrol/ui/about/AboutContent.kt index 5adb109..2e61628 100644 --- a/app/src/main/java/com/jttools/datacontrol/ui/about/AboutContent.kt +++ b/app/src/main/java/com/jttools/datacontrol/ui/about/AboutContent.kt @@ -32,7 +32,7 @@ object AboutContent { AboutEntry("Suivi à distance", "Batterie, position (arrière-plan compris si autorisé), état du pare-feu, trafic quotidien mobile/Wi-Fi et journal d'événements."), AboutEntry("Accès réseau par application", "Le parent autorise ou bloque Internet application par application, sur données mobiles comme sur Wi-Fi."), AboutEntry("Plages horaires, temps d'écran, applications interdites", "Appliqués par un service d'accessibilité : verrouillage hors plage, limite quotidienne globale ou par application, ajout de temps, lancement refusé."), - AboutEntry("Condition", "Ces fonctions n'existent que si l'application a été compilée avec une configuration Firebase et associée à un compte parent.") + AboutEntry("Condition", "Ces fonctions demandent une configuration Firebase saisie dans Réglages → Contrôle parental, puis l'association à un compte parent.") )) ) @@ -123,7 +123,7 @@ object AboutContent { AboutEntry("Service de premier plan + relance périodique", "Un service permanent et une tâche WorkManager toutes les 15 minutes, car certains systèmes arrêtent agressivement les applications en arrière-plan."), AboutEntry("Kotlin, Jetpack Compose (Material 3), Hilt, DataStore", "Interface déclarative et thème dynamique (Android 12+) ; réglages dans DataStore Preferences."), AboutEntry("HttpURLConnection plutôt qu'un client HTTP tiers", "Seule la mise à jour fait un appel HTTP : pas de dépendance réseau supplémentaire pour cela."), - AboutEntry("Firebase sans plugin google-services", "Firestore et l'authentification sont initialisés depuis un fichier local non versionné (firebase.properties). Sans lui, l'application se compile normalement et le contrôle à distance est simplement désactivé."), + AboutEntry("Firebase sans plugin google-services", "La configuration se saisit dans l'application (Réglages → Contrôle parental) et peut changer sans recompiler ; sans elle, le contrôle à distance est simplement désactivé. Un fichier firebase.properties à la compilation reste possible comme valeur par défaut."), AboutEntry("Service d'accessibilité plutôt que statistiques d'usage", "Permet de renvoyer à l'accueil et d'afficher un écran de blocage dès qu'une application interdite passe au premier plan."), AboutEntry("Administrateur d'appareil plutôt que Device Owner", "Pas de réinitialisation d'usine ni de branchement à un PC ; en contrepartie, la protection est moins robuste."), AboutEntry("Journal embarqué", "Le journal de diagnostic est conservé 15 jours (20 000 lignes au plus) dans un fichier local ; le changelog de cette page est le CHANGELOG.md du dépôt, embarqué dans l'APK.") diff --git a/app/src/main/java/com/jttools/datacontrol/ui/family/FamilySection.kt b/app/src/main/java/com/jttools/datacontrol/ui/family/FamilySection.kt index df4f572..6ac6e5c 100644 --- a/app/src/main/java/com/jttools/datacontrol/ui/family/FamilySection.kt +++ b/app/src/main/java/com/jttools/datacontrol/ui/family/FamilySection.kt @@ -42,6 +42,10 @@ fun FamilySection(systemActions: SystemActions) { val vm: FamilyViewModel = hiltViewModel() val settings by vm.settings.collectAsStateWithLifecycle() val pairing by vm.pairing.collectAsStateWithLifecycle() + val gw by vm.gatewayState.collectAsStateWithLifecycle() + var firebaseText by remember { mutableStateOf("") } + var firebaseError by remember { mutableStateOf(null) } + var editFirebase by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() var adminActive by remember { mutableStateOf(systemActions.isAdminActive()) } @@ -83,9 +87,41 @@ fun FamilySection(systemActions: SystemActions) { ) HorizontalDivider() + // --- Configuration Firebase (saisie ici, sans recompiler) --- + Text(stringResource(R.string.family_firebase_title), fontWeight = FontWeight.Medium) + if (gw.enabled && !editFirebase) { + Text( + stringResource(R.string.family_firebase_ok, gw.projectId ?: ""), + color = StateOk, style = MaterialTheme.typography.bodyMedium + ) + OutlinedButton(onClick = { editFirebase = true }) { Text(stringResource(R.string.family_firebase_edit)) } + } else { + Text( + stringResource(R.string.family_firebase_help), + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant + ) + OutlinedTextField( + value = firebaseText, onValueChange = { firebaseText = it; firebaseError = null }, + label = { Text(stringResource(R.string.family_firebase_field)) }, + minLines = 3, maxLines = 6, modifier = Modifier.fillMaxWidth() + ) + firebaseError?.let { + Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) + } + Button( + enabled = firebaseText.isNotBlank(), + onClick = { + firebaseError = vm.saveFirebaseConfig(firebaseText) + if (firebaseError == null) { firebaseText = ""; editFirebase = false } + } + ) { Text(stringResource(R.string.family_firebase_save)) } + } + + HorizontalDivider() + // --- Appairage --- when { - !vm.remoteAvailable -> Text( + !gw.enabled -> Text( stringResource(R.string.family_unavailable), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/jttools/datacontrol/ui/family/FamilyViewModel.kt b/app/src/main/java/com/jttools/datacontrol/ui/family/FamilyViewModel.kt index 202998d..9d17547 100644 --- a/app/src/main/java/com/jttools/datacontrol/ui/family/FamilyViewModel.kt +++ b/app/src/main/java/com/jttools/datacontrol/ui/family/FamilyViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.jttools.datacontrol.data.AppSettings import com.jttools.datacontrol.data.SettingsRepository +import com.jttools.datacontrol.remote.FirebaseConfig import com.jttools.datacontrol.remote.RemoteSync import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -26,7 +27,17 @@ class FamilyViewModel @Inject constructor( viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings() ) - val remoteAvailable: Boolean get() = remoteSync.available + val gatewayState = remoteSync.gatewayState.stateIn( + viewModelScope, SharingStarted.WhileSubscribed(5_000), remoteSync.gatewayState.value + ) + + /** Enregistre la configuration Firebase collée ; renvoie un message d'erreur, ou null si elle est acceptée. */ + fun saveFirebaseConfig(text: String): String? { + val cfg = FirebaseConfig.parse(text) + ?: return "Configuration non reconnue : il faut au minimum apiKey, appId et projectId (collez le bloc « firebaseConfig » complet, ou la chaîne « fc1:… » de FamilyControl)." + viewModelScope.launch { settingsRepository.setFirebaseConfig(cfg) } + return null + } private val _pairing = MutableStateFlow(PairingUiState()) val pairing: StateFlow = _pairing.asStateFlow() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82ea5b5..6fa6262 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,7 +122,7 @@ Confirmer Contrôle parental (FamilyControl) - Liaison FamilyControl non configurée dans cette version (fichier firebase.properties absent à la compilation). + Renseignez d\'abord la configuration Firebase ci-dessus pour pouvoir associer l\'appareil. Appareil associé à FamilyControl : le parent peut gérer les accès réseau, voir la batterie, la position et l\'historique de trafic. Dissocier cet appareil Saisissez le code d\'appairage affiché dans FamilyControl pour associer cet appareil. @@ -145,4 +145,10 @@ Plages horaires, temps d\'écran et blocage d\'applications décidés par le parent. Nécessite le service d\'accessibilité « DataControl ». Android 13+ : si l\'activation est grisée, ouvrez Infos sur l\'application → ⋮ → « Autoriser les paramètres restreints », puis réessayez. Activer le service + Configuration Firebase + Configurée — projet %1$s + Modifier la configuration + Collez ici la chaîne « fc1:… » partagée par FamilyControl (bouton « Partager pour DataControl »), ou le bloc « firebaseConfig » de la console Firebase. + Configuration Firebase + Enregistrer diff --git a/version.properties b/version.properties index 8af8958..6c4d7a8 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -versionName=0.1.7 -versionCode=8 +versionName=0.1.8 +versionCode=9