release: v0.1.8 — configuration Firebase saisie dans l'application
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eecad06eea
commit
06e590ee01
14 files changed
+210
-53
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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…).
|
||||
|
||||
@@ -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<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.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<GatewayState> = _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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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<String?>(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
|
||||
)
|
||||
|
||||
@@ -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<PairingUiState> = _pairing.asStateFlow()
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<string name="common_confirm">Confirmer</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_unpair">Dissocier 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_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_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>
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
versionName=0.1.7
|
||||
versionCode=8
|
||||
versionName=0.1.8
|
||||
versionCode=9
|
||||
Reference in new issue
Block a user