update + Fix

This commit is contained in:
Johnny
2026-07-03 07:56:33 +02:00
parent a2f0e8124c
commit 96e5bacc5a
5 changed files with 69 additions and 9 deletions

Binary file not shown.

View File

@@ -17,7 +17,10 @@ data class AppSettings(
val whitelistedPackages: Set<String> = emptySet(), val whitelistedPackages: Set<String> = emptySet(),
val showSystemApps: Boolean = false, val showSystemApps: Boolean = false,
val passwordHash: String? = null, val passwordHash: String? = null,
val passwordSalt: String? = null val passwordSalt: String? = null,
val cutoffCycleAnchorMillis: Long? = null,
val cutoffInPulse: Boolean = false,
val cutoffPulseStartMillis: Long = 0L
) { ) {
val planStartDate: LocalDate get() = LocalDate.ofEpochDay(planStartDateEpochDay) val planStartDate: LocalDate get() = LocalDate.ofEpochDay(planStartDateEpochDay)

View File

@@ -36,6 +36,9 @@ class SettingsRepository @Inject constructor(
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps") val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
val PASSWORD_HASH = stringPreferencesKey("password_hash") val PASSWORD_HASH = stringPreferencesKey("password_hash")
val PASSWORD_SALT = stringPreferencesKey("password_salt") val PASSWORD_SALT = stringPreferencesKey("password_salt")
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 settingsFlow: Flow<AppSettings> = context.dataStore.data.map { prefs -> val settingsFlow: Flow<AppSettings> = context.dataStore.data.map { prefs ->
@@ -52,7 +55,10 @@ class SettingsRepository @Inject constructor(
whitelistedPackages = prefs[Keys.WHITELISTED_PACKAGES] ?: defaults.whitelistedPackages, whitelistedPackages = prefs[Keys.WHITELISTED_PACKAGES] ?: defaults.whitelistedPackages,
showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps, showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps,
passwordHash = prefs[Keys.PASSWORD_HASH], passwordHash = prefs[Keys.PASSWORD_HASH],
passwordSalt = prefs[Keys.PASSWORD_SALT] 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
) )
} }
@@ -126,4 +132,17 @@ class SettingsRepository @Inject constructor(
val salt = prefs[Keys.PASSWORD_SALT] ?: return true val salt = prefs[Keys.PASSWORD_SALT] ?: return true
return PasswordHasher.hash(password, salt) == hash return PasswordHasher.hash(password, salt) == hash
} }
/**
* Persiste l'état du cycle de coupure automatique, pour que le minuteur ne reparte pas de
* zéro si [DataMonitorService] est tué puis relancé par le système avant la fin de l'intervalle
* (fréquent sur les ROM qui tuent agressivement les apps en arrière-plan sans exemption batterie).
*/
suspend fun setCutoffCycleState(anchorMillis: Long?, inPulse: Boolean, pulseStartMillis: Long) {
context.dataStore.edit { prefs ->
if (anchorMillis == null) prefs.remove(Keys.CUTOFF_ANCHOR_MILLIS) else prefs[Keys.CUTOFF_ANCHOR_MILLIS] = anchorMillis
prefs[Keys.CUTOFF_IN_PULSE] = inPulse
prefs[Keys.CUTOFF_PULSE_START_MILLIS] = pulseStartMillis
}
}
} }

View File

