refactor: add restart to lockdown on config change

This commit is contained in:
Zane Schepke
2025-11-01 00:06:40 -04:00
parent 7e4f055833
commit 6d483459a6
11 changed files with 124 additions and 52 deletions
@@ -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),
)
@@ -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),
)
}
}
@@ -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(
@@ -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),
)
}
@@ -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 },
)
}
}
@@ -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),
)
}
@@ -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),
)
}
@@ -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),
)
}
@@ -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,
)
@@ -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<LockdownSettingsUiState, Nothing>, ViewModel() {
constructor(
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelManager: TunnelManager,
private val globalEffectRepository: GlobalEffectRepository,
) : ContainerHost<LockdownSettingsUiState, Nothing>, ViewModel() {
override val container =
container<LockdownSettingsUiState, Nothing>(
@@ -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) } }
}
+6
View File
@@ -422,4 +422,10 @@
<string name="unavailable_in_mode">Unavailable in current mode</string>
<string name="global_split_tunneling">Global split tunneling</string>
<string name="global_dns_servers">Global DNS servers</string>
<string name="dual_stack">Dual-stack</string>
<string name="dual_stack_description">Tunnels must support IPv4 and IPv6</string>
<string name="save_changes">Save changes</string>
<string name="restart_message_template">Saving changes will cause the %1$s to restart, do you wish to continue?</string>
<string name="_continue">Continue</string>
<string name="kill_switch">kill switch</string>
</resources>