feat: disable kill switch on trusted

fix: debounce ui bug closes #532
This commit is contained in:
Zane Schepke 2025-01-19 12:37:43 -05:00
parent 3e6a2981cb
commit f3debcfe45
No known key found for this signature in database
GPG Key ID: 82217F0566F9B4A5
13 changed files with 433 additions and 72 deletions

View File

@ -0,0 +1,281 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "4827f3b1ab5a4e5aa35937a0925d50e4",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4827f3b1ab5a4e5aa35937a0925d50e4')"
]
}
}

View File

@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 14, version = 15,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@ -53,6 +53,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
from = 13, from = 13,
to = 14, to = 14,
), ),
AutoMigration(
from = 14,
to = 15,
),
], ],
exportSchema = true, exportSchema = true,
) )

View File

@ -85,6 +85,11 @@ data class Settings(
defaultValue = "3", defaultValue = "3",
) )
val debounceDelaySeconds: Int = 3, val debounceDelaySeconds: Int = 3,
@ColumnInfo(
name = "is_disable_kill_switch_on_trusted_enabled",
defaultValue = "false",
)
val isDisableKillSwitchOnTrustedEnabled: Boolean = false,
) { ) {
fun debounceDelayMillis(): Long { fun debounceDelayMillis(): Long {
return debounceDelaySeconds * 1000L return debounceDelaySeconds * 1000L

View File

@ -17,6 +17,7 @@ import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.KillSwitchEvent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.InternetConnectivityService import com.zaneschepke.wireguardautotunnel.service.network.InternetConnectivityService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
@ -24,6 +25,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
@ -111,6 +113,7 @@ class AutoTunnelService : LifecycleService() {
} }
startAutoTunnelJob() startAutoTunnelJob()
startAutoTunnelStateJob() startAutoTunnelStateJob()
startKillSwitchJob()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
@ -181,7 +184,6 @@ class AutoTunnelService : LifecycleService() {
) { double, networkState -> ) { double, networkState ->
AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second) AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second)
}.collect { state -> }.collect { state ->
Timber.d("Network state: ${state.networkState}")
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy(vpnState = state.vpnState, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels) it.copy(vpnState = state.vpnState, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
} }
@ -209,6 +211,23 @@ class AutoTunnelService : LifecycleService() {
}.distinctUntilChanged() }.distinctUntilChanged()
} }
private fun startKillSwitchJob() = lifecycleScope.launch(ioDispatcher) {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
when (val event = it.asKillSwitchEvent()) {
KillSwitchEvent.DoNothing -> Unit
is KillSwitchEvent.Start -> {
Timber.d("Starting kill switch")
tunnelService.get().setBackendState(BackendState.KILL_SWITCH_ACTIVE, event.allowedIps)
}
KillSwitchEvent.Stop -> {
Timber.d("Stopping kill switch")
tunnelService.get().setBackendState(BackendState.SERVICE_ACTIVE, emptySet())
}
}
}
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) { private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) {
Timber.i("Starting auto-tunnel network event watcher") Timber.i("Starting auto-tunnel network event watcher")

View File

@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
@ -52,6 +53,14 @@ data class AutoTunnelState(
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp() return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp()
} }
private fun stopKillSwitchOnTrusted(): Boolean {
return networkState.isWifiConnected && settings.isVpnKillSwitchEnabled && settings.isDisableKillSwitchOnTrustedEnabled && isCurrentSSIDTrusted() && vpnState.backendState == BackendState.KILL_SWITCH_ACTIVE
}
private fun startKillSwitch(): Boolean {
return settings.isVpnKillSwitchEnabled && vpnState.backendState != BackendState.KILL_SWITCH_ACTIVE && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
}
fun isNoConnectivity(): Boolean { fun isNoConnectivity(): Boolean {
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
} }
@ -116,6 +125,17 @@ data class AutoTunnelState(
} }
} }
fun asKillSwitchEvent(): KillSwitchEvent {
return when {
stopKillSwitchOnTrusted() -> KillSwitchEvent.Stop
startKillSwitch() -> {
val allowedIps = if (settings.isLanOnKillSwitchEnabled) TunnelConfig.LAN_BYPASS_ALLOWED_IPS else emptySet()
KillSwitchEvent.Start(allowedIps)
}
else -> KillSwitchEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean { private fun isCurrentSSIDTrusted(): Boolean {
return networkState.wifiName?.let { return networkState.wifiName?.let {
hasTrustedWifiName(it) hasTrustedWifiName(it)

View File

@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model
sealed class KillSwitchEvent {
data class Start(val allowedIps: Set<String>) : KillSwitchEvent()
data object Stop : KillSwitchEvent()
data object DoNothing : KillSwitchEvent()
}

View File

@ -5,6 +5,7 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStati
data class VpnState( data class VpnState(
val status: TunnelState = TunnelState.DOWN, val status: TunnelState = TunnelState.DOWN,
val backendState: BackendState = BackendState.INACTIVE,
val tunnelConfig: TunnelConfig? = null, val tunnelConfig: TunnelConfig? = null,
val statistics: TunnelStatistics? = null, val statistics: TunnelStatistics? = null,
) )

View File

@ -222,6 +222,9 @@ constructor(
when (val backend = backend()) { when (val backend = backend()) {
is org.amnezia.awg.backend.Backend -> { is org.amnezia.awg.backend.Backend -> {
backend.setBackendState(backendState.asAmBackendState(), allowedIps) backend.setBackendState(backendState.asAmBackendState(), allowedIps)
_vpnState.update {
it.copy(backendState = backendState)
}
} }
is Backend -> { is Backend -> {
// TODO not yet implemented // TODO not yet implemented

View File

@ -198,7 +198,7 @@ class MainActivity : AppCompatActivity() {
} }
composable<Route.AutoTunnel> { composable<Route.AutoTunnel> {
AutoTunnelScreen( AutoTunnelScreen(
appUiState, appUiState.settings,
) )
} }
composable<Route.Appearance> { composable<Route.Appearance> {
@ -214,7 +214,7 @@ class MainActivity : AppCompatActivity() {
SupportScreen(appUiState, viewModel) SupportScreen(appUiState, viewModel)
} }
composable<Route.AutoTunnelAdvanced> { composable<Route.AutoTunnelAdvanced> {
AdvancedScreen(appUiState, viewModel) AdvancedScreen(appUiState.settings, viewModel)
} }
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()

View File

@ -21,6 +21,7 @@ import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsEthernet import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.VpnKeyOff
import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -42,7 +43,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
@ -64,7 +65,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) { fun AutoTunnelScreen(settings: Settings, viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
@ -103,7 +104,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
} }
} }
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) { LaunchedEffect(settings.trustedNetworkSSIDs) {
currentText = "" currentText = ""
} }
@ -154,15 +155,15 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled, checked = settings.isTunnelOnWifiEnabled,
onClick = { onClick = {
viewModel.onToggleTunnelOnWifi() viewModel.onToggleTunnelOnWifi(settings)
}, },
) )
}, },
onClick = { onClick = {
viewModel.onToggleTunnelOnWifi() viewModel.onToggleTunnelOnWifi(settings)
}, },
), ),
SelectionItem( SelectionItem(
@ -181,19 +182,19 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
checked = uiState.settings.isWifiNameByShellEnabled, checked = settings.isWifiNameByShellEnabled,
onClick = { onClick = {
viewModel.onRootShellWifiToggle() viewModel.onRootShellWifiToggle(settings)
}, },
) )
}, },
onClick = { onClick = {
viewModel.onRootShellWifiToggle() viewModel.onRootShellWifiToggle(settings)
}, },
), ),
), ),
) )
if (uiState.settings.isTunnelOnWifiEnabled) { if (settings.isTunnelOnWifiEnabled) {
addAll( addAll(
listOf( listOf(
SelectionItem( SelectionItem(
@ -209,14 +210,14 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
checked = uiState.settings.isWildcardsEnabled, checked = settings.isWildcardsEnabled,
onClick = { onClick = {
viewModel.onToggleWildcards() viewModel.onToggleWildcards(settings)
}, },
) )
}, },
onClick = { onClick = {
viewModel.onToggleWildcards() viewModel.onToggleWildcards(settings)
}, },
), ),
SelectionItem( SelectionItem(
@ -255,21 +256,44 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
description = { description = {
TrustedNetworkTextBox( TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs, settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID, onDelete = { viewModel.onDeleteTrustedSSID(it, settings) },
currentText = currentText, currentText = currentText,
onSave = { ssid -> onSave = { ssid ->
if (uiState.settings.isWifiNameByShellEnabled || isWifiNameReadable()) viewModel.onSaveTrustedSSID(ssid) if (settings.isWifiNameByShellEnabled || isWifiNameReadable()) viewModel.onSaveTrustedSSID(ssid, settings)
}, },
onValueChange = { currentText = it }, onValueChange = { currentText = it },
supporting = { supporting = {
if (uiState.settings.isWildcardsEnabled) { if (settings.isWildcardsEnabled) {
WildcardsLabel() WildcardsLabel()
} }
}, },
) )
}, },
), ),
SelectionItem(
Icons.Outlined.VpnKeyOff,
title = {
Text(
stringResource(R.string.kill_switch_off),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
},
trailing = {
ScaledSwitch(
enabled = settings.isVpnKillSwitchEnabled,
checked = settings.isDisableKillSwitchOnTrustedEnabled,
onClick = {
viewModel.onToggleStopKillSwitchOnTrusted(settings)
},
)
},
onClick = {
viewModel.onToggleStopKillSwitchOnTrusted(settings)
},
),
), ),
) )
} }
@ -287,13 +311,13 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled, checked = settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() }, onClick = { viewModel.onToggleTunnelOnMobileData(settings) },
) )
}, },
onClick = { onClick = {
viewModel.onToggleTunnelOnMobileData() viewModel.onToggleTunnelOnMobileData(settings)
}, },
), ),
SelectionItem( SelectionItem(
@ -306,13 +330,13 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled, checked = settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() }, onClick = { viewModel.onToggleTunnelOnEthernet(settings) },
) )
}, },
onClick = { onClick = {
viewModel.onToggleTunnelOnEthernet() viewModel.onToggleTunnelOnEthernet(settings)
}, },
), ),
SelectionItem( SelectionItem(
@ -331,12 +355,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
}, },
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
checked = uiState.settings.isStopOnNoInternetEnabled, checked = settings.isStopOnNoInternetEnabled,
onClick = { viewModel.onToggleStopOnNoInternet() }, onClick = { viewModel.onToggleStopOnNoInternet(settings) },
) )
}, },
onClick = { onClick = {
viewModel.onToggleStopOnNoInternet() viewModel.onToggleStopOnNoInternet(settings)
}, },
), ),
), ),

