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
f33776bbb5
14 files changed
+210
-53
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -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,37 +85,43 @@ 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 {
|
||||||
settingsRepository.settingsFlow
|
gateway.state.collectLatest { st ->
|
||||||
.map { it.familyId to it.deviceId }
|
if (!st.enabled) return@collectLatest
|
||||||
.distinctUntilChanged()
|
coroutineScope {
|
||||||
.flatMapLatest { (fid, did) ->
|
launch {
|
||||||
if (fid == null || did == null) flowOf(null) else configFlow(fid, did)
|
settingsRepository.settingsFlow
|
||||||
}
|
.map { it.familyId to it.deviceId }
|
||||||
.collect { cfg -> if (cfg != null) apply(cfg) }
|
.distinctUntilChanged()
|
||||||
}
|
.flatMapLatest { (fid, did) ->
|
||||||
scope.launch {
|
if (fid == null || did == null) flowOf(null) else configFlow(fid, did)
|
||||||
// Si le parent supprime l'appareil de la famille, on se dissocie localement (plus de règles,
|
}
|
||||||
// plus d'envois qui échoueraient en silence).
|
.collect { cfg -> if (cfg != null) apply(cfg) }
|
||||||
settingsRepository.settingsFlow
|
}
|
||||||
.map { it.familyId to it.deviceId }
|
launch {
|
||||||
.distinctUntilChanged()
|
// Si le parent supprime l'appareil de la famille, on se dissocie localement (plus de règles,
|
||||||
.flatMapLatest { (fid, did) -> if (fid == null || did == null) flowOf(null) else existenceFlow(fid, did) }
|
// plus d'envois qui échoueraient en silence).
|
||||||
.collect { exists ->
|
settingsRepository.settingsFlow
|
||||||
if (exists == false) {
|
.map { it.familyId to it.deviceId }
|
||||||
AppLog.w("FamilyControl", "Appareil supprimé par le parent : dissociation locale")
|
.distinctUntilChanged()
|
||||||
settingsRepository.setPairing(null, null)
|
.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("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()
|
||||||
|
|||||||
@@ -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
@@ -1,2 +1,2 @@
|
|||||||
versionName=0.1.7
|
versionName=0.1.8
|
||||||
versionCode=8
|
versionCode=9
|
||||||
Reference in new issue
Block a user