update + Fix

This commit is contained in:
Johnny
2026-07-03 07:19:17 +02:00
parent 68c63958f4
commit a2f0e8124c
14 changed files with 475 additions and 4 deletions

Binary file not shown.

View File

@@ -15,10 +15,15 @@ data class AppSettings(
val cutoffIntervalMinutes: Int = 15,
val baselineMobileBytesAtReset: Long = 0L,
val whitelistedPackages: Set<String> = emptySet(),
val showSystemApps: Boolean = false
val showSystemApps: Boolean = false,
val passwordHash: String? = null,
val passwordSalt: String? = null
) {
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. */
val hasPassword: Boolean get() = !passwordHash.isNullOrEmpty()
/** Prochaine date de réinitialisation automatique (anniversaire mensuel du début de forfait). */
fun nextAutoResetDate(): LocalDate {
var next = planStartDate.plusMonths(1)

View File

@@ -0,0 +1,23 @@
package com.jttools.datacontrol.data
import java.security.MessageDigest
import java.security.SecureRandom
/** Hachage salé (SHA-256) du mot de passe de verrouillage — jamais stocké en clair. */
object PasswordHasher {
private const val SALT_BYTES = 16
fun newSalt(): String {
val bytes = ByteArray(SALT_BYTES)
SecureRandom().nextBytes(bytes)
return bytes.toHex()
}
fun hash(password: String, saltHex: String): String {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(saltHex.toByteArray(Charsets.UTF_8))
return digest.digest(password.toByteArray(Charsets.UTF_8)).toHex()
}
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
}

View File

@@ -6,10 +6,12 @@ import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.time.LocalDate
import javax.inject.Inject
@@ -32,6 +34,8 @@ class SettingsRepository @Inject constructor(
val BASELINE_MOBILE_BYTES = longPreferencesKey("baseline_mobile_bytes")
val WHITELISTED_PACKAGES = stringSetPreferencesKey("whitelisted_packages")
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
val PASSWORD_HASH = stringPreferencesKey("password_hash")
val PASSWORD_SALT = stringPreferencesKey("password_salt")
}
val settingsFlow: Flow<AppSettings> = context.dataStore.data.map { prefs ->
@@ -46,7 +50,9 @@ class SettingsRepository @Inject constructor(
cutoffIntervalMinutes = prefs[Keys.CUTOFF_INTERVAL_MINUTES] ?: defaults.cutoffIntervalMinutes,
baselineMobileBytesAtReset = prefs[Keys.BASELINE_MOBILE_BYTES] ?: defaults.baselineMobileBytesAtReset,
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],
passwordSalt = prefs[Keys.PASSWORD_SALT]
)
}
@@ -95,4 +101,29 @@ class SettingsRepository @Inject constructor(
suspend fun rebaseline(currentMobileBytes: Long) {
context.dataStore.edit { it[Keys.BASELINE_MOBILE_BYTES] = currentMobileBytes }
}
/** Définit (ou supprime, si [password] est vide/null) le mot de passe de verrouillage. */
suspend fun setPassword(password: String?) {
if (password.isNullOrBlank()) {
context.dataStore.edit {
it.remove(Keys.PASSWORD_HASH)
it.remove(Keys.PASSWORD_SALT)
}
} else {
val salt = PasswordHasher.newSalt()
val hash = PasswordHasher.hash(password, salt)
context.dataStore.edit { prefs ->
prefs[Keys.PASSWORD_HASH] = hash
prefs[Keys.PASSWORD_SALT] = salt
}
}
}
/** Vrai si aucun mot de passe n'est défini, ou si [password] correspond au mot de passe stocké. */
suspend fun verifyPassword(password: String): Boolean {
val prefs = context.dataStore.data.first()
val hash = prefs[Keys.PASSWORD_HASH] ?: return true
val salt = prefs[Keys.PASSWORD_SALT] ?: return true
return PasswordHasher.hash(password, salt) == hash
}
}

View File

@@ -0,0 +1,105 @@
package com.jttools.datacontrol.ui.about
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 com.jttools.datacontrol.R
@Composable
fun AboutScreen() {
val context = LocalContext.current
val versionName = remember {
runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName }
.getOrNull() ?: "?"
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(stringResource(R.string.about_title), style = MaterialTheme.typography.headlineMedium)
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Column(
Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Image(
painter = painterResource(R.mipmap.ic_launcher),
contentDescription = null,
modifier = Modifier.size(72.dp)
)
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
stringResource(R.string.about_version, versionName),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(stringResource(R.string.about_section_description), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
HorizontalDivider()
Text(stringResource(R.string.about_description), style = MaterialTheme.typography.bodyMedium)
}
}
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(stringResource(R.string.about_section_how), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
HorizontalDivider()
Text(stringResource(R.string.about_how_it_works), style = MaterialTheme.typography.bodyMedium)
}
}
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Row(
Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.about_author), style = MaterialTheme.typography.bodyMedium)
Text(
stringResource(R.string.about_author_value),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
}

