release: v0.1.3 — correctif VPN, alerte pare-feu inactif, contrôle parental (FamilyControl), anti-désinstallation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c63cc45c57
commit
82d47fcd72
25 files changed
+996
-16
No files matched your search
@@ -7,7 +7,7 @@ import java.time.LocalDate
|
||||
*/
|
||||
data class AppSettings(
|
||||
val controlEnabled: Boolean = false,
|
||||
val planLimitMo: Long = 5000L,
|
||||
val planLimitMo: Long = 1000L,
|
||||
val planPriceEuro: Double = 19.99,
|
||||
val overagePricePerMoEuro: Double = 0.01,
|
||||
val planStartDateEpochDay: Long = LocalDate.now().toEpochDay(),
|
||||
@@ -22,8 +22,15 @@ data class AppSettings(
|
||||
val passwordSalt: String? = null,
|
||||
val cutoffCycleAnchorMillis: Long? = null,
|
||||
val cutoffInPulse: Boolean = false,
|
||||
val cutoffPulseStartMillis: Long = 0L
|
||||
val cutoffPulseStartMillis: Long = 0L,
|
||||
/** Appairage avec FamilyControl (null tant que l'appareil n'est pas associé). */
|
||||
val familyId: String? = null,
|
||||
val deviceId: String? = null,
|
||||
/** Horodatage (ms) de la dernière configuration distante appliquée. */
|
||||
val remoteConfigVersion: Long = 0L
|
||||
) {
|
||||
val isPaired: Boolean get() = !familyId.isNullOrEmpty() && !deviceId.isNullOrEmpty()
|
||||
|
||||
val planStartDate: LocalDate get() = LocalDate.ofEpochDay(planStartDateEpochDay)
|
||||
|
||||
/** Un mot de passe de verrouillage protège la désactivation du contrôle et l'accès aux réglages. */
|
||||
@@ -59,6 +66,9 @@ data class UsageState(
|
||||
get() = if (planLimitMo <= 0) 0f else (consumedMo.toFloat() / planLimitMo.toFloat()).coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
/** Trafic cumulé d'une journée, pour l'historique envoyé à FamilyControl. */
|
||||
data class TrafficDay(val date: String, val mobileBytes: Long, val wifiBytes: Long)
|
||||
|
||||
enum class DataState {
|
||||
OK, // sous le seuil d'alerte, liste blanche active, pas de coupure en cours
|
||||
WARNING, // proche ou au-delà du forfait (>= 80%), liste blanche active, pas de coupure en cours
|
||||
|
||||
@@ -14,7 +14,7 @@ class InstalledAppsRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun listInstalledApps(): List<AppInfo> = withContext(Dispatchers.Default) {
|
||||
suspend fun listInstalledApps(withIcons: Boolean = true): List<AppInfo> = withContext(Dispatchers.Default) {
|
||||
val pm = context.packageManager
|
||||
val ownPackage = context.packageName
|
||||
pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
@@ -24,7 +24,7 @@ class InstalledAppsRepository @Inject constructor(
|
||||
AppInfo(
|
||||
packageName = appInfo.packageName,
|
||||
label = pm.getApplicationLabel(appInfo).toString(),
|
||||
icon = runCatching { pm.getApplicationIcon(appInfo) }.getOrNull(),
|
||||
icon = if (withIcons) runCatching { pm.getApplicationIcon(appInfo) }.getOrNull() else null,
|
||||
isSystemApp = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@ class SettingsRepository @Inject constructor(
|
||||
val CUTOFF_ANCHOR_MILLIS = longPreferencesKey("cutoff_anchor_millis")
|
||||
val CUTOFF_IN_PULSE = booleanPreferencesKey("cutoff_in_pulse")
|
||||
val CUTOFF_PULSE_START_MILLIS = longPreferencesKey("cutoff_pulse_start_millis")
|
||||
val FAMILY_ID = stringPreferencesKey("family_id")
|
||||
val DEVICE_ID = stringPreferencesKey("device_id")
|
||||
val REMOTE_CONFIG_VERSION = longPreferencesKey("remote_config_version")
|
||||
val TRAFFIC_DAY = stringPreferencesKey("traffic_day")
|
||||
val TRAFFIC_MOBILE_ACC = longPreferencesKey("traffic_mobile_acc")
|
||||
val TRAFFIC_WIFI_ACC = longPreferencesKey("traffic_wifi_acc")
|
||||
val TRAFFIC_LAST_MOBILE = longPreferencesKey("traffic_last_mobile")
|
||||
val TRAFFIC_LAST_OTHER = longPreferencesKey("traffic_last_other")
|
||||
}
|
||||
|
||||
val settingsFlow: Flow<AppSettings> = context.dataStore.data.map { prefs ->
|
||||
@@ -63,7 +71,10 @@ class SettingsRepository @Inject constructor(
|
||||
passwordSalt = prefs[Keys.PASSWORD_SALT],
|
||||
cutoffCycleAnchorMillis = prefs[Keys.CUTOFF_ANCHOR_MILLIS],
|
||||
cutoffInPulse = prefs[Keys.CUTOFF_IN_PULSE] ?: defaults.cutoffInPulse,
|
||||
cutoffPulseStartMillis = prefs[Keys.CUTOFF_PULSE_START_MILLIS] ?: defaults.cutoffPulseStartMillis
|
||||
cutoffPulseStartMillis = prefs[Keys.CUTOFF_PULSE_START_MILLIS] ?: defaults.cutoffPulseStartMillis,
|
||||
familyId = prefs[Keys.FAMILY_ID],
|
||||
deviceId = prefs[Keys.DEVICE_ID],
|
||||
remoteConfigVersion = prefs[Keys.REMOTE_CONFIG_VERSION] ?: defaults.remoteConfigVersion
|
||||
)
|
||||
}
|
||||
|
||||
@@ -187,4 +198,71 @@ class SettingsRepository @Inject constructor(
|
||||
prefs[Keys.CUTOFF_PULSE_START_MILLIS] = pulseStartMillis
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPairing(familyId: String?, deviceId: String?) {
|
||||
context.dataStore.edit {
|
||||
if (familyId == null || deviceId == null) {
|
||||
it.remove(Keys.FAMILY_ID)
|
||||
it.remove(Keys.DEVICE_ID)
|
||||
it.remove(Keys.REMOTE_CONFIG_VERSION)
|
||||
} else {
|
||||
it[Keys.FAMILY_ID] = familyId
|
||||
it[Keys.DEVICE_ID] = deviceId
|
||||
it[Keys.REMOTE_CONFIG_VERSION] = 0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique une configuration envoyée par FamilyControl. Chaque champ null est ignoré.
|
||||
* [version] (horodatage de l'écriture côté parent) évite de ré-appliquer une ancienne
|
||||
* configuration au redémarrage et d'écraser un réglage local plus récent.
|
||||
*/
|
||||
suspend fun applyRemoteConfig(
|
||||
version: Long,
|
||||
controlEnabled: Boolean?,
|
||||
whitelistedPackages: Set<String>?,
|
||||
whitelistedPackagesWifi: Set<String>?,
|
||||
wifiFilterEnabled: Boolean?,
|
||||
planLimitMo: Long?,
|
||||
cutoffIntervalMinutes: Int?
|
||||
) {
|
||||
context.dataStore.edit { prefs ->
|
||||
if ((prefs[Keys.REMOTE_CONFIG_VERSION] ?: 0L) >= version) return@edit
|
||||
prefs[Keys.REMOTE_CONFIG_VERSION] = version
|
||||
controlEnabled?.let { prefs[Keys.CONTROL_ENABLED] = it }
|
||||
whitelistedPackages?.let { prefs[Keys.WHITELISTED_PACKAGES] = it }
|
||||
whitelistedPackagesWifi?.let { prefs[Keys.WHITELISTED_PACKAGES_WIFI] = it }
|
||||
wifiFilterEnabled?.let { prefs[Keys.WIFI_FILTER_ENABLED] = it }
|
||||
planLimitMo?.takeIf { it > 0 }?.let { prefs[Keys.PLAN_LIMIT_MO] = it }
|
||||
cutoffIntervalMinutes?.takeIf { it > 0 }?.let { prefs[Keys.CUTOFF_INTERVAL_MINUTES] = it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cumule le trafic de la journée à partir des compteurs système (qui repartent de zéro au
|
||||
* redémarrage : un compteur inférieur au précédent est traité comme un redémarrage).
|
||||
* [otherBytes] = trafic non mobile (Wi-Fi, etc.) = total - mobile.
|
||||
*/
|
||||
suspend fun accumulateTraffic(today: String, mobileBytes: Long, otherBytes: Long): TrafficDay {
|
||||
var result = TrafficDay(today, 0L, 0L)
|
||||
context.dataStore.edit { prefs ->
|
||||
val sameDay = prefs[Keys.TRAFFIC_DAY] == today
|
||||
var mobileAcc = if (sameDay) prefs[Keys.TRAFFIC_MOBILE_ACC] ?: 0L else 0L
|
||||
var wifiAcc = if (sameDay) prefs[Keys.TRAFFIC_WIFI_ACC] ?: 0L else 0L
|
||||
val lastMobile = prefs[Keys.TRAFFIC_LAST_MOBILE]
|
||||
val lastOther = prefs[Keys.TRAFFIC_LAST_OTHER]
|
||||
if (lastMobile != null && lastOther != null) {
|
||||
mobileAcc += if (mobileBytes >= lastMobile) mobileBytes - lastMobile else mobileBytes
|
||||
wifiAcc += if (otherBytes >= lastOther) otherBytes - lastOther else otherBytes
|
||||
}
|
||||
prefs[Keys.TRAFFIC_DAY] = today
|
||||
prefs[Keys.TRAFFIC_MOBILE_ACC] = mobileAcc
|
||||
prefs[Keys.TRAFFIC_WIFI_ACC] = wifiAcc
|
||||
prefs[Keys.TRAFFIC_LAST_MOBILE] = mobileBytes
|
||||
prefs[Keys.TRAFFIC_LAST_OTHER] = otherBytes
|
||||
result = TrafficDay(today, mobileAcc, wifiAcc)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user