update + Fix
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
) {
|
) {
|
||||||
|
if (iconBitmap != null) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.ic_launcher),
|
bitmap = iconBitmap,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(72.dp)
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user