View File

@ -12,8 +12,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -28,11 +26,8 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow() fun onToggleTunnelOnWifi(settings: Settings) = viewModelScope.launch {
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings()) with(settings) {
fun onToggleTunnelOnWifi() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled, isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
@ -41,8 +36,8 @@ constructor(
} }
} }
fun onToggleTunnelOnMobileData() = viewModelScope.launch { fun onToggleTunnelOnMobileData(settings: Settings) = viewModelScope.launch {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled, isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled,
@ -51,8 +46,8 @@ constructor(
} }
} }
fun onToggleWildcards() = viewModelScope.launch { fun onToggleWildcards(settings: Settings) = viewModelScope.launch {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isWildcardsEnabled = !isWildcardsEnabled, isWildcardsEnabled = !isWildcardsEnabled,
@ -61,8 +56,8 @@ constructor(
} }
} }
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch { fun onDeleteTrustedSSID(ssid: String, settings: Settings) = viewModelScope.launch {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(), trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(),
@ -71,9 +66,9 @@ constructor(
} }
} }
fun onRootShellWifiToggle() = viewModelScope.launch { fun onRootShellWifiToggle(settings: Settings) = viewModelScope.launch {
requestRoot().onSuccess { requestRoot().onSuccess {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled), copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled),
) )
@ -92,8 +87,8 @@ constructor(
} }
} }
fun onToggleTunnelOnEthernet() = viewModelScope.launch { fun onToggleTunnelOnEthernet(settings: Settings) = viewModelScope.launch {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy( copy(
isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled, isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
@ -102,13 +97,16 @@ constructor(
} }
} }
fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch { fun onSaveTrustedSSID(ssid: String, settings: Settings) = viewModelScope.launch {
if (ssid.isEmpty()) return@launch if (ssid.isEmpty()) return@launch
val trimmed = ssid.trim() val trimmed = ssid.trim()
with(settings.value) { with(settings) {
if (!trustedNetworkSSIDs.contains(trimmed)) { if (!trustedNetworkSSIDs.contains(trimmed)) {
this.trustedNetworkSSIDs.add(ssid) appDataRepository.settings.save(
appDataRepository.settings.save(this) settings.copy(
trustedNetworkSSIDs = (trustedNetworkSSIDs + ssid).toMutableList(),
),
)
} else { } else {
SnackbarController.showMessage( SnackbarController.showMessage(
StringValue.StringResource( StringValue.StringResource(
@ -119,11 +117,19 @@ constructor(
} }
} }
fun onToggleStopOnNoInternet() = viewModelScope.launch { fun onToggleStopOnNoInternet(settings: Settings) = viewModelScope.launch {
with(settings.value) { with(settings) {
appDataRepository.settings.save( appDataRepository.settings.save(
copy(isStopOnNoInternetEnabled = !isStopOnNoInternetEnabled), copy(isStopOnNoInternetEnabled = !isStopOnNoInternetEnabled),
) )
} }
} }
fun onToggleStopKillSwitchOnTrusted(settings: Settings) = viewModelScope.launch {
with(settings) {
appDataRepository.settings.save(
copy(isDisableKillSwitchOnTrustedEnabled = !isDisableKillSwitchOnTrustedEnabled),
)
}
}
} }

View File

@ -18,9 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
@ -29,7 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
@ -38,21 +36,11 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun AdvancedScreen(appUiState: AppUiState, appViewModel: AppViewModel) { fun AdvancedScreen(settings: Settings, appViewModel: AppViewModel) {
var isDropDownExpanded by remember { var isDropDownExpanded by remember {
mutableStateOf(false) mutableStateOf(false)
} }
var selected by remember { mutableIntStateOf(appUiState.settings.debounceDelaySeconds) }
LaunchedEffect(selected) {
if (selected == appUiState.settings.debounceDelaySeconds) return@LaunchedEffect
appViewModel.saveSettings(appUiState.settings.copy(debounceDelaySeconds = selected))
if (appUiState.settings.isAutoTunnelEnabled) {
appViewModel.bounceAutoTunnel()
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopNavBar(stringResource(R.string.advanced_settings)) TopNavBar(stringResource(R.string.advanced_settings))
@ -87,7 +75,7 @@ fun AdvancedScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = selected.toString(), style = MaterialTheme.typography.bodyMedium) Text(text = settings.debounceDelaySeconds.toString(), style = MaterialTheme.typography.bodyMedium)
val icon = Icons.Default.ArrowDropDown val icon = Icons.Default.ArrowDropDown
Icon(icon, icon.name) Icon(icon, icon.name)
} }
@ -107,7 +95,9 @@ fun AdvancedScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
}, },
onClick = { onClick = {
isDropDownExpanded = false isDropDownExpanded = false
selected = num appViewModel.saveSettings(
settings.copy(debounceDelaySeconds = num),
)
}, },
) )
} }

View File

@ -205,4 +205,5 @@
<string name="error_tunnel_start">Failed to starting tunnel</string> <string name="error_tunnel_start">Failed to starting tunnel</string>
<string name="tunnel_control">Tunnel control</string> <string name="tunnel_control">Tunnel control</string>
<string name="auto_tunnel">Auto-tunnel</string> <string name="auto_tunnel">Auto-tunnel</string>
<string name="kill_switch_off">Stop kill switch on trusted</string>
</resources> </resources>