refactor: disbale unsupported features on mode changes, copy app version

closes #930
This commit is contained in:
Zane Schepke
2025-09-08 21:28:10 -04:00
parent 82b39695de
commit 5bd497c8bb
14 changed files with 211 additions and 92 deletions
@@ -12,6 +12,8 @@ import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
@@ -20,8 +22,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.amnezia.awg.crypto.Key
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
abstract class BaseTunnel(
@ApplicationScope protected val applicationScope: CoroutineScope,
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
data class SelectionItem(
@@ -11,5 +12,9 @@ data class SelectionItem(
val title: (@Composable () -> Unit),
val description: (@Composable () -> Unit)? = null,
val onClick: (() -> Unit)? = null,
val modifier: Modifier = Modifier.height(64.dp),
val isEnabled: Boolean = true,
val disabledReason: String? = null,
val modifier: Modifier = Modifier.height(48.dp),
val padding: Dp = if (description == null && disabledReason == null) 16.dp else 6.dp,
val onLongPress: (() -> Unit)? = null,
)
@@ -1,65 +1,113 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>, modifier: Modifier = Modifier) {
if (items.isEmpty()) return
val context = LocalContext.current
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.map { item ->
Box(
contentAlignment = Alignment.Center,
modifier =
modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.then(item.onClick?.let { modifier.clickable { it() } } ?: modifier),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.fillMaxWidth(),
) {
items.forEach { item ->
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.alpha(if (item.isEnabled) 1f else 0.6f)
.semantics {
if (!item.isEnabled) {
stateDescription =
item.disabledReason ?: context.getString(R.string.disabled)
}
}
.combinedClickable(
onClick = { item.onClick?.invoke() },
onLongClick = { item.onLongPress?.invoke() },
enabled = true,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(4f, false).fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
) {
item.leading?.invoke()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement =
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth()
.padding(start = if (item.leading != null) 16.dp else 0.dp)
.weight(1f)
.padding(
vertical = if (item.description == null) 16.dp else 6.dp
),
Modifier.weight(4f, false)
.fillMaxWidth()
.padding(vertical = item.padding),
) {
item.title()
item.description?.let { it() }
item.leading?.let {
Box(modifier = Modifier.alpha(if (item.isEnabled) 1f else 0.6f)) {
it()
}
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement =
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier =
Modifier.fillMaxWidth()
.padding(start = if (item.leading != null) 16.dp else 0.dp)
.weight(1f),
) {
Box(modifier = Modifier.alpha(if (item.isEnabled) 1f else 0.6f)) {
item.title()
}
item.description?.invoke()
if (!item.isEnabled && item.disabledReason != null) {
Text(
text = item.disabledReason,
style = MaterialTheme.typography.bodySmall,
color =
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
)
}
}
}
}
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.padding(start = 16.dp),
) {
it()
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier =
Modifier.padding(start = 16.dp)
.alpha(if (item.isEnabled) 1f else 0.6f)
.run {
if (!item.isEnabled) {
semantics {
stateDescription =
context.getString(R.string.disabled)
}
} else {
this
}
},
) {
it()
}
}
}
}
@@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
@@ -27,6 +28,8 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents.AppModeBottomSheet
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
import xyz.teamgravity.pin_lock_compose.PinManager
@@ -45,6 +48,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
var showBackupSheet by rememberSaveable { mutableStateOf(false) }
var showAppModeSheet by rememberSaveable { mutableStateOf(false) }
val appMode by
rememberSaveable(settingsState.settings.appMode) {
mutableStateOf(settingsState.settings.appMode)
}
if (!settingsState.stateInitialized) return
sharedViewModel.collectSideEffect { sideEffect ->
@@ -91,40 +99,62 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
),
) {
SurfaceSelectionGroupButton(
buildList {
add(appModeItem(settingsState.settings.appMode) { showAppModeSheet = true })
}
listOf(appModeItem(settingsState.settings.appMode) { showAppModeSheet = true })
)
SectionDivider()
SurfaceSelectionGroupButton(
items =
buildList {
if (settingsState.settings.appMode == AppMode.LOCK_DOWN) {
if (appMode == AppMode.LOCK_DOWN) {
add(
lanTrafficItem(settingsState.settings.isLanOnKillSwitchEnabled) {
viewModel.setLanKillSwitchEnabled(it)
}
)
}
add(tunnelMonitoringItem(navController))
add(dnsSettingsItem(navController))
// TODO changing these settings won't work in certain app states
if (showProxySettings) add(proxYSettingsItem(navController))
add(
tunnelMonitoringItem(
appMode,
onClick = { navController.navigate(Route.TunnelMonitoring) },
) {
sharedViewModel.showSnackMessage(
StringValue.StringResource(
R.string.mode_disabled_template,
appMode.asString(context),
)
)
}
)
add(
dnsSettingsItem(appMode, onClick = { navController.navigate(Route.Dns) }) {
sharedViewModel.showSnackMessage(
StringValue.StringResource(
R.string.mode_disabled_template,
appMode.asString(context),
)
)
}
)
if (showProxySettings)
add(proxYSettingsItem() { navController.navigate(Route.ProxySettings) })
}
)
SectionDivider()
SurfaceSelectionGroupButton(listOf(systemFeaturesItem(navController)))
SurfaceSelectionGroupButton(
listOf(systemFeaturesItem() { navController.navigate(Route.SystemFeatures) })
)
SectionDivider()
SurfaceSelectionGroupButton(
items =
buildList {
add(appearanceItem(navController))
add(appearanceItem() { navController.navigate(Route.Appearance) })
add(
localLoggingItem(settingsState.isLocalLoggingEnabled) {
viewModel.setLocalLogging(it)
}
)
if (settingsState.isLocalLoggingEnabled) add(readLogsItem(navController))
if (settingsState.isLocalLoggingEnabled)
add(readLogsItem() { navController.navigate(Route.Logs) })
add(
pinLockItem(settingsState.isPinLockEnabled) { enabled ->
if (enabled) {
@@ -7,14 +7,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun appearanceItem(navController: NavController): SelectionItem {
fun appearanceItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.AutoMirrored.Outlined.ViewQuilt, contentDescription = null) },
title = {
@@ -24,7 +22,7 @@ fun appearanceItem(navController: NavController): SelectionItem {
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = { ForwardButton { navController.navigate(Route.Appearance) } },
onClick = { navController.navigate(Route.Appearance) },
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -6,18 +6,29 @@ import androidx.compose.material3.Icon
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.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
@Composable
fun dnsSettingsItem(navController: NavController): SelectionItem {
fun dnsSettingsItem(
appMode: AppMode,
onClick: () -> Unit,
onDisabledClick: () -> Unit,
): SelectionItem {
val context = LocalContext.current
val enabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val click = if (enabled) onClick else onDisabledClick
return SelectionItem(
leading = { Icon(Icons.Outlined.Dns, null) },
trailing = { ForwardButton { navController.navigate(Route.Dns) } },
trailing = { ForwardButton { click() } },
title = {
Text(
text = stringResource(R.string.dns_settings),
@@ -25,6 +36,8 @@ fun dnsSettingsItem(navController: NavController): SelectionItem {
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { navController.navigate(Route.Dns) },
onClick = click,
disabledReason =
context.getString(R.string.mode_disabled_template, appMode.asString(context)),
)
}
@@ -7,17 +7,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun proxYSettingsItem(navController: NavController): SelectionItem {
fun proxYSettingsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
trailing = { ForwardButton { navController.navigate(Route.ProxySettings) } },
trailing = { ForwardButton { onClick() } },
title = {
Text(
text = stringResource(R.string.proxy_settings),
@@ -25,6 +23,6 @@ fun proxYSettingsItem(navController: NavController): SelectionItem {
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { navController.navigate(Route.ProxySettings) },
onClick = onClick,
)
}
@@ -5,22 +5,20 @@ import androidx.compose.material.icons.filled.ViewTimeline
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun readLogsItem(navController: NavController): SelectionItem {
fun readLogsItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Filled.ViewTimeline, contentDescription = null) },
title = {
SelectionItemLabel(stringResource(R.string.read_logs), SelectionLabelType.TITLE)
},
trailing = { ForwardButton { navController.navigate(Route.Logs) } },
onClick = { navController.navigate(Route.Logs) },
trailing = { ForwardButton { onClick() } },
onClick = onClick,
)
}
@@ -7,17 +7,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun systemFeaturesItem(navController: NavController): SelectionItem {
fun systemFeaturesItem(onClick: () -> Unit): SelectionItem {
return SelectionItem(
leading = { Icon(Icons.Outlined.Android, null) },
trailing = { ForwardButton { navController.navigate(Route.SystemFeatures) } },
trailing = { ForwardButton { onClick.invoke() } },
title = {
Text(
text = stringResource(R.string.system_features),
@@ -25,6 +23,6 @@ fun systemFeaturesItem(navController: NavController): SelectionItem {
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { navController.navigate(Route.SystemFeatures) },
onClick = onClick,
)
}
@@ -6,18 +6,32 @@ import androidx.compose.material3.Icon
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.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
@Composable
fun tunnelMonitoringItem(navController: NavController): SelectionItem {
fun tunnelMonitoringItem(
appMode: AppMode,
onClick: () -> Unit,
onDisabledClick: () -> Unit,
): SelectionItem {
val context = LocalContext.current
val enabled by
rememberSaveable(appMode) {
mutableStateOf(appMode == AppMode.VPN || appMode == AppMode.KERNEL)
}
val click = if (enabled) onClick else onDisabledClick
return SelectionItem(
leading = { Icon(Icons.Outlined.MonitorHeart, null) },
trailing = { ForwardButton { navController.navigate(Route.TunnelMonitoring) } },
trailing = { ForwardButton { click() } },
title = {
Text(
text = stringResource(R.string.tunnel_monitoring),
@@ -25,6 +39,9 @@ fun tunnelMonitoringItem(navController: NavController): SelectionItem {
MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { navController.navigate(Route.TunnelMonitoring) },
isEnabled = enabled,
onClick = click,
disabledReason =
context.getString(R.string.mode_disabled_template, appMode.asString(context)),
)
}
@@ -134,7 +134,7 @@ fun SupportScreen(viewModel: SupportViewModel) {
stringResource(R.string.thank_you),
modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 12.dp),
)
UpdateSection(onUpdateCheck = { viewModel.checkForUpdate() })
UpdateSection() { viewModel.checkForUpdate() }
SectionDivider()
GeneralSupportOptions(navController)
SectionDivider()
@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudDownload
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
@@ -12,9 +13,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
@Composable
fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
fun UpdateSection(onUpdateCheck: () -> Unit) {
val clipboardManager = rememberClipboardHelper()
val version = remember {
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }"
}
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
@@ -28,11 +35,7 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
description = {
Column {
SelectionItemLabel(
stringResource(
R.string.version_template,
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }",
),
stringResource(R.string.version_template, version),
SelectionLabelType.DESCRIPTION,
)
SelectionItemLabel(
@@ -42,6 +45,7 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
}
},
onClick = onUpdateCheck,
onLongPress = { clipboardManager.copy(version) },
)
)
)
@@ -70,13 +70,20 @@ fun WifiDetectionMethod.asDescriptionString(context: Context): String? {
}
fun AppMode.asTitleString(context: Context): String {
return when (this) {
AppMode.VPN -> asString(context)
AppMode.PROXY -> context.getString(R.string.expiremental_template, asString(context))
AppMode.KERNEL -> context.getString(R.string.root_required_template, asString(context))
AppMode.LOCK_DOWN -> context.getString(R.string.expiremental_template, asString(context))
}
}
fun AppMode.asString(context: Context): String {
return when (this) {
AppMode.VPN -> context.getString(R.string.vpn)
AppMode.PROXY ->
context.getString(R.string.expiremental_template, context.getString(R.string.proxy))
AppMode.KERNEL ->
context.getString(R.string.root_required_template, context.getString(R.string.kernel))
AppMode.LOCK_DOWN -> context.getString(R.string.expiremental_template, "Lockdown")
AppMode.PROXY -> context.getString(R.string.proxy)
AppMode.KERNEL -> context.getString(R.string.kernel)
AppMode.LOCK_DOWN -> context.getString(R.string.lockdown)
}
}
+4 -1
View File
@@ -291,7 +291,7 @@
<string name="move_down">Move Down</string>
<string name="error_tunnel_name">Tunnel name must be 15 characters or fewer in kernel mode</string>
<string name="tunnel">tunnel</string>
<string name="tunnel_item_description">1$s, %2$s, %3$s</string>
<string name="tunnel_item_description">%1$s, %2$s, %3$s</string>
<string name="warning">Warning</string>
<string name="location_permissions">Location Permissions</string>
<string name="location_justification">In order to read Wi-Fi names in the background with your current detection
@@ -386,4 +386,7 @@
<string name="ddns_auto_update_description">Auto-updates IP on DDNS changes</string>
<string name="prefer_ipv6_resolution">Prefer IPv6 peer resolution</string>
<string name="ipv4_auto_fallback">IPv4 peer auto-fallback</string>
<string name="disabled">Disabled</string>
<string name="mode_disabled_template">Feature unavailable in %1$s mode.</string>
<string name="lockdown">Lockdown</string>
</resources>