@@ -57,6 +57,7 @@ class DataMonitorService : LifecycleService() {
private var cutoffCycleAnchorMillis: Long? = null private var cutoffCycleAnchorMillis: Long? = null
private var inPulse = false private var inPulse = false
private var pulseStartMillis: Long = 0L private var pulseStartMillis: Long = 0L
private var settingsLoaded = false
private val vpnRevokedReceiver = object : BroadcastReceiver() { private val vpnRevokedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
@@ -77,6 +78,15 @@ class DataMonitorService : LifecycleService() {
lifecycleScope.launch { lifecycleScope.launch {
settingsRepository.settingsFlow.collect { settingsRepository.settingsFlow.collect {
// Ne recopier l'état persisté du minuteur de coupure qu'à la toute première valeur
// réelle lue en base — pas question d'écraser avec les valeurs par défaut si un
// tick() déclenché par une autre coroutine s'exécute avant cette première émission.
if (!settingsLoaded) {
cutoffCycleAnchorMillis = it.cutoffCycleAnchorMillis
inPulse = it.cutoffInPulse
pulseStartMillis = it.cutoffPulseStartMillis
settingsLoaded = true
}
latestSettings = it latestSettings = it
if (!it.controlEnabled) vpnRevoked = false if (!it.controlEnabled) vpnRevoked = false
tick() tick()
@@ -135,12 +145,17 @@ class DataMonitorService : LifecycleService() {
} else { } else {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val intervalMs = settings.cutoffIntervalMinutes * 60_000L val intervalMs = settings.cutoffIntervalMinutes * 60_000L
val anchor = cutoffCycleAnchorMillis ?: now.also { cutoffCycleAnchorMillis = it } if (cutoffCycleAnchorMillis == null) {
cutoffCycleAnchorMillis = now
persistCutoffState()
}
val anchor = cutoffCycleAnchorMillis!!
if (inPulse) { if (inPulse) {
if (now - pulseStartMillis >= PULSE_DURATION_MS) { if (now - pulseStartMillis >= PULSE_DURATION_MS) {
inPulse = false inPulse = false
cutoffCycleAnchorMillis = now cutoffCycleAnchorMillis = now
persistCutoffState()
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages)
} else { } else {
applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) applyVpnMode(VpnMode.BLOCK_ALL, emptySet())
@@ -148,6 +163,7 @@ class DataMonitorService : LifecycleService() {
} else if (now - anchor >= intervalMs) { } else if (now - anchor >= intervalMs) {
inPulse = true inPulse = true
pulseStartMillis = now pulseStartMillis = now
persistCutoffState()
applyVpnMode(VpnMode.BLOCK_ALL, emptySet()) applyVpnMode(VpnMode.BLOCK_ALL, emptySet())
} else { } else {
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages) applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages)
@@ -180,8 +196,17 @@ class DataMonitorService : LifecycleService() {
} }
private fun resetCutoffCycle() { private fun resetCutoffCycle() {
if (cutoffCycleAnchorMillis == null && !inPulse) return
cutoffCycleAnchorMillis = null cutoffCycleAnchorMillis = null
inPulse = false inPulse = false
persistCutoffState()
}
private fun persistCutoffState() {
val anchor = cutoffCycleAnchorMillis
val pulse = inPulse
val pulseStart = pulseStartMillis
lifecycleScope.launch { settingsRepository.setCutoffCycleState(anchor, pulse, pulseStart) }
} }
private fun maybeAutoReset(settings: AppSettings) { private fun maybeAutoReset(settings: AppSettings) {

View File

@@ -20,11 +20,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.jttools.datacontrol.R import com.jttools.datacontrol.R
@Composable @Composable
@@ -34,6 +36,15 @@ fun AboutScreen() {
runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName } runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName }
.getOrNull() ?: "?" .getOrNull() ?: "?"
} }
// R.mipmap.ic_launcher est une <adaptive-icon> (avant-plan/arrière-plan séparés) : painterResource()
// ne sait pas la charger directement (IllegalArgumentException à l'exécution), il faut la rasteriser.
val iconBitmap = remember {
runCatching {
ContextCompat.getDrawable(context, R.mipmap.ic_launcher)
?.toBitmap(width = 144, height = 144)
?.asImageBitmap()
}.getOrNull()
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -52,11 +63,13 @@ fun AboutScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Image( if (iconBitmap != null) {
painter = painterResource(R.mipmap.ic_launcher), Image(
contentDescription = null, bitmap = iconBitmap,
modifier = Modifier.size(72.dp) contentDescription = null,
) modifier = Modifier.size(72.dp)
)
}
Text( Text(
stringResource(R.string.app_name), stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,