View File

@@ -0,0 +1,141 @@
package com.jttools.datacontrol.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.jttools.datacontrol.R
import kotlinx.coroutines.launch
/**
* Verrouille [content] derrière une saisie de mot de passe tant que [passwordSet] est vrai.
* Le déverrouillage n'est valable que pour la composition courante (redemandé à chaque visite de l'écran).
*/
@Composable
fun PasswordGate(
passwordSet: Boolean,
verify: suspend (String) -> Boolean,
content: @Composable () -> Unit
) {
var unlocked by remember(passwordSet) { mutableStateOf(!passwordSet) }
if (unlocked) {
content()
return
}
var password by remember { mutableStateOf("") }
var error by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(Icons.Filled.Lock, contentDescription = null, modifier = Modifier.size(48.dp))
Spacer(Modifier.height(16.dp))
Text(stringResource(R.string.password_gate_title), style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it; error = false },
label = { Text(stringResource(R.string.password_field_label)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
isError = error,
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
if (error) {
Spacer(Modifier.height(4.dp))
Text(
stringResource(R.string.password_incorrect),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
Spacer(Modifier.height(16.dp))
Button(
onClick = {
scope.launch {
if (verify(password)) {
unlocked = true
} else {
error = true
}
}
},
modifier = Modifier.fillMaxWidth()
) { Text(stringResource(R.string.password_unlock)) }
}
}
/** Boîte de dialogue de saisie de mot de passe pour confirmer une action ponctuelle (ex : désactivation). */
@Composable
fun PasswordPromptDialog(
title: String,
error: Boolean,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
var password by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column {
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text(stringResource(R.string.password_field_label)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
isError = error,
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
if (error) {
Spacer(Modifier.height(4.dp))
Text(
stringResource(R.string.password_incorrect),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
},
confirmButton = {
Button(onClick = { onConfirm(password) }) { Text(stringResource(R.string.common_confirm)) }
},
dismissButton = {
OutlinedButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) }
}
)
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -40,12 +41,14 @@ import com.jttools.datacontrol.R
import com.jttools.datacontrol.data.DataState
import com.jttools.datacontrol.service.DataMonitorService
import com.jttools.datacontrol.ui.SystemActions
import com.jttools.datacontrol.ui.components.PasswordPromptDialog
import com.jttools.datacontrol.ui.theme.StateCut
import com.jttools.datacontrol.ui.theme.StateOk
import com.jttools.datacontrol.ui.theme.StateWarning
import com.jttools.datacontrol.ui.theme.StateWifi
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlinx.coroutines.launch
@Composable
fun DashboardScreen(systemActions: SystemActions) {
@@ -54,6 +57,9 @@ fun DashboardScreen(systemActions: SystemActions) {
val settings by viewModel.settings.collectAsStateWithLifecycle()
val snapshot by viewModel.snapshot.collectAsStateWithLifecycle()
var showResetDialog by remember { mutableStateOf(false) }
var showDisablePasswordDialog by remember { mutableStateOf(false) }
var disablePasswordError by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val stateColor = when {
!snapshot.serviceRunning -> Color.Gray
@@ -165,7 +171,12 @@ fun DashboardScreen(systemActions: SystemActions) {
}
}
} else {
viewModel.setControlEnabled(false)
if (settings.hasPassword) {
disablePasswordError = false
showDisablePasswordDialog = true
} else {
viewModel.setControlEnabled(false)
}
}
}
)
@@ -205,6 +216,24 @@ fun DashboardScreen(systemActions: SystemActions) {
}
)
}
if (showDisablePasswordDialog) {
PasswordPromptDialog(
title = stringResource(R.string.password_gate_title),
error = disablePasswordError,
onDismiss = { showDisablePasswordDialog = false },
onConfirm = { password ->
scope.launch {
if (viewModel.verifyPassword(password)) {
viewModel.setControlEnabled(false)
showDisablePasswordDialog = false
} else {
disablePasswordError = true
}
}
}
)
}
}
@Composable

View File

@@ -32,4 +32,6 @@ class DashboardViewModel @Inject constructor(
fun resetPlanNow() {
viewModelScope.launch { settingsRepository.resetPlan(usageRepository.totalMobileBytes()) }
}
suspend fun verifyPassword(password: String): Boolean = settingsRepository.verifyPassword(password)
}

View File

