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 showSystemApps: Boolean = false,
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)

View File

@@ -36,6 +36,9 @@ class SettingsRepository @Inject constructor(
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
val PASSWORD_HASH = stringPreferencesKey("password_hash")
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 ->
@@ -52,7 +55,10 @@ class SettingsRepository @Inject constructor(
whitelistedPackages = prefs[Keys.WHITELISTED_PACKAGES] ?: defaults.whitelistedPackages,
showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps,
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
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 inPulse = false
private var pulseStartMillis: Long = 0L
private var settingsLoaded = false
private val vpnRevokedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@@ -77,6 +78,15 @@ class DataMonitorService : LifecycleService() {
lifecycleScope.launch {
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
if (!it.controlEnabled) vpnRevoked = false
tick()
@@ -135,12 +145,17 @@ class DataMonitorService : LifecycleService() {
} else {
val now = System.currentTimeMillis()
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 (now - pulseStartMillis >= PULSE_DURATION_MS) {
inPulse = false
cutoffCycleAnchorMillis = now
persistCutoffState()
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages)
} else {
applyVpnMode(VpnMode.BLOCK_ALL, emptySet())
@@ -148,6 +163,7 @@ class DataMonitorService : LifecycleService() {
} else if (now - anchor >= intervalMs) {
inPulse = true
pulseStartMillis = now
persistCutoffState()
applyVpnMode(VpnMode.BLOCK_ALL, emptySet())
} else {
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackages)
@@ -180,8 +196,17 @@ class DataMonitorService : LifecycleService() {
}
private fun resetCutoffCycle() {
if (cutoffCycleAnchorMillis == null && !inPulse) return
cutoffCycleAnchorMillis = null
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) {

View File

@@ -20,11 +20,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.jttools.datacontrol.R
@Composable
@@ -34,6 +36,15 @@ fun AboutScreen() {
runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName }
.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(
modifier = Modifier
@@ -52,11 +63,13 @@ fun AboutScreen() {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Image(
painter = painterResource(R.mipmap.ic_launcher),
contentDescription = null,
modifier = Modifier.size(72.dp)
)
if (iconBitmap != null) {
Image(
bitmap = iconBitmap,
contentDescription = null,
modifier = Modifier.size(72.dp)
)
}
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,