update + Fix
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user