@@ -3,6 +3,7 @@ package com.jttools.datacontrol.ui.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
@@ -22,6 +23,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.compose.ui.res.stringResource
import com.jttools.datacontrol.R
import com.jttools.datacontrol.ui.SystemActions
import com.jttools.datacontrol.ui.about.AboutScreen
import com.jttools.datacontrol.ui.dashboard.DashboardScreen
import com.jttools.datacontrol.ui.settings.SettingsScreen
import com.jttools.datacontrol.ui.whitelist.WhitelistScreen
@@ -30,9 +32,10 @@ private sealed class Destination(val route: String, val labelRes: Int, val icon:
data object Dashboard : Destination("dashboard", R.string.nav_dashboard, Icons.Filled.Dashboard)
data object Settings : Destination("settings", R.string.nav_settings, Icons.Filled.Settings)
data object Whitelist : Destination("whitelist", R.string.nav_whitelist, Icons.AutoMirrored.Filled.List)
data object About : Destination("about", R.string.nav_about, Icons.Filled.Info)
}
private val destinations = listOf(Destination.Dashboard, Destination.Settings, Destination.Whitelist)
private val destinations = listOf(Destination.Dashboard, Destination.Settings, Destination.Whitelist, Destination.About)
@Composable
fun DataControlNavHost(systemActions: SystemActions) {
@@ -69,6 +72,7 @@ fun DataControlNavHost(systemActions: SystemActions) {
composable(Destination.Dashboard.route) { DashboardScreen(systemActions) }
composable(Destination.Settings.route) { SettingsScreen(systemActions) }
composable(Destination.Whitelist.route) { WhitelistScreen() }
composable(Destination.About.route) { AboutScreen() }
}
}
}

View File

