update + Fix
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,25 @@
|
||||
package com.jttools.datacontrol
|
||||
|
||||
import android.app.Application
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.jttools.datacontrol.service.DataMonitorRestartWorker
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@HiltAndroidApp
|
||||
class DataControlApp : Application()
|
||||
class DataControlApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Filet de sécurité : relance périodiquement DataMonitorService s'il a été tué en arrière-plan.
|
||||
// enqueueUniquePeriodicWork + KEEP : sans effet si déjà planifié, donc sûr à appeler à chaque démarrage.
|
||||
val request = PeriodicWorkRequestBuilder<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
DataMonitorRestartWorker.WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ data class AppSettings(
|
||||
val cutoffIntervalMinutes: Int = 15,
|
||||
val baselineMobileBytesAtReset: Long = 0L,
|
||||
val whitelistedPackages: Set<String> = emptySet(),
|
||||
val whitelistedPackagesWifi: Set<String> = emptySet(),
|
||||
val wifiFilterEnabled: Boolean = false,
|
||||
val showSystemApps: Boolean = false,
|
||||
val passwordHash: String? = null,
|
||||
val passwordSalt: String? = null,
|
||||
|
||||
@@ -33,6 +33,8 @@ class SettingsRepository @Inject constructor(
|
||||
val CUTOFF_INTERVAL_MINUTES = intPreferencesKey("cutoff_interval_minutes")
|
||||
val BASELINE_MOBILE_BYTES = longPreferencesKey("baseline_mobile_bytes")
|
||||
val WHITELISTED_PACKAGES = stringSetPreferencesKey("whitelisted_packages")
|
||||
val WHITELISTED_PACKAGES_WIFI = stringSetPreferencesKey("whitelisted_packages_wifi")
|
||||
val WIFI_FILTER_ENABLED = booleanPreferencesKey("wifi_filter_enabled")
|
||||
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
|
||||
val PASSWORD_HASH = stringPreferencesKey("password_hash")
|
||||
val PASSWORD_SALT = stringPreferencesKey("password_salt")
|
||||
@@ -53,6 +55,8 @@ 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,
|
||||
whitelistedPackagesWifi = prefs[Keys.WHITELISTED_PACKAGES_WIFI] ?: defaults.whitelistedPackagesWifi,
|
||||
wifiFilterEnabled = prefs[Keys.WIFI_FILTER_ENABLED] ?: defaults.wifiFilterEnabled,
|
||||
showSystemApps = prefs[Keys.SHOW_SYSTEM_APPS] ?: defaults.showSystemApps,
|
||||
passwordHash = prefs[Keys.PASSWORD_HASH],
|
||||
passwordSalt = prefs[Keys.PASSWORD_SALT],
|
||||
@@ -91,6 +95,14 @@ class SettingsRepository @Inject constructor(
|
||||
context.dataStore.edit { it[Keys.WHITELISTED_PACKAGES] = packages }
|
||||
}
|
||||
|
||||
suspend fun setWhitelistedPackagesWifi(packages: Set<String>) {
|
||||
context.dataStore.edit { it[Keys.WHITELISTED_PACKAGES_WIFI] = packages }
|
||||
}
|
||||
|
||||
suspend fun setWifiFilterEnabled(enabled: Boolean) {
|
||||
context.dataStore.edit { it[Keys.WIFI_FILTER_ENABLED] = enabled }
|
||||
}
|
||||
|
||||
suspend fun setShowSystemApps(show: Boolean) {
|
||||
context.dataStore.edit { it[Keys.SHOW_SYSTEM_APPS] = show }
|
||||
}
|
||||
@@ -108,6 +120,16 @@ class SettingsRepository @Inject constructor(
|
||||
context.dataStore.edit { it[Keys.BASELINE_MOBILE_BYTES] = currentMobileBytes }
|
||||
}
|
||||
|
||||
/**
|
||||
* Force la consommation affichée à [consumedMo] sans changer la date de début du cycle, en
|
||||
* recalculant la base de référence à partir du compteur système actuel ([currentTotalBytes]).
|
||||
* Utile pour recaler l'app sur le décompte opérateur réel.
|
||||
*/
|
||||
suspend fun setConsumedMo(consumedMo: Long, currentTotalBytes: Long) {
|
||||
val baseline = (currentTotalBytes - UsageRepository.moToBytes(consumedMo)).coerceAtLeast(0L)
|
||||
context.dataStore.edit { it[Keys.BASELINE_MOBILE_BYTES] = baseline }
|
||||
}
|
||||
|
||||
/** Définit (ou supprime, si [password] est vide/null) le mot de passe de verrouillage. */
|
||||
suspend fun setPassword(password: String?) {
|
||||
if (password.isNullOrBlank()) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jttools.datacontrol.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.jttools.datacontrol.data.SettingsRepository
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Filet de sécurité périodique (WorkManager) : relance [DataMonitorService] s'il a été tué par le
|
||||
* système alors que le contrôle est censé être actif. START_STICKY et le redémarrage au boot
|
||||
* (voir [BootReceiver]) ne suffisent pas toujours sur les ROM qui tuent agressivement les apps en
|
||||
* arrière-plan (ex: ColorOS) sans exemption d'optimisation batterie. Démarrer un service déjà en
|
||||
* cours d'exécution est sans effet, cet appel est donc sûr à répéter à chaque déclenchement.
|
||||
*/
|
||||
class DataMonitorRestartWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface RestartWorkerEntryPoint {
|
||||
fun settingsRepository(): SettingsRepository
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val appContext = applicationContext
|
||||
val entryPoint = EntryPointAccessors.fromApplication(appContext, RestartWorkerEntryPoint::class.java)
|
||||
val enabled = entryPoint.settingsRepository().settingsFlow.first().controlEnabled
|
||||
if (enabled) DataMonitorService.start(appContext)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WORK_NAME = "data_monitor_restart_watchdog"
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,13 @@ class DataMonitorService : LifecycleService() {
|
||||
|
||||
private enum class VpnMode { FILTER, BLOCK_ALL, STOPPED }
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
// START_STICKY : si le système tue ce service (mémoire faible), il le relance
|
||||
// automatiquement (avec un Intent null) dès que les ressources redeviennent disponibles.
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ContextCompat.registerReceiver(
|
||||
@@ -133,7 +140,11 @@ class DataMonitorService : LifecycleService() {
|
||||
|
||||
when {
|
||||
latestTransport == NetworkTransport.WIFI -> {
|
||||
applyVpnMode(VpnMode.STOPPED, emptySet())
|
||||
if (settings.wifiFilterEnabled && FirewallVpnService.isPrepared(this)) {
|
||||
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackagesWifi)
|
||||
} else {
|
||||
applyVpnMode(VpnMode.STOPPED, emptySet())
|
||||
}
|
||||
resetCutoffCycle()
|
||||
dataState = DataState.WIFI
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -72,6 +73,10 @@ private fun SettingsContent(
|
||||
var autoReset by remember(settings.autoResetEnabled) { mutableStateOf(settings.autoResetEnabled) }
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
val consumedMo by viewModel.consumedMo.collectAsStateWithLifecycle()
|
||||
var showConsumedDialog by remember { mutableStateOf(false) }
|
||||
var consumedMoText by remember { mutableStateOf("") }
|
||||
|
||||
var vpnGranted by remember { mutableStateOf(systemActions.isVpnPrepared()) }
|
||||
var notifGranted by remember { mutableStateOf(systemActions.hasNotificationPermission()) }
|
||||
var batteryIgnored by remember { mutableStateOf(systemActions.isIgnoringBatteryOptimizations()) }
|
||||
@@ -142,6 +147,25 @@ private fun SettingsContent(
|
||||
OutlinedButton(onClick = { showDatePicker = true }) { Text(stringResource(R.string.settings_edit_date)) }
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(stringResource(R.string.settings_current_consumption), style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
"$consumedMo Mo",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
OutlinedButton(onClick = {
|
||||
consumedMoText = consumedMo.toString()
|
||||
showConsumedDialog = true
|
||||
}) { Text(stringResource(R.string.settings_edit_date)) }
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -294,6 +318,32 @@ private fun SettingsContent(
|
||||
}
|
||||
) { DatePicker(state = datePickerState) }
|
||||
}
|
||||
|
||||
if (showConsumedDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showConsumedDialog = false },
|
||||
title = { Text(stringResource(R.string.settings_current_consumption)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = consumedMoText,
|
||||
onValueChange = { consumedMoText = it.filter(Char::isDigit) },
|
||||
label = { Text(stringResource(R.string.settings_plan_limit_mo_unit)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
consumedMoText.toLongOrNull()?.let { viewModel.setConsumedMo(it.coerceAtLeast(0L)) }
|
||||
showConsumedDialog = false
|
||||
}) { Text(stringResource(R.string.common_confirm)) }
|
||||
},
|
||||
dismissButton = {
|
||||
OutlinedButton(onClick = { showConsumedDialog = false }) { Text(stringResource(R.string.common_cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -4,8 +4,11 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.jttools.datacontrol.data.AppSettings
|
||||
import com.jttools.datacontrol.data.SettingsRepository
|
||||
import com.jttools.datacontrol.data.UsageRepository
|
||||
import com.jttools.datacontrol.service.MonitorStateHolder
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
@@ -13,13 +16,19 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val usageRepository: UsageRepository,
|
||||
stateHolder: MonitorStateHolder
|
||||
) : ViewModel() {
|
||||
|
||||
val settings = settingsRepository.settingsFlow.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings()
|
||||
)
|
||||
|
||||
val consumedMo = stateHolder.snapshot.map { it.consumedMo }.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5_000), 0L
|
||||
)
|
||||
|
||||
fun updatePlan(
|
||||
planLimitMo: Long,
|
||||
planPriceEuro: Double,
|
||||
@@ -38,6 +47,10 @@ class SettingsViewModel @Inject constructor(
|
||||
viewModelScope.launch { settingsRepository.setStartDate(date) }
|
||||
}
|
||||
|
||||
fun setConsumedMo(consumedMo: Long) {
|
||||
viewModelScope.launch { settingsRepository.setConsumedMo(consumedMo, usageRepository.totalMobileBytes()) }
|
||||
}
|
||||
|
||||
fun setPassword(password: String?) {
|
||||
viewModelScope.launch { settingsRepository.setPassword(password) }
|
||||
}
|
||||
|
||||
@@ -20,11 +20,15 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -56,11 +60,15 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
||||
val apps by viewModel.apps.collectAsStateWithLifecycle()
|
||||
val loading by viewModel.loading.collectAsStateWithLifecycle()
|
||||
var query by remember { mutableStateOf("") }
|
||||
var tabIndex by remember { mutableIntStateOf(0) }
|
||||
val forWifi = tabIndex == 1
|
||||
|
||||
val filtered = apps
|
||||
.filter { settings.showSystemApps || !it.isSystemApp }
|
||||
.filter { it.label.contains(query, ignoreCase = true) || it.packageName.contains(query, ignoreCase = true) }
|
||||
|
||||
val activeSet = if (forWifi) settings.whitelistedPackagesWifi else settings.whitelistedPackages
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(20.dp)) {
|
||||
Text(stringResource(R.string.whitelist_title), style = MaterialTheme.typography.headlineMedium)
|
||||
Text(
|
||||
@@ -70,13 +78,44 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
||||
modifier = Modifier.padding(top = 6.dp, bottom = 14.dp)
|
||||
)
|
||||
|
||||
TabRow(selectedTabIndex = tabIndex) {
|
||||
Tab(
|
||||
selected = tabIndex == 0,
|
||||
onClick = { tabIndex = 0 },
|
||||
text = { Text(stringResource(R.string.whitelist_tab_mobile)) }
|
||||
)
|
||||
Tab(
|
||||
selected = tabIndex == 1,
|
||||
onClick = { tabIndex = 1 },
|
||||
text = { Text(stringResource(R.string.whitelist_tab_wifi)) }
|
||||
)
|
||||
}
|
||||
|
||||
if (forWifi) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(top = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.whitelist_wifi_filter_toggle), style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
stringResource(R.string.whitelist_wifi_filter_help),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(checked = settings.wifiFilterEnabled, onCheckedChange = { viewModel.setWifiFilterEnabled(it) })
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = query,
|
||||
onValueChange = { query = it },
|
||||
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
||||
label = { Text(stringResource(R.string.whitelist_search_hint)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -87,6 +126,20 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
||||
Text(stringResource(R.string.whitelist_system_apps_toggle), style = MaterialTheme.typography.bodyMedium)
|
||||
Switch(checked = settings.showSystemApps, onCheckedChange = { viewModel.setShowSystemApps(it) })
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(bottom = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.setWhitelistedBulk(filtered.map { it.packageName }, whitelisted = true, forWifi = forWifi) },
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text(stringResource(R.string.whitelist_select_all)) }
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.setWhitelistedBulk(filtered.map { it.packageName }, whitelisted = false, forWifi = forWifi) },
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text(stringResource(R.string.whitelist_select_none)) }
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (loading) {
|
||||
@@ -96,8 +149,8 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
||||
items(filtered, key = { it.packageName }) { app ->
|
||||
AppRow(
|
||||
app = app,
|
||||
checked = settings.whitelistedPackages.contains(app.packageName),
|
||||
onCheckedChange = { viewModel.setWhitelisted(app.packageName, it) }
|
||||
checked = activeSet.contains(app.packageName),
|
||||
onCheckedChange = { viewModel.setWhitelisted(app.packageName, it, forWifi) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +37,27 @@ class WhitelistViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setWhitelisted(packageName: String, whitelisted: Boolean) {
|
||||
fun setWhitelisted(packageName: String, whitelisted: Boolean, forWifi: Boolean) {
|
||||
viewModelScope.launch {
|
||||
val current = settings.value.whitelistedPackages
|
||||
val current = if (forWifi) settings.value.whitelistedPackagesWifi else settings.value.whitelistedPackages
|
||||
val updated = if (whitelisted) current + packageName else current - packageName
|
||||
settingsRepository.setWhitelistedPackages(updated)
|
||||
if (forWifi) settingsRepository.setWhitelistedPackagesWifi(updated) else settingsRepository.setWhitelistedPackages(updated)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sélectionne ou désélectionne en bloc les paquets [packageNames] (ex : la liste actuellement filtrée à l'écran). */
|
||||
fun setWhitelistedBulk(packageNames: List<String>, whitelisted: Boolean, forWifi: Boolean) {
|
||||
viewModelScope.launch {
|
||||
val current = if (forWifi) settings.value.whitelistedPackagesWifi else settings.value.whitelistedPackages
|
||||
val updated = if (whitelisted) current + packageNames else current - packageNames.toSet()
|
||||
if (forWifi) settingsRepository.setWhitelistedPackagesWifi(updated) else settingsRepository.setWhitelistedPackages(updated)
|
||||
}
|
||||
}
|
||||
|
||||
fun setWifiFilterEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch { settingsRepository.setWifiFilterEnabled(enabled) }
|
||||
}
|
||||
|
||||
fun setShowSystemApps(show: Boolean) {
|
||||
viewModelScope.launch { settingsRepository.setShowSystemApps(show) }
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<string name="settings_title">Réglages</string>
|
||||
<string name="settings_section_plan">Forfait</string>
|
||||
<string name="settings_plan_limit_mo">Forfait data (Mo)</string>
|
||||
<string name="settings_plan_limit_mo_unit">Mo</string>
|
||||
<string name="settings_current_consumption">Consommation actuelle</string>
|
||||
<string name="settings_plan_price">Prix du forfait (€)</string>
|
||||
<string name="settings_overage_price">Prix par Mo hors-forfait (€)</string>
|
||||
<string name="settings_start_date">Date de début du forfait</string>
|
||||
@@ -91,6 +93,12 @@
|
||||
<string name="whitelist_description">Ces applications gardent toujours accès aux données mobiles, y compris en dépassement de forfait (sauf pendant les coupures cycliques totales).</string>
|
||||
<string name="whitelist_search_hint">Rechercher une application</string>
|
||||
<string name="whitelist_system_apps_toggle">Afficher les applications système</string>
|
||||
<string name="whitelist_tab_mobile">Données mobiles</string>
|
||||
<string name="whitelist_tab_wifi">Wi-Fi</string>
|
||||
<string name="whitelist_wifi_filter_toggle">Filtrer aussi le Wi-Fi</string>
|
||||
<string name="whitelist_wifi_filter_help">Par défaut, le Wi-Fi n\'est jamais filtré. Activez pour restreindre l\'accès Wi-Fi aux applications cochées ci-dessous.</string>
|
||||
<string name="whitelist_select_all">Tout sélectionner</string>
|
||||
<string name="whitelist_select_none">Tout désélectionner</string>
|
||||
|
||||
<!-- Notification -->
|
||||
<string name="notification_channel_name">Surveillance des données</string>
|
||||
|
||||
Reference in New Issue
Block a user