From 6d483459a65190d52da22dac50be00941a46f2bc Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sat, 1 Nov 2025 00:06:40 -0400 Subject: [PATCH] refactor: add restart to lockdown on config change --- .../ui/common/dialog/InfoDialog.kt | 15 ++-- .../ui/common/dialog/VpnDeniedDialog.kt | 4 +- .../currentBackStackEntryAsNavbarState.kt | 10 +++ .../autotunnel/wifi/WifiSettingsScreen.kt | 4 +- .../lockdown/LockdownSettingsScreen.kt | 73 +++++++++++++------ .../support/components/PermissionDialog.kt | 4 +- .../support/components/UpdateDialog.kt | 12 +-- .../ui/screens/tunnels/TunnelsScreen.kt | 5 +- .../ui/state/LockdownSettingsUiState.kt | 1 + .../viewmodel/LockdownViewModel.kt | 42 ++++++++--- app/src/main/res/values/strings.xml | 6 ++ 11 files changed, 124 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt index 47600ab3..01a1e035 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/InfoDialog.kt @@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.dialog import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties @@ -11,22 +12,26 @@ import com.zaneschepke.wireguardautotunnel.R fun InfoDialog( onAttest: () -> Unit, onDismiss: () -> Unit, - title: @Composable () -> Unit, - body: @Composable () -> Unit, - confirmText: @Composable () -> Unit, + title: String, + body: @Composable (() -> Unit), + confirmText: String, + modifier: Modifier = Modifier, ) { MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy()) { Surface(color = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) { AlertDialog( + modifier = modifier, onDismissRequest = { onDismiss() }, - confirmButton = { TextButton(onClick = { onAttest() }) { confirmText() } }, + confirmButton = { + TextButton(onClick = { onAttest() }) { Text(text = confirmText) } + }, dismissButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(R.string.cancel)) } }, containerColor = MaterialTheme.colorScheme.surface, - title = { title() }, + title = { Text(text = title) }, text = { body() }, properties = DialogProperties(usePlatformDefaultWidth = true), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt index 235417d3..5c6f1553 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/dialog/VpnDeniedDialog.kt @@ -34,7 +34,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) { InfoDialog( onDismiss = { onDismiss() }, onAttest = { onDismiss() }, - title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) }, + title = stringResource(R.string.vpn_denied_dialog_title), body = { Text( text = alwaysOnDescription, @@ -44,7 +44,7 @@ fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) { ), ) }, - confirmText = { Text(text = stringResource(R.string.okay)) }, + confirmText = stringResource(R.string.okay), ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index 56c78707..63efe81c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -125,6 +125,16 @@ fun currentRouteAsNavbarState( }, showBottomItems = true, topTitle = context.getString(R.string.lockdown_settings), + topTrailing = { + IconButton( + onClick = { + keyboardController?.hide() + sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges) + } + ) { + Icon(Icons.Rounded.Save, stringResource(R.string.save)) + } + }, ) License -> NavbarState( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/wifi/WifiSettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/wifi/WifiSettingsScreen.kt index 2dbc219d..fe747100 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/wifi/WifiSettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/wifi/WifiSettingsScreen.kt @@ -88,9 +88,9 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) { showLocationDialog = false }, onDismiss = { showLocationDialog = false }, - title = { Text(stringResource(R.string.location_permissions)) }, + title = stringResource(R.string.location_permissions), body = { Text(stringResource(R.string.location_justification)) }, - confirmText = { Text(stringResource(R.string.open_settings)) }, + confirmText = stringResource(R.string.open_settings), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt index 83042596..10253a25 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/lockdown/LockdownSettingsScreen.kt @@ -14,6 +14,9 @@ import androidx.compose.material3.MaterialTheme 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -23,18 +26,58 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch +import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel +import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText +import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) { + val sharedViewModel = LocalSharedVm.current + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + var metered by remember { mutableStateOf(uiState.lockdownSettings.metered) } + var dualStack by remember { mutableStateOf(uiState.lockdownSettings.dualStack) } + var bypassLan by remember { mutableStateOf(uiState.lockdownSettings.bypassLan) } + + sharedViewModel.collectSideEffect { + if (it is LocalSideEffect.SaveChanges) viewModel.setShowSaveModal(true) + } + if (uiState.isLoading) return + if (uiState.showSaveModal) { + InfoDialog( + onDismiss = { viewModel.setShowSaveModal(false) }, + onAttest = { + viewModel.setLockdownSettings( + uiState.lockdownSettings.copy( + metered = metered, + dualStack = dualStack, + bypassLan = bypassLan, + ) + ) + }, + title = stringResource(R.string.save_changes), + body = { + Text( + stringResource( + R.string.restart_message_template, + stringResource(R.string.kill_switch), + ) + ) + }, + confirmText = stringResource(R.string._continue), + ) + } + Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), @@ -57,37 +100,23 @@ fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) { ), ) }, - trailing = { - ThemedSwitch( - checked = uiState.lockdownSettings.bypassLan, - onClick = { viewModel.setBypassLan(it) }, - ) - }, - onClick = { viewModel.setBypassLan(!uiState.lockdownSettings.bypassLan) }, + trailing = { ThemedSwitch(checked = bypassLan, onClick = { bypassLan = it }) }, + onClick = { bypassLan = !bypassLan }, ) SurfaceRow( leading = { Icon(Icons.Outlined.DataUsage, contentDescription = null) }, title = stringResource(R.string.metered_tunnel), - trailing = { - ThemedSwitch( - checked = uiState.lockdownSettings.metered, - onClick = { viewModel.setMetered(it) }, - ) - }, - onClick = { viewModel.setMetered(!uiState.lockdownSettings.metered) }, + trailing = { ThemedSwitch(checked = metered, onClick = { metered = it }) }, + onClick = { metered = !metered }, ) SurfaceRow( leading = { Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null) }, - title = "Dual-stack", - trailing = { - ThemedSwitch( - checked = uiState.lockdownSettings.dualStack, - onClick = { viewModel.setDualStack(it) }, - ) - }, - onClick = { viewModel.setDualStack(!uiState.lockdownSettings.dualStack) }, + title = stringResource(R.string.dual_stack), + description = { DescriptionText(stringResource(R.string.dual_stack_description)) }, + trailing = { ThemedSwitch(checked = dualStack, onClick = { dualStack = it }) }, + onClick = { dualStack = !dualStack }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/PermissionDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/PermissionDialog.kt index 54f8e30a..c197f3e2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/PermissionDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/PermissionDialog.kt @@ -16,8 +16,8 @@ fun PermissionDialog(context: Context, onDismiss: () -> Unit) { context.requestInstallPackagesPermission() onDismiss() }, - title = { Text(stringResource(R.string.permission_required)) }, + title = stringResource(R.string.permission_required), body = { Text(stringResource(R.string.install_updated_permission)) }, - confirmText = { Text(stringResource(R.string.allow)) }, + confirmText = stringResource(R.string.allow), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/UpdateDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/UpdateDialog.kt index f2850b1f..d3a1de88 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/UpdateDialog.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/UpdateDialog.kt @@ -46,7 +46,7 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed onPermissionNeeded() } }, - title = { Text(stringResource(R.string.update_available)) }, + title = stringResource(R.string.update_available), body = { Column( horizontalAlignment = Alignment.Start, @@ -89,12 +89,8 @@ fun UpdateDialog(viewModel: SupportViewModel, context: Context, onPermissionNeed } } }, - confirmText = { - Text( - if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) - stringResource(R.string.download) - else stringResource(R.string.download_and_install) - ) - }, + confirmText = + if (BuildConfig.FLAVOR != Constants.STANDALONE_FLAVOR) stringResource(R.string.download) + else stringResource(R.string.download_and_install), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt index adce377b..740bb191 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/TunnelsScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.zaneschepke.wireguardautotunnel.R @@ -94,9 +95,9 @@ fun TunnelsScreen() { viewModel.deleteSelectedTunnels() showDeleteModal = false }, - title = { Text(text = stringResource(R.string.delete_tunnel)) }, + title = stringResource(R.string.delete_tunnel), body = { Text(text = stringResource(R.string.delete_tunnel_message)) }, - confirmText = { Text(text = stringResource(R.string.yes)) }, + confirmText = stringResource(R.string.yes), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/LockdownSettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/LockdownSettingsUiState.kt index fb751e36..876b81ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/LockdownSettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/state/LockdownSettingsUiState.kt @@ -5,4 +5,5 @@ import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings data class LockdownSettingsUiState( val lockdownSettings: LockdownSettings = LockdownSettings(), val isLoading: Boolean = true, + val showSaveModal: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt index 1c138660..68ac2c75 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/LockdownViewModel.kt @@ -1,8 +1,16 @@ package com.zaneschepke.wireguardautotunnel.viewmodel import androidx.lifecycle.ViewModel +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager +import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode +import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings +import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository +import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect import com.zaneschepke.wireguardautotunnel.ui.state.LockdownSettingsUiState +import com.zaneschepke.wireguardautotunnel.util.StringValue import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import org.orbitmvi.orbit.ContainerHost @@ -11,8 +19,11 @@ import org.orbitmvi.orbit.viewmodel.container @HiltViewModel class LockdownViewModel @Inject -constructor(private val lockdownSettingsRepository: LockdownSettingsRepository) : - ContainerHost, ViewModel() { +constructor( + private val lockdownSettingsRepository: LockdownSettingsRepository, + private val tunnelManager: TunnelManager, + private val globalEffectRepository: GlobalEffectRepository, +) : ContainerHost, ViewModel() { override val container = container( @@ -24,15 +35,28 @@ constructor(private val lockdownSettingsRepository: LockdownSettingsRepository) } } - fun setBypassLan(to: Boolean) = intent { - lockdownSettingsRepository.upsert(state.lockdownSettings.copy(bypassLan = to)) + fun setLockdownSettings(lockdownSettings: LockdownSettings) = intent { + reduce { state.copy(showSaveModal = false) } + lockdownSettingsRepository.upsert(lockdownSettings) + tunnelManager.setBackendMode(BackendMode.Inactive) + val allowedIps = + if (lockdownSettings.bypassLan) TunnelConfig.LAN_BYPASS_ALLOWED_IPS else emptySet() + tunnelManager.setBackendMode( + BackendMode.KillSwitch( + allowedIps = allowedIps, + isMetered = lockdownSettings.metered, + dualStack = lockdownSettings.dualStack, + ) + ) + postSideEffect(GlobalSideEffect.PopBackStack) + postSideEffect( + GlobalSideEffect.Toast(StringValue.StringResource(R.string.config_changes_saved)) + ) } - fun setMetered(to: Boolean) = intent { - lockdownSettingsRepository.upsert(state.lockdownSettings.copy(metered = to)) + suspend fun postSideEffect(globalSideEffect: GlobalSideEffect) { + globalEffectRepository.post(globalSideEffect) } - fun setDualStack(to: Boolean) = intent { - lockdownSettingsRepository.upsert(state.lockdownSettings.copy(dualStack = to)) - } + fun setShowSaveModal(to: Boolean) = intent { reduce { state.copy(showSaveModal = to) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2531551f..abbd41d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -422,4 +422,10 @@ Unavailable in current mode Global split tunneling Global DNS servers + Dual-stack + Tunnels must support IPv4 and IPv6 + Save changes + Saving changes will cause the %1$s to restart, do you wish to continue? + Continue + kill switch