release: v0.1.8 — configuration Firebase saisie dans l'application

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tuxgyverandClaude Sonnet 5 committed 2026-09-21 13:39:37 +02:00
1 parent eecad06eea
commit f33776bbb5
14 files changed
+188 -31

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# Changelog # 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 ## 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. - 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.
+1 -1
View File
@@ -26,7 +26,7 @@
## Contrôle parental (avec FamilyControl) ## 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*. - **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…). - **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…).
@@ -9,6 +9,8 @@ import com.jttools.datacontrol.service.DataMonitorRestartWorker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@@ -17,11 +19,16 @@ import javax.inject.Inject
class DataControlApp : Application() { class DataControlApp : Application() {
@Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var firebaseGateway: com.jttools.datacontrol.remote.FirebaseGateway
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
com.jttools.datacontrol.log.AppLog.init(this) com.jttools.datacontrol.log.AppLog.init(this)
CoroutineScope(Dispatchers.IO).launch { settingsRepository.resetPasswordOnce() } 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. // 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. // enqueueUniquePeriodicWork + KEEP : sans effet si déjà planifié, donc sûr à appeler à chaque démarrage.
val request = PeriodicWorkRequestBuilder<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build() val request = PeriodicWorkRequestBuilder<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build()
@@ -29,7 +29,9 @@ data class AppSettings(
/** Horodatage (ms) de la dernière configuration distante appliquée. */ /** Horodatage (ms) de la dernière configuration distante appliquée. */
val remoteConfigVersion: Long = 0L, val remoteConfigVersion: Long = 0L,
/** Règles d'usage (plages horaires, limites de temps, applications interdites), au format JSON. */ /** 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() val isPaired: Boolean get() = !familyId.isNullOrEmpty() && !deviceId.isNullOrEmpty()
@@ -46,6 +46,9 @@ class SettingsRepository @Inject constructor(
val DEVICE_ID = stringPreferencesKey("device_id") val DEVICE_ID = stringPreferencesKey("device_id")
val REMOTE_CONFIG_VERSION = longPreferencesKey("remote_config_version") val REMOTE_CONFIG_VERSION = longPreferencesKey("remote_config_version")
val RULES_JSON = stringPreferencesKey("rules_json") 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_DAY = stringPreferencesKey("usage_day")
val USAGE_JSON = stringPreferencesKey("usage_json") val USAGE_JSON = stringPreferencesKey("usage_json")
val TRAFFIC_DAY = stringPreferencesKey("traffic_day") val TRAFFIC_DAY = stringPreferencesKey("traffic_day")
@@ -78,7 +81,12 @@ class SettingsRepository @Inject constructor(
familyId = prefs[Keys.FAMILY_ID], familyId = prefs[Keys.FAMILY_ID],
deviceId = prefs[Keys.DEVICE_ID], deviceId = prefs[Keys.DEVICE_ID],
remoteConfigVersion = prefs[Keys.REMOTE_CONFIG_VERSION] ?: defaults.remoteConfigVersion, 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 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
}
}
}
} }
@@ -53,7 +53,7 @@ class DeviceReporter @Inject constructor(
private var lastUsageSentTotal = -1L private var lastUsageSentTotal = -1L
fun start(scope: CoroutineScope) { fun start(scope: CoroutineScope) {
if (started || !remote.available) return if (started) return
started = true started = true
remote.start(scope) remote.start(scope)
@@ -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())
}
}
@@ -6,37 +6,65 @@ import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.FirebaseFirestore
import com.jttools.datacontrol.BuildConfig import com.jttools.datacontrol.BuildConfig
import com.jttools.datacontrol.log.AppLog
import dagger.hilt.android.qualifiers.ApplicationContext 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 kotlinx.coroutines.tasks.await
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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` * Accès à Firebase (Auth anonyme + Firestore). La configuration se saisit dans l'application
* (voir build.gradle.kts), sans plugin google-services : si la config est absente, * (Réglages → Contrôle parental) et peut changer à tout moment ; à défaut, on retombe sur
* [enabled] est faux et toutes les fonctions distantes restent inertes. * `firebase.properties` (compilation). Sans configuration, [enabled] est faux et tout reste inerte.
*/ */
@Singleton @Singleton
class FirebaseGateway @Inject constructor( class FirebaseGateway @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) { ) {
val enabled: Boolean = BuildConfig.FIREBASE_API_KEY.isNotBlank() && private val _state = MutableStateFlow(GatewayState())
BuildConfig.FIREBASE_APP_ID.isNotBlank() && val state: StateFlow<GatewayState> = _state.asStateFlow()
BuildConfig.FIREBASE_PROJECT_ID.isNotBlank() val enabled: Boolean get() = _state.value.enabled
private val app: FirebaseApp? by lazy { @Volatile private var app: FirebaseApp? = null
if (!enabled) return@lazy null private var currentKey: String? = null
runCatching {
FirebaseApp.getApps(context).firstOrNull() private fun buildConfigFallback(): FirebaseConfig? =
?: FirebaseApp.initializeApp( 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, context,
FirebaseOptions.Builder() FirebaseOptions.Builder()
.setApiKey(BuildConfig.FIREBASE_API_KEY) .setApiKey(cfg.apiKey)
.setApplicationId(BuildConfig.FIREBASE_APP_ID) .setApplicationId(cfg.appId)
.setProjectId(BuildConfig.FIREBASE_PROJECT_ID) .setProjectId(cfg.projectId)
.build() .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) } val db: FirebaseFirestore? get() = app?.let { FirebaseFirestore.getInstance(it) }
@@ -46,6 +74,8 @@ class FirebaseGateway @Inject constructor(
suspend fun ensureSignedIn(): String? { suspend fun ensureSignedIn(): String? {
val a = auth ?: return null val a = auth ?: return null
a.currentUser?.let { return it.uid } 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()
} }
} }
@@ -10,6 +10,8 @@ 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 com.jttools.datacontrol.log.AppLog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@@ -44,6 +46,7 @@ class RemoteSync @Inject constructor(
private val installedApps: InstalledAppsRepository private val installedApps: InstalledAppsRepository
) { ) {
val available: Boolean get() = gateway.enabled val available: Boolean get() = gateway.enabled
val gatewayState = gateway.state
private fun deviceDoc(s: AppSettings): DocumentReference? { private fun deviceDoc(s: AppSettings): DocumentReference? {
val db = gateway.db ?: return null val db = gateway.db ?: return null
@@ -82,11 +85,14 @@ class RemoteSync @Inject constructor(
settingsRepository.setPairing(null, null) 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) @OptIn(ExperimentalCoroutinesApi::class)
fun start(scope: CoroutineScope) { fun start(scope: CoroutineScope) {
if (!gateway.enabled) return
scope.launch { scope.launch {
gateway.state.collectLatest { st ->
if (!st.enabled) return@collectLatest
coroutineScope {
launch {
settingsRepository.settingsFlow settingsRepository.settingsFlow
.map { it.familyId to it.deviceId } .map { it.familyId to it.deviceId }
.distinctUntilChanged() .distinctUntilChanged()
@@ -95,7 +101,7 @@ class RemoteSync @Inject constructor(
} }
.collect { cfg -> if (cfg != null) apply(cfg) } .collect { cfg -> if (cfg != null) apply(cfg) }
} }
scope.launch { launch {
// Si le parent supprime l'appareil de la famille, on se dissocie localement (plus de règles, // Si le parent supprime l'appareil de la famille, on se dissocie localement (plus de règles,
// plus d'envois qui échoueraient en silence). // plus d'envois qui échoueraient en silence).
settingsRepository.settingsFlow settingsRepository.settingsFlow
@@ -109,13 +115,16 @@ class RemoteSync @Inject constructor(
} }
} }
} }
scope.launch { 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 ->
if (paired) pushApps() if (paired) pushApps()
} }
} }
} }
}
}
}
/** Vrai/faux selon que le document de l'appareil existe côté serveur (ignore le cache local). */ /** 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?> { private fun existenceFlow(fid: String, did: String) = callbackFlow<Boolean?> {
@@ -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("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("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("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("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("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("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("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("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.") 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.")
@@ -42,6 +42,10 @@ fun FamilySection(systemActions: SystemActions) {
val vm: FamilyViewModel = hiltViewModel() val vm: FamilyViewModel = hiltViewModel()
val settings by vm.settings.collectAsStateWithLifecycle() val settings by vm.settings.collectAsStateWithLifecycle()
val pairing by vm.pairing.collectAsStateWithLifecycle() val pairing by vm.pairing.collectAsStateWithLifecycle()
val gw by vm.gatewayState.collectAsStateWithLifecycle()
var firebaseText by remember { mutableStateOf("") }
var firebaseError by remember { mutableStateOf<String?>(null) }
var editFirebase by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var adminActive by remember { mutableStateOf(systemActions.isAdminActive()) } var adminActive by remember { mutableStateOf(systemActions.isAdminActive()) }
@@ -83,9 +87,41 @@ fun FamilySection(systemActions: SystemActions) {
) )
HorizontalDivider() 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 --- // --- Appairage ---
when { when {
!vm.remoteAvailable -> Text( !gw.enabled -> Text(
stringResource(R.string.family_unavailable), stringResource(R.string.family_unavailable),
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.jttools.datacontrol.data.AppSettings import com.jttools.datacontrol.data.AppSettings
import com.jttools.datacontrol.data.SettingsRepository import com.jttools.datacontrol.data.SettingsRepository
import com.jttools.datacontrol.remote.FirebaseConfig
import com.jttools.datacontrol.remote.RemoteSync import com.jttools.datacontrol.remote.RemoteSync
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,7 +27,17 @@ class FamilyViewModel @Inject constructor(
viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings() 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()) private val _pairing = MutableStateFlow(PairingUiState())
val pairing: StateFlow<PairingUiState> = _pairing.asStateFlow() val pairing: StateFlow<PairingUiState> = _pairing.asStateFlow()
+7 -1
View File
@@ -122,7 +122,7 @@
<string name="common_confirm">Confirmer</string> <string name="common_confirm">Confirmer</string>
<string name="family_title">Contrôle parental (FamilyControl)</string> <string name="family_title">Contrôle parental (FamilyControl)</string>
<string name="family_unavailable">Liaison FamilyControl non configurée dans cette version (fichier firebase.properties absent à la compilation).</string> <string name="family_unavailable">Renseignez d\'abord la configuration Firebase ci-dessus pour pouvoir associer l\'appareil.</string>
<string name="family_paired">Appareil associé à FamilyControl : le parent peut gérer les accès réseau, voir la batterie, la position et l\'historique de trafic.</string> <string name="family_paired">Appareil associé à FamilyControl : le parent peut gérer les accès réseau, voir la batterie, la position et l\'historique de trafic.</string>
<string name="family_unpair">Dissocier cet appareil</string> <string name="family_unpair">Dissocier cet appareil</string>
<string name="family_pair_help">Saisissez le code d\'appairage affiché dans FamilyControl pour associer cet appareil.</string> <string name="family_pair_help">Saisissez le code d\'appairage affiché dans FamilyControl pour associer cet appareil.</string>
@@ -145,4 +145,10 @@
<string name="family_guard_desc">Plages horaires, temps d\'écran et blocage d\'applications décidés par le parent. Nécessite le service d\'accessibilité « DataControl ».</string> <string name="family_guard_desc">Plages horaires, temps d\'écran et blocage d\'applications décidés par le parent. Nécessite le service d\'accessibilité « DataControl ».</string>
<string name="family_guard_hint">Android 13+ : si l\'activation est grisée, ouvrez Infos sur l\'application → ⋮ → « Autoriser les paramètres restreints », puis réessayez.</string> <string name="family_guard_hint">Android 13+ : si l\'activation est grisée, ouvrez Infos sur l\'application → ⋮ → « Autoriser les paramètres restreints », puis réessayez.</string>
<string name="family_guard_enable">Activer le service</string> <string name="family_guard_enable">Activer le service</string>
<string name="family_firebase_title">Configuration Firebase</string>
<string name="family_firebase_ok">Configurée — projet %1$s</string>
<string name="family_firebase_edit">Modifier la configuration</string>
<string name="family_firebase_help">Collez ici la chaîne « fc1:… » partagée par FamilyControl (bouton « Partager pour DataControl »), ou le bloc « firebaseConfig » de la console Firebase.</string>
<string name="family_firebase_field">Configuration Firebase</string>
<string name="family_firebase_save">Enregistrer</string>
</resources> </resources>
+2 -2
View File
@@ -1,2 +1,2 @@
versionName=0.1.7 versionName=0.1.8
versionCode=8 versionCode=9