update + Fix
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,25 @@
|
|||||||
package com.jttools.datacontrol
|
package com.jttools.datacontrol
|
||||||
|
|
||||||
import android.app.Application
|
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 dagger.hilt.android.HiltAndroidApp
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@HiltAndroidApp
|
@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 cutoffIntervalMinutes: Int = 15,
|
||||||
val baselineMobileBytesAtReset: Long = 0L,
|
val baselineMobileBytesAtReset: Long = 0L,
|
||||||
val whitelistedPackages: Set<String> = emptySet(),
|
val whitelistedPackages: Set<String> = emptySet(),
|
||||||
|
val whitelistedPackagesWifi: Set<String> = emptySet(),
|
||||||
|
val wifiFilterEnabled: Boolean = false,
|
||||||
val showSystemApps: Boolean = false,
|
val showSystemApps: Boolean = false,
|
||||||
val passwordHash: String? = null,
|
val passwordHash: String? = null,
|
||||||
val passwordSalt: String? = null,
|
val passwordSalt: String? = null,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class SettingsRepository @Inject constructor(
|
|||||||
val CUTOFF_INTERVAL_MINUTES = intPreferencesKey("cutoff_interval_minutes")
|
val CUTOFF_INTERVAL_MINUTES = intPreferencesKey("cutoff_interval_minutes")
|
||||||
val BASELINE_MOBILE_BYTES = longPreferencesKey("baseline_mobile_bytes")
|
val BASELINE_MOBILE_BYTES = longPreferencesKey("baseline_mobile_bytes")
|
||||||
val WHITELISTED_PACKAGES = stringSetPreferencesKey("whitelisted_packages")
|
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 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")
|
||||||
@@ -53,6 +55,8 @@ class SettingsRepository @Inject constructor(
|
|||||||
cutoffIntervalMinutes = prefs[Keys.CUTOFF_INTERVAL_MINUTES] ?: defaults.cutoffIntervalMinutes,
|
cutoffIntervalMinutes = prefs[Keys.CUTOFF_INTERVAL_MINUTES] ?: defaults.cutoffIntervalMinutes,
|
||||||
baselineMobileBytesAtReset = prefs[Keys.BASELINE_MOBILE_BYTES] ?: defaults.baselineMobileBytesAtReset,
|
baselineMobileBytesAtReset = prefs[Keys.BASELINE_MOBILE_BYTES] ?: defaults.baselineMobileBytesAtReset,
|
||||||
whitelistedPackages = prefs[Keys.WHITELISTED_PACKAGES] ?: defaults.whitelistedPackages,
|
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,
|
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],
|
||||||
@@ -91,6 +95,14 @@ class SettingsRepository @Inject constructor(
|
|||||||
context.dataStore.edit { it[Keys.WHITELISTED_PACKAGES] = packages }
|
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) {
|
suspend fun setShowSystemApps(show: Boolean) {
|
||||||
context.dataStore.edit { it[Keys.SHOW_SYSTEM_APPS] = show }
|
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 }
|
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. */
|
/** Définit (ou supprime, si [password] est vide/null) le mot de passe de verrouillage. */
|
||||||
suspend fun setPassword(password: String?) {
|
suspend fun setPassword(password: String?) {
|
||||||
if (password.isNullOrBlank()) {
|
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 }
|
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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ContextCompat.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
@@ -133,7 +140,11 @@ class DataMonitorService : LifecycleService() {
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
latestTransport == NetworkTransport.WIFI -> {
|
latestTransport == NetworkTransport.WIFI -> {
|
||||||
applyVpnMode(VpnMode.STOPPED, emptySet())
|
if (settings.wifiFilterEnabled && FirewallVpnService.isPrepared(this)) {
|
||||||
|
applyVpnMode(VpnMode.FILTER, settings.whitelistedPackagesWifi)
|
||||||
|
} else {
|
||||||
|
applyVpnMode(VpnMode.STOPPED, emptySet())
|
||||||
|
}
|
||||||
resetCutoffCycle()
|
resetCutoffCycle()
|
||||||
dataState = DataState.WIFI
|
dataState = DataState.WIFI
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -72,6 +73,10 @@ private fun SettingsContent(
|
|||||||
var autoReset by remember(settings.autoResetEnabled) { mutableStateOf(settings.autoResetEnabled) }
|
var autoReset by remember(settings.autoResetEnabled) { mutableStateOf(settings.autoResetEnabled) }
|
||||||
var showDatePicker by remember { mutableStateOf(false) }
|
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 vpnGranted by remember { mutableStateOf(systemActions.isVpnPrepared()) }
|
||||||
var notifGranted by remember { mutableStateOf(systemActions.hasNotificationPermission()) }
|
var notifGranted by remember { mutableStateOf(systemActions.hasNotificationPermission()) }
|
||||||
var batteryIgnored by remember { mutableStateOf(systemActions.isIgnoringBatteryOptimizations()) }
|
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)) }
|
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(
|
Row(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -294,6 +318,32 @@ private fun SettingsContent(
|
|||||||
}
|
}
|
||||||
) { DatePicker(state = datePickerState) }
|
) { 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
|
@Composable
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.jttools.datacontrol.data.AppSettings
|
import com.jttools.datacontrol.data.AppSettings
|
||||||
import com.jttools.datacontrol.data.SettingsRepository
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -13,13 +16,19 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(
|
class SettingsViewModel @Inject constructor(
|
||||||
private val settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val usageRepository: UsageRepository,
|
||||||
|
stateHolder: MonitorStateHolder
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val settings = settingsRepository.settingsFlow.stateIn(
|
val settings = settingsRepository.settingsFlow.stateIn(
|
||||||
viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings()
|
viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val consumedMo = stateHolder.snapshot.map { it.consumedMo }.stateIn(
|
||||||
|
viewModelScope, SharingStarted.WhileSubscribed(5_000), 0L
|
||||||
|
)
|
||||||
|
|
||||||
fun updatePlan(
|
fun updatePlan(
|
||||||
planLimitMo: Long,
|
planLimitMo: Long,
|
||||||
planPriceEuro: Double,
|
planPriceEuro: Double,
|
||||||
@@ -38,6 +47,10 @@ class SettingsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch { settingsRepository.setStartDate(date) }
|
viewModelScope.launch { settingsRepository.setStartDate(date) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setConsumedMo(consumedMo: Long) {
|
||||||
|
viewModelScope.launch { settingsRepository.setConsumedMo(consumedMo, usageRepository.totalMobileBytes()) }
|
||||||
|
}
|
||||||
|
|
||||||
fun setPassword(password: String?) {
|
fun setPassword(password: String?) {
|
||||||
viewModelScope.launch { settingsRepository.setPassword(password) }
|
viewModelScope.launch { settingsRepository.setPassword(password) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ import androidx.compose.material3.Checkbox
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -56,11 +60,15 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
|||||||
val apps by viewModel.apps.collectAsStateWithLifecycle()
|
val apps by viewModel.apps.collectAsStateWithLifecycle()
|
||||||
val loading by viewModel.loading.collectAsStateWithLifecycle()
|
val loading by viewModel.loading.collectAsStateWithLifecycle()
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
var tabIndex by remember { mutableIntStateOf(0) }
|
||||||
|
val forWifi = tabIndex == 1
|
||||||
|
|
||||||
val filtered = apps
|
val filtered = apps
|
||||||
.filter { settings.showSystemApps || !it.isSystemApp }
|
.filter { settings.showSystemApps || !it.isSystemApp }
|
||||||
.filter { it.label.contains(query, ignoreCase = true) || it.packageName.contains(query, ignoreCase = true) }
|
.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)) {
|
Column(Modifier.fillMaxSize().padding(20.dp)) {
|
||||||
Text(stringResource(R.string.whitelist_title), style = MaterialTheme.typography.headlineMedium)
|
Text(stringResource(R.string.whitelist_title), style = MaterialTheme.typography.headlineMedium)
|
||||||
Text(
|
Text(
|
||||||
@@ -70,13 +78,44 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
|||||||
modifier = Modifier.padding(top = 6.dp, bottom = 14.dp)
|
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(
|
OutlinedTextField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = { query = it },
|
onValueChange = { query = it },
|
||||||
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
||||||
label = { Text(stringResource(R.string.whitelist_search_hint)) },
|
label = { Text(stringResource(R.string.whitelist_search_hint)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth().padding(top = 10.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
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)
|
Text(stringResource(R.string.whitelist_system_apps_toggle), style = MaterialTheme.typography.bodyMedium)
|
||||||
Switch(checked = settings.showSystemApps, onCheckedChange = { viewModel.setShowSystemApps(it) })
|
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()
|
HorizontalDivider()
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -96,8 +149,8 @@ private fun WhitelistContent(viewModel: WhitelistViewModel, settings: com.jttool
|
|||||||
items(filtered, key = { it.packageName }) { app ->
|
items(filtered, key = { it.packageName }) { app ->
|
||||||
AppRow(
|
AppRow(
|
||||||
app = app,
|
app = app,
|
||||||
checked = settings.whitelistedPackages.contains(app.packageName),
|
checked = activeSet.contains(app.packageName),
|
||||||
onCheckedChange = { viewModel.setWhitelisted(app.packageName, it) }
|
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 {
|
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
|
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) {
|
fun setShowSystemApps(show: Boolean) {
|
||||||
viewModelScope.launch { settingsRepository.setShowSystemApps(show) }
|
viewModelScope.launch { settingsRepository.setShowSystemApps(show) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
<string name="settings_title">Réglages</string>
|
<string name="settings_title">Réglages</string>
|
||||||
<string name="settings_section_plan">Forfait</string>
|
<string name="settings_section_plan">Forfait</string>
|
||||||
<string name="settings_plan_limit_mo">Forfait data (Mo)</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_plan_price">Prix du forfait (€)</string>
|
||||||
<string name="settings_overage_price">Prix par Mo hors-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>
|
<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_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_search_hint">Rechercher une application</string>
|
||||||
<string name="whitelist_system_apps_toggle">Afficher les applications système</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 -->
|
<!-- Notification -->
|
||||||
<string name="notification_channel_name">Surveillance des données</string>
|
<string name="notification_channel_name">Surveillance des données</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user