fix: add back per tunnel ping settings

This commit is contained in:
Zane Schepke 2024-11-17 01:30:13 -05:00
parent 72bf0a1979
commit 777a948244
7 changed files with 279 additions and 214 deletions

View File

@ -2,13 +2,19 @@ package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
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.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -21,6 +27,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -32,6 +39,7 @@ constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>, private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
) : ViewModel() { ) : ViewModel() {
@ -107,4 +115,79 @@ constructor(
fun setLocationDisclosureShown() = viewModelScope.launch { fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true) appDataRepository.appState.setLocationDisclosureShown(true)
} }
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(uiState.value.settings) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
} }

View File

@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -10,7 +11,6 @@ import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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
@ -20,10 +20,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import com.zaneschepke.wireguardautotunnel.R import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SubmitConfigurationTextBox( fun SubmitConfigurationTextBox(
@ -44,17 +45,22 @@ fun SubmitConfigurationTextBox(
var stateValue by remember { mutableStateOf(value ?: "") } var stateValue by remember { mutableStateOf(value ?: "") }
OutlinedTextField( CustomTextField(
isError = isErrorValue(stateValue), isError = isErrorValue(stateValue),
modifier = Modifier textStyle = MaterialTheme.typography.bodySmall,
.fillMaxWidth(),
value = stateValue, value = stateValue,
singleLine = true,
interactionSource = interactionSource,
onValueChange = { stateValue = it }, onValueChange = { stateValue = it },
interactionSource = interactionSource,
label = { Text(label) }, label = { Text(label) },
maxLines = 1, containerColor = MaterialTheme.colorScheme.surface,
placeholder = { Text(hint) }, placeholder = { Text(hint, style = MaterialTheme.typography.bodySmall) },
modifier =
Modifier
.padding(
top = 5.dp,
bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
singleLine = true,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { onDone = {
@ -62,16 +68,17 @@ fun SubmitConfigurationTextBox(
keyboardController?.hide() keyboardController?.hide()
}, },
), ),
trailingIcon = { trailing = {
if (!isErrorValue(stateValue) && isFocused) { if (!isErrorValue(stateValue) && isFocused) {
IconButton(onClick = { IconButton(onClick = {
onSubmit(stateValue) onSubmit(stateValue)
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
}) { }) {
val icon = Icons.Outlined.Save
Icon( Icon(
imageVector = Icons.Outlined.Save, imageVector = icon,
contentDescription = stringResource(R.string.save_changes), contentDescription = icon.name,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }

View File

@ -10,7 +10,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
@ -37,8 +36,8 @@ fun CustomTextField(
isError: Boolean = false, isError: Boolean = false,
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
interactionSource: MutableInteractionSource,
) { ) {
val interactionSource = remember { MutableInteractionSource() }
val space = " " val space = " "
BasicTextField( BasicTextField(
value = value, value = value,

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
@ -30,6 +31,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@ -38,12 +41,15 @@ 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
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -88,112 +94,175 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
) { ) {
GroupLabel(stringResource(R.string.auto_tunneling)) GroupLabel(stringResource(R.string.auto_tunneling))
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
listOf( buildList {
SelectionItem( addAll(
Icons.Outlined.Star, listOf(
title = { SelectionItem(
Text( Icons.Outlined.Star,
stringResource(R.string.primary_tunnel), title = {
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), Text(
) stringResource(R.string.primary_tunnel),
}, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
description = { )
Text( },
stringResource(R.string.set_primary_tunnel), description = {
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), Text(
) stringResource(R.string.set_primary_tunnel),
}, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
trailing = { )
ScaledSwitch( },
config.isPrimaryTunnel, trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
) ),
}, SelectionItem(
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }, Icons.Outlined.PhoneAndroid,
), title = {
SelectionItem( Text(
Icons.Outlined.PhoneAndroid, stringResource(R.string.mobile_tunnel),
title = { Text(stringResource(R.string.mobile_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
description = { )
Text( },
stringResource(R.string.mobile_data_tunnel), description = {
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), Text(
) stringResource(R.string.mobile_data_tunnel),
}, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
trailing = { )
ScaledSwitch( },
config.isMobileDataTunnel, trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
) ),
}, SelectionItem(
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }, Icons.Outlined.NetworkPing,
), title = {
SelectionItem( Text(
Icons.Outlined.NetworkPing, stringResource(R.string.restart_on_ping),
title = { style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
Text( )
stringResource(R.string.restart_on_ping), },
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), trailing = {
) ScaledSwitch(
}, checked = config.isPingEnabled,
trailing = { onClick = { optionsViewModel.onToggleRestartOnPing(config) },
ScaledSwitch( )
checked = config.isPingEnabled, },
onClick = { optionsViewModel.onToggleRestartOnPing(config) }, onClick = { optionsViewModel.onToggleRestartOnPing(config) },
) ),
}, ),
onClick = { optionsViewModel.onToggleRestartOnPing(config) }, )
), if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
SelectionItem( add(
title = { SelectionItem(
Row( title = {},
verticalAlignment = Alignment.CenterVertically, description = {
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), SubmitConfigurationTextBox(
) { config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
add(
SelectionItem(
title = {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
.weight(4f, false)
.fillMaxWidth(),
) { ) {
val icon = Icons.Outlined.Security Row(
Icon( verticalAlignment = Alignment.CenterVertically,
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .weight(4f, false)
.padding(start = 16.dp.scaledWidth()) .fillMaxWidth(),
.padding(vertical = 6.dp.scaledHeight()),
) { ) {
Text( val icon = Icons.Outlined.Security
stringResource(R.string.use_tunnel_on_wifi_name), Icon(
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), icon,
icon.name,
modifier = Modifier.size(iconSize),
) )
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.use_tunnel_on_wifi_name),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
} }
} }
} },
}, description = {
description = { TrustedNetworkTextBox(
TrustedNetworkTextBox( config.tunnelNetworks,
config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, currentText = currentText,
currentText = currentText, onSave = { optionsViewModel.onSaveRunSSID(it, config) },
onSave = { optionsViewModel.onSaveRunSSID(it, config) }, onValueChange = { currentText = it },
onValueChange = { currentText = it }, supporting = {
supporting = { if (appUiState.settings.isWildcardsEnabled) {
if (appUiState.settings.isWildcardsEnabled) { WildcardsLabel()
WildcardsLabel() }
} },
}, )
) },
}, ),
), )
), },
) )
} }
} }

View File

@ -150,7 +150,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
{ {
ScaledSwitch( ScaledSwitch(
uiState.settings.isShortcutsEnabled, uiState.settings.isShortcutsEnabled,
onClick = { viewModel.onToggleShortcutsEnabled() }, onClick = { appViewModel.onToggleShortcutsEnabled() },
) )
}, },
title = { title = {
@ -159,7 +159,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { viewModel.onToggleShortcutsEnabled() }, onClick = { appViewModel.onToggleShortcutsEnabled() },
), ),
SelectionItem( SelectionItem(
Icons.Outlined.VpnLock, Icons.Outlined.VpnLock,
@ -173,7 +173,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) && ) &&
uiState.settings.isAutoTunnelEnabled uiState.settings.isAutoTunnelEnabled
), ),
onClick = { viewModel.onToggleAlwaysOnVPN() }, onClick = { appViewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled, checked = uiState.settings.isAlwaysOnVpnEnabled,
) )
}, },
@ -183,7 +183,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { viewModel.onToggleAlwaysOnVPN() }, onClick = { appViewModel.onToggleAlwaysOnVPN() },
), ),
SelectionItem( SelectionItem(
Icons.Outlined.AdminPanelSettings, Icons.Outlined.AdminPanelSettings,
@ -209,7 +209,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
{ {
ScaledSwitch( ScaledSwitch(
uiState.settings.isRestoreOnBootEnabled, uiState.settings.isRestoreOnBootEnabled,
onClick = { viewModel.onToggleRestartAtBoot() }, onClick = { appViewModel.onToggleRestartAtBoot() },
) )
}, },
title = { title = {
@ -218,7 +218,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
) )
}, },
onClick = { viewModel.onToggleRestartAtBoot() }, onClick = { appViewModel.onToggleRestartAtBoot() },
), ),
) )
}, },
@ -294,7 +294,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
trailing = { trailing = {
ScaledSwitch( ScaledSwitch(
uiState.settings.isKernelEnabled, uiState.settings.isKernelEnabled,
onClick = { viewModel.onToggleKernelMode() }, onClick = { appViewModel.onToggleKernelMode() },
enabled = !( enabled = !(
uiState.settings.isAutoTunnelEnabled || uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled || uiState.settings.isAlwaysOnVpnEnabled ||
@ -303,7 +303,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) )
}, },
onClick = { onClick = {
viewModel.onToggleKernelMode() appViewModel.onToggleKernelMode()
}, },
), ),
), ),

View File

@ -4,119 +4,23 @@ import android.content.Context
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
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 java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@AppShell private val rootShell: Provider<RootShell>,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(settings.value) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun exportAllConfigs(context: Context) = viewModelScope.launch { fun exportAllConfigs(context: Context) = viewModelScope.launch {
kotlin.runCatching { kotlin.runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip") val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")

View File

@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -16,6 +17,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -66,6 +68,7 @@ fun TrustedNetworkTextBox(
textStyle = MaterialTheme.typography.bodySmall, textStyle = MaterialTheme.typography.bodySmall,
value = currentText, value = currentText,
onValueChange = onValueChange, onValueChange = onValueChange,
interactionSource = remember { MutableInteractionSource() },
label = { Text(stringResource(R.string.add_wifi_name)) }, label = { Text(stringResource(R.string.add_wifi_name)) },
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting, supportingText = supporting,