@@ -33,11 +33,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.jttools.datacontrol.R
import com.jttools.datacontrol.ui.SystemActions
import com.jttools.datacontrol.ui.components.PasswordGate
import com.jttools.datacontrol.ui.theme.StateOk
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
@@ -50,6 +53,18 @@ fun SettingsScreen(systemActions: SystemActions) {
val viewModel: SettingsViewModel = hiltViewModel()
val settings by viewModel.settings.collectAsStateWithLifecycle()
PasswordGate(passwordSet = settings.hasPassword, verify = { viewModel.verifyPassword(it) }) {
SettingsContent(systemActions, viewModel, settings)
}
}
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@Composable
private fun SettingsContent(
systemActions: SystemActions,
viewModel: SettingsViewModel,
settings: com.jttools.datacontrol.data.AppSettings
) {
var planLimitText by remember(settings.planLimitMo) { mutableStateOf(settings.planLimitMo.toString()) }
var planPriceText by remember(settings.planPriceEuro) { mutableStateOf(settings.planPriceEuro.toString()) }
var overagePriceText by remember(settings.overagePricePerMoEuro) { mutableStateOf(settings.overagePricePerMoEuro.toString()) }
@@ -61,6 +76,15 @@ fun SettingsScreen(systemActions: SystemActions) {
var notifGranted by remember { mutableStateOf(systemActions.hasNotificationPermission()) }
var batteryIgnored by remember { mutableStateOf(systemActions.isIgnoringBatteryOptimizations()) }
var newPassword by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf<String?>(null) }
var passwordMessage by remember { mutableStateOf<String?>(null) }
val passwordTooShort = stringResource(R.string.settings_password_too_short)
val passwordMismatch = stringResource(R.string.settings_password_mismatch)
val passwordSetMessage = stringResource(R.string.settings_password_set_confirmation)
val passwordRemovedMessage = stringResource(R.string.settings_password_removed_confirmation)
LaunchedEffect(Unit) {
vpnGranted = systemActions.isVpnPrepared()
notifGranted = systemActions.hasNotificationPermission()
@@ -174,6 +198,68 @@ fun SettingsScreen(systemActions: SystemActions) {
}
}
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(stringResource(R.string.settings_section_security), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
HorizontalDivider()
Text(
if (settings.hasPassword) stringResource(R.string.settings_password_active)
else stringResource(R.string.settings_password_inactive),
style = MaterialTheme.typography.bodyMedium,
color = if (settings.hasPassword) StateOk else MaterialTheme.colorScheme.onSurfaceVariant
)
OutlinedTextField(
value = newPassword,
onValueChange = { newPassword = it; passwordError = null; passwordMessage = null },
label = { Text(stringResource(R.string.settings_password_new)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it; passwordError = null; passwordMessage = null },
label = { Text(stringResource(R.string.settings_password_confirm)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
passwordError?.let {
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
}
passwordMessage?.let {
Text(it, color = StateOk, style = MaterialTheme.typography.bodySmall)
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Button(onClick = {
when {
newPassword.length < 4 -> passwordError = passwordTooShort
newPassword != confirmPassword -> passwordError = passwordMismatch
else -> {
viewModel.setPassword(newPassword)
newPassword = ""
confirmPassword = ""
passwordError = null
passwordMessage = passwordSetMessage
}
}
}) { Text(stringResource(R.string.settings_password_set)) }
if (settings.hasPassword) {
OutlinedButton(onClick = {
viewModel.setPassword(null)
newPassword = ""
confirmPassword = ""
passwordError = null
passwordMessage = passwordRemovedMessage
}) { Text(stringResource(R.string.settings_password_remove)) }
}
}
}
}
Button(
onClick = {
viewModel.updatePlan(

View File

@@ -37,4 +37,10 @@ class SettingsViewModel @Inject constructor(
fun setStartDate(date: LocalDate) {
viewModelScope.launch { settingsRepository.setStartDate(date) }
}
fun setPassword(password: String?) {
viewModelScope.launch { settingsRepository.setPassword(password) }
}
suspend fun verifyPassword(password: String): Boolean = settingsRepository.verifyPassword(password)
}

View File

@@ -39,11 +39,20 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.jttools.datacontrol.R
import com.jttools.datacontrol.data.AppInfo
import com.jttools.datacontrol.ui.components.PasswordGate
@Composable
fun WhitelistScreen() {
val viewModel: WhitelistViewModel = hiltViewModel()
val settings by viewModel.settings.collectAsStateWithLifecycle()
PasswordGate(passwordSet = settings.hasPassword, verify = { viewModel.verifyPassword(it) }) {
WhitelistContent(viewModel, settings)
}
}
@Composable
private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttools.datacontrol.data.AppSettings) {
val apps by viewModel.apps.collectAsStateWithLifecycle()
val loading by viewModel.loading.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }

View File

@@ -48,4 +48,6 @@ class WhitelistViewModel @Inject constructor(
fun setShowSystemApps(show: Boolean) {
viewModelScope.launch { settingsRepository.setShowSystemApps(show) }
}
suspend fun verifyPassword(password: String): Boolean = settingsRepository.verifyPassword(password)
}

View File

@@ -6,6 +6,7 @@
<string name="nav_dashboard">Tableau de bord</string>
<string name="nav_settings">Réglages</string>
<string name="nav_whitelist">Liste blanche</string>
<string name="nav_about">À propos</string>
<!-- Dashboard -->
<string name="dashboard_title">Tableau de bord</string>
@@ -57,6 +58,33 @@
<string name="settings_save">Enregistrer</string>
<string name="settings_edit_date">Modifier</string>
<string name="settings_saved">Réglages enregistrés</string>
<string name="settings_section_security">Sécurité</string>
<string name="settings_password_active">Mot de passe actif — protège la désactivation et les réglages</string>
<string name="settings_password_inactive">Aucun mot de passe défini</string>
<string name="settings_password_new">Nouveau mot de passe</string>
<string name="settings_password_confirm">Confirmer le mot de passe</string>
<string name="settings_password_set">Définir</string>
<string name="settings_password_remove">Supprimer</string>
<string name="settings_password_too_short">Le mot de passe doit contenir au moins 4 caractères</string>
<string name="settings_password_mismatch">Les mots de passe ne correspondent pas</string>
<string name="settings_password_set_confirmation">Mot de passe enregistré</string>
<string name="settings_password_removed_confirmation">Mot de passe supprimé</string>
<!-- Verrouillage par mot de passe -->
<string name="password_gate_title">Zone protégée</string>
<string name="password_field_label">Mot de passe</string>
<string name="password_incorrect">Mot de passe incorrect</string>
<string name="password_unlock">Déverrouiller</string>
<!-- About -->
<string name="about_title">À propos</string>
<string name="about_version">Version %1$s</string>
<string name="about_section_description">Description</string>
<string name="about_description">DataControl surveille en arrière-plan la consommation de données mobiles et coupe automatiquement l\'accès à Internet en cas de dépassement du forfait, sans jamais toucher au Wi-Fi.</string>
<string name="about_section_how">Fonctionnement</string>
<string name="about_how_it_works">Le filtrage repose sur un pare-feu local basé sur VpnService (sans root) : le trafic des applications non autorisées est capturé dans un tunnel VPN local puis silencieusement ignoré, tandis que les applications de la liste blanche continuent de communiquer normalement.</string>
<string name="about_author">Auteur</string>
<string name="about_author_value">JT-Tools — Johnny</string>
<!-- Whitelist -->
<string name="whitelist_title">Liste blanche</string>