summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-09-17 15:34:41 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-09-17 15:34:41 +0200
commit95cd7d3e57fe08a93068c34d069d259c9d58dddd (patch)
treee1a608d11a51fa2246002695b5ecaed9c71f1da5 /android
parent196061b0c06c031d3e19b4bfa13268010e8a4e2c (diff)
downloadmullvadvpn-95cd7d3e57fe08a93068c34d069d259c9d58dddd.tar.xz
mullvadvpn-95cd7d3e57fe08a93068c34d069d259c9d58dddd.zip
Implement wireguard over shadowsocks
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/InformationComposeCell.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ObfuscationModeCell.kt117
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SelectableCell.kt36
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialog.kt78
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ShadowsocksCustomPortDialog.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt78
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectObfuscationCellPreviewParameterProvider.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt131
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt89
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt130
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt83
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt87
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt37
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt23
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt26
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt25
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt34
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt)3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt4
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ShadowsocksSettings.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt5
-rw-r--r--android/lib/resource/src/main/res/values-da/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-de/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-es/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-fi/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-fr/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-it/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ja/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ko/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-my/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-nb/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-nl/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-pl/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-pt/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ru/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-sv/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-th/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-tr/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-zh-rTW/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml4
-rw-r--r--android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt1
56 files changed, 990 insertions, 297 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/InformationComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/InformationComposeCell.kt
index 230f28a194..adfa64585c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/InformationComposeCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/InformationComposeCell.kt
@@ -41,6 +41,7 @@ fun InformationComposeCell(
background: Color = MaterialTheme.colorScheme.primary,
onCellClicked: () -> Unit = {},
onInfoClicked: (() -> Unit)? = null,
+ testTag: String = "",
) {
val titleModifier = Modifier.alpha(if (isEnabled) AlphaVisible else AlphaInactive)
val bodyViewModifier = Modifier
@@ -60,6 +61,7 @@ fun InformationComposeCell(
InformationComposeCellBody(modifier = bodyViewModifier, onInfoClicked = onInfoClicked)
},
onCellClicked = onCellClicked,
+ testTag = testTag,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ObfuscationModeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ObfuscationModeCell.kt
new file mode 100644
index 0000000000..495b9d61b3
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ObfuscationModeCell.kt
@@ -0,0 +1,117 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.preview.SelectObfuscationCellPreviewParameterProvider
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.selected
+import net.mullvad.mullvadvpn.lib.theme.typeface.listItemSubText
+import net.mullvad.mullvadvpn.lib.theme.typeface.listItemText
+
+@Preview
+@Composable
+private fun PreviewObfuscationCell(
+ @PreviewParameter(SelectObfuscationCellPreviewParameterProvider::class)
+ selectedObfuscationCellData: Triple<ObfuscationMode, Constraint<Port>, Boolean>
+) {
+ AppTheme {
+ ObfuscationModeCell(
+ obfuscationMode = selectedObfuscationCellData.first,
+ port = selectedObfuscationCellData.second,
+ isSelected = selectedObfuscationCellData.third,
+ onSelected = {},
+ onNavigate = {},
+ )
+ }
+}
+
+@Composable
+fun ObfuscationModeCell(
+ obfuscationMode: ObfuscationMode,
+ port: Constraint<Port>,
+ isSelected: Boolean,
+ onSelected: (ObfuscationMode) -> Unit,
+ onNavigate: () -> Unit = {},
+) {
+ Row(
+ modifier =
+ Modifier.height(IntrinsicSize.Min)
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceContainerLow)
+ ) {
+ TwoRowCell(
+ modifier = Modifier.weight(1f),
+ titleStyle = MaterialTheme.typography.listItemText,
+ titleColor = MaterialTheme.colorScheme.onSurface,
+ subtitleStyle = MaterialTheme.typography.listItemSubText,
+ subtitleColor = MaterialTheme.colorScheme.onSurface,
+ titleText = obfuscationMode.toTitle(),
+ subtitleText = stringResource(id = R.string.port_x, port.toSubTitle()),
+ onCellClicked = { onSelected(obfuscationMode) },
+ minHeight = Dimens.cellHeight,
+ background =
+ if (isSelected) {
+ MaterialTheme.colorScheme.selected
+ } else {
+ Color.Transparent
+ },
+ iconView = {
+ SelectableIcon(
+ iconContentDescription = null,
+ isSelected = isSelected,
+ isEnabled = true,
+ )
+ },
+ )
+ VerticalDivider(
+ color = MaterialTheme.colorScheme.surface,
+ modifier = Modifier.fillMaxHeight().padding(vertical = Dimens.verticalDividerPadding),
+ )
+ Icon(
+ painterResource(id = R.drawable.icon_chevron),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimary,
+ modifier =
+ Modifier.fillMaxHeight()
+ .clickable { onNavigate() }
+ .padding(horizontal = Dimens.obfuscationNavigationPadding),
+ )
+ }
+}
+
+@Composable
+private fun ObfuscationMode.toTitle() =
+ when (this) {
+ ObfuscationMode.Auto -> stringResource(id = R.string.automatic)
+ ObfuscationMode.Off -> stringResource(id = R.string.off)
+ ObfuscationMode.Udp2Tcp -> stringResource(id = R.string.upd_over_tcp)
+ ObfuscationMode.Shadowsocks -> stringResource(id = R.string.shadowsocks)
+ }
+
+@Composable
+private fun Constraint<Port>.toSubTitle() =
+ when (this) {
+ Constraint.Any -> stringResource(id = R.string.automatic)
+ is Constraint.Only -> this.value.toString()
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SelectableCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SelectableCell.kt
index 0864f99cb1..f958bec319 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SelectableCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SelectableCell.kt
@@ -42,16 +42,10 @@ fun SelectableCell(
isEnabled: Boolean = true,
iconContentDescription: String? = null,
selectedIcon: @Composable RowScope.() -> Unit = {
- Icon(
- painter = painterResource(id = R.drawable.icon_tick),
- contentDescription = iconContentDescription,
- tint = MaterialTheme.colorScheme.onSelected,
- modifier =
- Modifier.padding(end = Dimens.selectableCellTextMargin)
- .alpha(
- if (isSelected && !isEnabled) AlphaDisabled
- else if (isSelected) AlphaVisible else AlphaInvisible
- ),
+ SelectableIcon(
+ iconContentDescription = iconContentDescription,
+ isSelected = isSelected,
+ isEnabled = isEnabled,
)
},
titleStyle: TextStyle = MaterialTheme.typography.labelLarge,
@@ -98,3 +92,25 @@ fun SelectableCell(
testTag = testTag,
)
}
+
+@Composable
+fun RowScope.SelectableIcon(
+ iconContentDescription: String?,
+ isSelected: Boolean,
+ isEnabled: Boolean,
+) {
+ Icon(
+ painter = painterResource(id = R.drawable.icon_tick),
+ contentDescription = iconContentDescription,
+ tint = MaterialTheme.colorScheme.onSelected,
+ modifier =
+ Modifier.padding(end = Dimens.selectableCellTextMargin)
+ .alpha(
+ when {
+ isSelected && !isEnabled -> AlphaDisabled
+ isSelected -> AlphaVisible
+ else -> AlphaInvisible
+ }
+ ),
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt
index b14063a1ea..4c86a33452 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt
@@ -1,7 +1,9 @@
package net.mullvad.mullvadvpn.compose.cell
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -11,6 +13,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
@@ -24,15 +27,20 @@ private fun PreviewTwoRowCell() {
fun TwoRowCell(
titleText: String,
subtitleText: String,
+ modifier: Modifier = Modifier,
bodyView: @Composable ColumnScope.() -> Unit = {},
- onCellClicked: () -> Unit = {},
+ iconView: @Composable RowScope.() -> Unit = {},
+ onCellClicked: (() -> Unit)? = null,
titleColor: Color = MaterialTheme.colorScheme.onPrimary,
subtitleColor: Color = MaterialTheme.colorScheme.onPrimary,
titleStyle: TextStyle = MaterialTheme.typography.labelLarge,
subtitleStyle: TextStyle = MaterialTheme.typography.labelLarge,
background: Color = MaterialTheme.colorScheme.primary,
+ endPadding: Dp = Dimens.cellEndPadding,
+ minHeight: Dp = Dimens.cellHeightTwoRows,
) {
BaseCell(
+ modifier = modifier,
headlineContent = {
Column(modifier = Modifier.weight(1f)) {
Text(
@@ -54,8 +62,11 @@ fun TwoRowCell(
}
},
bodyView = bodyView,
- onCellClicked = onCellClicked,
+ iconView = iconView,
+ onCellClicked = onCellClicked ?: {},
background = background,
- minHeight = Dimens.cellHeightTwoRows,
+ isRowEnabled = onCellClicked != null,
+ minHeight = minHeight,
+ endPadding = endPadding,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialog.kt
new file mode 100644
index 0000000000..b71e8d6774
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialog.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.compose.dialog
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import kotlinx.parcelize.Parcelize
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
+import net.mullvad.mullvadvpn.compose.textfield.CustomPortTextField
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.util.asString
+
+@Preview
+@Composable
+private fun PreviewWireguardCustomPortDialog() {
+ AppTheme {
+ CustomPortDialog(
+ title = "Custom port",
+ portInput = "",
+ isValidInput = false,
+ allowedPortRanges = listOf(PortRange(10..10), PortRange(40..50)),
+ showResetToDefault = false,
+ onInputChanged = {},
+ onSavePort = {},
+ onResetPort = {},
+ onDismiss = {},
+ )
+ }
+}
+
+@Parcelize
+data class CustomPortNavArgs(val customPort: Port?, val allowedPortRanges: List<PortRange>) :
+ Parcelable
+
+@Composable
+fun CustomPortDialog(
+ title: String,
+ portInput: String,
+ isValidInput: Boolean,
+ allowedPortRanges: List<PortRange>,
+ showResetToDefault: Boolean,
+ onInputChanged: (String) -> Unit,
+ onSavePort: (String) -> Unit,
+ onResetPort: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ InputDialog(
+ title = title,
+ input = {
+ CustomPortTextField(
+ value = portInput,
+ onValueChanged = onInputChanged,
+ onSubmit = onSavePort,
+ isValidValue = isValidInput,
+ maxCharLength = 5,
+ modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth(),
+ )
+ },
+ message =
+ stringResource(
+ id = R.string.custom_port_dialog_valid_ranges,
+ allowedPortRanges.asString(),
+ ),
+ confirmButtonEnabled = isValidInput,
+ confirmButtonText = stringResource(id = R.string.custom_port_dialog_submit),
+ onResetButtonText = stringResource(R.string.custom_port_dialog_remove),
+ onBack = onDismiss,
+ onReset = if (showResetToDefault) onResetPort else null,
+ onConfirm = { onSavePort(portInput) },
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ShadowsocksCustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ShadowsocksCustomPortDialog.kt
new file mode 100644
index 0000000000..a19d89ed8b
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ShadowsocksCustomPortDialog.kt
@@ -0,0 +1,46 @@
+package net.mullvad.mullvadvpn.compose.dialog
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.compose.dropUnlessResumed
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.result.ResultBackNavigator
+import com.ramcosta.composedestinations.spec.DestinationStyle
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.viewmodel.ShadowsocksCustomPortDialogSideEffect
+import net.mullvad.mullvadvpn.viewmodel.ShadowsocksCustomPortDialogViewModel
+import org.koin.androidx.compose.koinViewModel
+
+@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
+@Composable
+fun ShadowsocksCustomPort(
+ @Suppress("UNUSED_PARAMETER") navArg: CustomPortNavArgs,
+ backNavigator: ResultBackNavigator<Port?>,
+) {
+ val viewModel = koinViewModel<ShadowsocksCustomPortDialogViewModel>()
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ CollectSideEffectWithLifecycle(viewModel.uiSideEffect) {
+ when (it) {
+ is ShadowsocksCustomPortDialogSideEffect.Success -> backNavigator.navigateBack(it.port)
+ }
+ }
+ CustomPortDialog(
+ title =
+ stringResource(R.string.custom_port_dialog_title, stringResource(R.string.shadowsocks)),
+ portInput = uiState.portInput,
+ isValidInput = uiState.isValidInput,
+ showResetToDefault = uiState.showResetToDefault,
+ allowedPortRanges = uiState.allowedPortRanges,
+ onInputChanged = viewModel::onInputChanged,
+ onSavePort = viewModel::onSaveClick,
+ onResetPort = viewModel::onResetClick,
+ onDismiss = dropUnlessResumed { backNavigator.navigateBack() },
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
index 9bf8b8bf3b..7d0db3c37b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
@@ -1,58 +1,25 @@
package net.mullvad.mullvadvpn.compose.dialog
-import android.os.Parcelable
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
-import com.ramcosta.composedestinations.result.EmptyResultBackNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
-import kotlinx.parcelize.Parcelize
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
-import net.mullvad.mullvadvpn.compose.textfield.CustomPortTextField
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.lib.model.Port
-import net.mullvad.mullvadvpn.lib.model.PortRange
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.util.asString
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogSideEffect
-import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogUiState
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel
import org.koin.androidx.compose.koinViewModel
-@Preview
-@Composable
-private fun PreviewWireguardCustomPortDialog() {
- AppTheme {
- WireguardCustomPort(
- WireguardCustomPortNavArgs(
- customPort = null,
- allowedPortRanges = listOf(PortRange(10..10), PortRange(40..50)),
- ),
- EmptyResultBackNavigator(),
- )
- }
-}
-
-@Parcelize
-data class WireguardCustomPortNavArgs(
- val customPort: Port?,
- val allowedPortRanges: List<PortRange>,
-) : Parcelable
-
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun WireguardCustomPort(
- @Suppress("UNUSED_PARAMETER") navArg: WireguardCustomPortNavArgs,
+ @Suppress("UNUSED_PARAMETER") navArg: CustomPortNavArgs,
backNavigator: ResultBackNavigator<Port?>,
) {
val viewModel = koinViewModel<WireguardCustomPortDialogViewModel>()
@@ -65,45 +32,16 @@ fun WireguardCustomPort(
}
}
- WireguardCustomPortDialog(
- uiState,
+ CustomPortDialog(
+ title =
+ stringResource(R.string.custom_port_dialog_title, stringResource(R.string.wireguard)),
+ portInput = uiState.portInput,
+ isValidInput = uiState.isValidInput,
+ showResetToDefault = uiState.showResetToDefault,
+ allowedPortRanges = uiState.allowedPortRanges,
onInputChanged = viewModel::onInputChanged,
onSavePort = viewModel::onSaveClick,
onResetPort = viewModel::onResetClick,
onDismiss = dropUnlessResumed { backNavigator.navigateBack() },
)
}
-
-@Composable
-fun WireguardCustomPortDialog(
- state: WireguardCustomPortDialogUiState,
- onInputChanged: (String) -> Unit,
- onSavePort: (String) -> Unit,
- onResetPort: () -> Unit,
- onDismiss: () -> Unit,
-) {
- InputDialog(
- title = stringResource(id = R.string.custom_port_dialog_title),
- input = {
- CustomPortTextField(
- value = state.portInput,
- onValueChanged = onInputChanged,
- onSubmit = onSavePort,
- isValidValue = state.isValidInput,
- maxCharLength = 5,
- modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth(),
- )
- },
- message =
- stringResource(
- id = R.string.custom_port_dialog_valid_ranges,
- state.allowedPortRanges.asString(),
- ),
- confirmButtonEnabled = state.isValidInput,
- confirmButtonText = stringResource(id = R.string.custom_port_dialog_submit),
- onResetButtonText = stringResource(R.string.custom_port_dialog_remove),
- onBack = onDismiss,
- onReset = if (state.showResetToDefault) onResetPort else null,
- onConfirm = { onSavePort(state.portInput) },
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectObfuscationCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectObfuscationCellPreviewParameterProvider.kt
new file mode 100644
index 0000000000..646d4eb6e4
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectObfuscationCellPreviewParameterProvider.kt
@@ -0,0 +1,23 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
+import net.mullvad.mullvadvpn.lib.model.Port
+
+class SelectObfuscationCellPreviewParameterProvider :
+ PreviewParameterProvider<Triple<ObfuscationMode, Constraint<Port>, Boolean>> {
+ override val values: Sequence<Triple<ObfuscationMode, Constraint<Port>, Boolean>> =
+ sequenceOf(
+ Triple(ObfuscationMode.Shadowsocks, Constraint.Any, false),
+ Triple(ObfuscationMode.Shadowsocks, Constraint.Any, true),
+ Triple(ObfuscationMode.Shadowsocks, Constraint.Only(Port(PORT)), false),
+ Triple(ObfuscationMode.Shadowsocks, Constraint.Only(Port(PORT)), true),
+ Triple(ObfuscationMode.Udp2Tcp, Constraint.Any, false),
+ Triple(ObfuscationMode.Udp2Tcp, Constraint.Any, true),
+ Triple(ObfuscationMode.Udp2Tcp, Constraint.Only(Port(PORT)), false),
+ Triple(ObfuscationMode.Udp2Tcp, Constraint.Only(Port(PORT)), true),
+ )
+}
+
+private const val PORT = 44
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
index 48b2d62839..56219e62d3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
@@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
import net.mullvad.mullvadvpn.compose.button.VariantButton
-import net.mullvad.mullvadvpn.compose.cell.BaseCell
+import net.mullvad.mullvadvpn.compose.cell.TwoRowCell
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorMedium
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
@@ -297,25 +297,13 @@ private fun ColumnScope.DeviceListHeader(state: DeviceListUiState) {
@Composable
private fun DeviceListItem(device: Device, isLoading: Boolean, onDeviceRemovalClicked: () -> Unit) {
- BaseCell(
- isRowEnabled = false,
- headlineContent = {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- modifier = Modifier.fillMaxWidth(),
- text = device.displayName(),
- style = MaterialTheme.typography.listItemText,
- color = MaterialTheme.colorScheme.onPrimary,
- )
- Text(
- modifier = Modifier.fillMaxWidth(),
- text =
- stringResource(id = R.string.created_x, device.creationDate.formatDate()),
- style = MaterialTheme.typography.listItemSubText,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
- },
+ TwoRowCell(
+ titleStyle = MaterialTheme.typography.listItemText,
+ titleColor = MaterialTheme.colorScheme.onPrimary,
+ subtitleStyle = MaterialTheme.typography.listItemSubText,
+ subtitleColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ titleText = device.displayName(),
+ subtitleText = stringResource(id = R.string.created_x, device.creationDate.formatDate()),
bodyView = {
if (isLoading) {
MullvadCircularProgressIndicatorMedium(
@@ -332,7 +320,9 @@ private fun DeviceListItem(device: Device, isLoading: Boolean, onDeviceRemovalCl
}
}
},
+ onCellClicked = null,
endPadding = Dimens.smallPadding,
+ minHeight = Dimens.cellHeight,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt
new file mode 100644
index 0000000000..0ea4b7fbb0
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt
@@ -0,0 +1,131 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.compose.dropUnlessResumed
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.ShadowsocksCustomPortDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultRecipient
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.cell.CustomPortCell
+import net.mullvad.mullvadvpn.compose.cell.InformationComposeCell
+import net.mullvad.mullvadvpn.compose.cell.SelectableCell
+import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
+import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
+import net.mullvad.mullvadvpn.compose.dialog.CustomPortNavArgs
+import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
+import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState
+import net.mullvad.mullvadvpn.compose.test.SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.SHADOWSOCKS_PORT_ITEM_X_TEST_TAG
+import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
+import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_AVAILABLE_PORTS
+import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_PRESET_PORTS
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.viewmodel.ShadowsocksSettingsViewModel
+import org.koin.androidx.compose.koinViewModel
+
+@Preview
+@Composable
+private fun PreviewShadowsocksSettingsScreen() {
+ AppTheme {
+ ShadowsocksSettingsScreen(
+ state = ShadowsocksSettingsState(port = Constraint.Any, validPortRanges = emptyList())
+ )
+ }
+}
+
+@Destination<RootGraph>(style = SlideInFromRightTransition::class)
+@Composable
+fun ShadowsocksSettings(
+ navigator: DestinationsNavigator,
+ customPortResult: ResultRecipient<ShadowsocksCustomPortDestination, Port?>,
+) {
+ val viewModel = koinViewModel<ShadowsocksSettingsViewModel>()
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ customPortResult.OnNavResultValue { port ->
+ if (port != null) {
+ viewModel.onObfuscationPortSelected(Constraint.Only(port))
+ } else {
+ viewModel.resetCustomPort()
+ }
+ }
+
+ ShadowsocksSettingsScreen(
+ state = state,
+ navigateToCustomPortDialog =
+ dropUnlessResumed {
+ navigator.navigate(
+ ShadowsocksCustomPortDestination(
+ CustomPortNavArgs(
+ customPort = state.customPort,
+ allowedPortRanges = SHADOWSOCKS_AVAILABLE_PORTS,
+ )
+ )
+ )
+ },
+ onObfuscationPortSelected = viewModel::onObfuscationPortSelected,
+ onBackClick = dropUnlessResumed { navigator.navigateUp() },
+ )
+}
+
+@Composable
+fun ShadowsocksSettingsScreen(
+ state: ShadowsocksSettingsState,
+ navigateToCustomPortDialog: () -> Unit = {},
+ onObfuscationPortSelected: (Constraint<Port>) -> Unit = {},
+ onBackClick: () -> Unit = {},
+) {
+ ScaffoldWithMediumTopBar(
+ appBarTitle = stringResource(id = R.string.shadowsocks),
+ navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) },
+ ) { modifier, lazyListState ->
+ LazyColumn(modifier = modifier, state = lazyListState) {
+ itemWithDivider { InformationComposeCell(title = stringResource(R.string.port)) }
+ itemWithDivider {
+ SelectableCell(
+ title = stringResource(id = R.string.automatic),
+ isSelected = state.port is Constraint.Any,
+ onCellClicked = { onObfuscationPortSelected(Constraint.Any) },
+ testTag = SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG,
+ )
+ }
+ itemWithDivider {
+ SHADOWSOCKS_PRESET_PORTS.forEach { port ->
+ SelectableCell(
+ title = port.toString(),
+ isSelected = state.port.getOrNull() == port,
+ onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) },
+ testTag = String.format(null, SHADOWSOCKS_PORT_ITEM_X_TEST_TAG, port.value),
+ )
+ }
+ }
+ itemWithDivider {
+ CustomPortCell(
+ title = stringResource(id = R.string.wireguard_custon_port_title),
+ isSelected = state.isCustom,
+ port = state.customPort,
+ onMainCellClicked = {
+ if (state.customPort != null) {
+ onObfuscationPortSelected(Constraint.Only(state.customPort))
+ } else {
+ navigateToCustomPortDialog()
+ }
+ },
+ onPortCellClicked = navigateToCustomPortDialog,
+ mainTestTag = SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG,
+ )
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt
new file mode 100644
index 0000000000..b77a8016bf
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt
@@ -0,0 +1,89 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.compose.dropUnlessResumed
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.UdpOverTcpPortInfoDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.cell.InformationComposeCell
+import net.mullvad.mullvadvpn.compose.cell.SelectableCell
+import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
+import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
+import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
+import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState
+import net.mullvad.mullvadvpn.compose.test.UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG
+import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.constant.UDP2TCP_PRESET_PORTS
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.viewmodel.Udp2TcpSettingsViewModel
+import org.koin.androidx.compose.koinViewModel
+
+@Preview
+@Composable
+private fun PreviewUdp2TcpSettingsScreen() {
+ AppTheme { Udp2TcpSettingsScreen(state = Udp2TcpSettingsState(port = Constraint.Any)) }
+}
+
+@Destination<RootGraph>(style = SlideInFromRightTransition::class)
+@Composable
+fun Udp2TcpSettings(navigator: DestinationsNavigator) {
+ val viewModel = koinViewModel<Udp2TcpSettingsViewModel>()
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ Udp2TcpSettingsScreen(
+ state = state,
+ onObfuscationPortSelected = viewModel::onObfuscationPortSelected,
+ navigateUdp2TcpInfo =
+ dropUnlessResumed { navigator.navigate(UdpOverTcpPortInfoDestination) },
+ onBackClick = dropUnlessResumed { navigator.navigateUp() },
+ )
+}
+
+@Composable
+fun Udp2TcpSettingsScreen(
+ state: Udp2TcpSettingsState,
+ onObfuscationPortSelected: (Constraint<Port>) -> Unit = {},
+ navigateUdp2TcpInfo: () -> Unit = {},
+ onBackClick: () -> Unit = {},
+) {
+ ScaffoldWithMediumTopBar(
+ appBarTitle = stringResource(id = R.string.upd_over_tcp),
+ navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) },
+ ) { modifier, lazyListState ->
+ LazyColumn(modifier = modifier, state = lazyListState) {
+ itemWithDivider {
+ InformationComposeCell(
+ title = stringResource(R.string.port),
+ onInfoClicked = navigateUdp2TcpInfo,
+ )
+ }
+ itemWithDivider {
+ SelectableCell(
+ title = stringResource(id = R.string.automatic),
+ isSelected = state.port is Constraint.Any,
+ onCellClicked = { onObfuscationPortSelected(Constraint.Any) },
+ testTag = UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG,
+ )
+ }
+ itemWithDivider {
+ UDP2TCP_PRESET_PORTS.forEach { port ->
+ SelectableCell(
+ title = port.toString(),
+ isSelected = state.port.getOrNull() == port,
+ onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) },
+ testTag = String.format(null, UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, port.value),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
index e77dbf00ef..d5416ad0e7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
@@ -45,7 +45,8 @@ import com.ramcosta.composedestinations.generated.destinations.MtuDestination
import com.ramcosta.composedestinations.generated.destinations.ObfuscationInfoDestination
import com.ramcosta.composedestinations.generated.destinations.QuantumResistanceInfoDestination
import com.ramcosta.composedestinations.generated.destinations.ServerIpOverridesDestination
-import com.ramcosta.composedestinations.generated.destinations.UdpOverTcpPortInfoDestination
+import com.ramcosta.composedestinations.generated.destinations.ShadowsocksSettingsDestination
+import com.ramcosta.composedestinations.generated.destinations.Udp2TcpSettingsDestination
import com.ramcosta.composedestinations.generated.destinations.WireguardCustomPortDestination
import com.ramcosta.composedestinations.generated.destinations.WireguardPortInfoDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -64,13 +65,14 @@ import net.mullvad.mullvadvpn.compose.cell.MtuComposeCell
import net.mullvad.mullvadvpn.compose.cell.MtuSubtitle
import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
import net.mullvad.mullvadvpn.compose.cell.NormalSwitchComposeCell
+import net.mullvad.mullvadvpn.compose.cell.ObfuscationModeCell
import net.mullvad.mullvadvpn.compose.cell.SelectableCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
import net.mullvad.mullvadvpn.compose.communication.DnsDialogResult
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
import net.mullvad.mullvadvpn.compose.component.textResource
-import net.mullvad.mullvadvpn.compose.dialog.WireguardCustomPortNavArgs
+import net.mullvad.mullvadvpn.compose.dialog.CustomPortNavArgs
import net.mullvad.mullvadvpn.compose.dialog.info.WireguardPortInfoDialogArgument
import net.mullvad.mullvadvpn.compose.extensions.dropUnlessResumed
import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
@@ -81,29 +83,23 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_LAST_ITEM_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_QUANTUM_ITEM_OFF_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_QUANTUM_ITEM_ON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
-import net.mullvad.mullvadvpn.constant.UDP2TCP_PRESET_PORTS
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.util.hasValue
-import net.mullvad.mullvadvpn.util.isCustom
-import net.mullvad.mullvadvpn.util.toPortOrNull
import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsSideEffect
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
@@ -134,7 +130,7 @@ private fun PreviewVpnSettings() {
navigateToDns = { _, _ -> },
onToggleDnsClick = {},
onBackClick = {},
- onSelectObfuscationSetting = {},
+ onSelectObfuscationMode = {},
onSelectQuantumResistanceSetting = {},
onWireguardPortSelected = {},
)
@@ -221,8 +217,6 @@ fun VpnSettings(
dropUnlessResumed { navigator.navigate(ObfuscationInfoDestination) },
navigateToQuantumResistanceInfo =
dropUnlessResumed { navigator.navigate(QuantumResistanceInfoDestination) },
- navigateUdp2TcpInfo =
- dropUnlessResumed { navigator.navigate(UdpOverTcpPortInfoDestination) },
navigateToWireguardPortInfo =
dropUnlessResumed { availablePortRanges: List<PortRange> ->
navigator.navigate(
@@ -255,19 +249,24 @@ fun VpnSettings(
},
navigateToWireguardPortDialog =
dropUnlessResumed {
- val args =
- WireguardCustomPortNavArgs(
- state.customWireguardPort?.toPortOrNull(),
- state.availablePortRanges,
+ navigator.navigate(
+ WireguardCustomPortDestination(
+ CustomPortNavArgs(
+ customPort = state.customWireguardPort,
+ allowedPortRanges = state.availablePortRanges,
+ )
)
- navigator.navigate(WireguardCustomPortDestination(args))
+ )
},
onToggleDnsClick = vm::onToggleCustomDns,
onBackClick = dropUnlessResumed { navigator.navigateUp() },
- onSelectObfuscationSetting = vm::onSelectObfuscationSetting,
+ onSelectObfuscationMode = vm::onSelectObfuscationMode,
onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting,
onWireguardPortSelected = vm::onWireguardPortSelected,
- onObfuscationPortSelected = vm::onObfuscationPortSelected,
+ navigateToShadowSocksSettings =
+ dropUnlessResumed { navigator.navigate(ShadowsocksSettingsDestination) },
+ navigateToUdp2TcpSettings =
+ dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) },
)
}
@@ -283,7 +282,6 @@ fun VpnSettingsScreen(
navigateToMalwareInfo: () -> Unit = {},
navigateToObfuscationInfo: () -> Unit = {},
navigateToQuantumResistanceInfo: () -> Unit = {},
- navigateUdp2TcpInfo: () -> Unit = {},
navigateToWireguardPortInfo: (availablePortRanges: List<PortRange>) -> Unit = {},
navigateToLocalNetworkSharingInfo: () -> Unit = {},
navigateToDaitaInfo: () -> Unit = {},
@@ -303,13 +301,13 @@ fun VpnSettingsScreen(
navigateToDns: (index: Int?, address: String?) -> Unit = { _, _ -> },
onToggleDnsClick: (Boolean) -> Unit = {},
onBackClick: () -> Unit = {},
- onSelectObfuscationSetting: (selectedObfuscation: SelectedObfuscation) -> Unit = {},
+ onSelectObfuscationMode: (obfuscationMode: ObfuscationMode) -> Unit = {},
onSelectQuantumResistanceSetting: (quantumResistant: QuantumResistantState) -> Unit = {},
onWireguardPortSelected: (port: Constraint<Port>) -> Unit = {},
- onObfuscationPortSelected: (port: Constraint<Port>) -> Unit = {},
+ navigateToShadowSocksSettings: () -> Unit = {},
+ navigateToUdp2TcpSettings: () -> Unit = {},
) {
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
- var expandUdp2TcpPortSettings by rememberSaveable { mutableStateOf(false) }
val biggerPadding = 54.dp
val topPadding = 6.dp
@@ -551,9 +549,13 @@ fun VpnSettingsScreen(
SelectableCell(
title = port.toString(),
testTag =
- String.format(null, LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG, port),
- isSelected = state.selectedWireguardPort.hasValue(port),
- onCellClicked = { onWireguardPortSelected(Constraint.Only(Port(port))) },
+ String.format(
+ null,
+ LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG,
+ port.value,
+ ),
+ isSelected = state.selectedWireguardPort.getOrNull() == port,
+ onCellClicked = { onWireguardPortSelected(Constraint.Only(port)) },
)
}
}
@@ -561,11 +563,11 @@ fun VpnSettingsScreen(
itemWithDivider {
CustomPortCell(
title = stringResource(id = R.string.wireguard_custon_port_title),
- isSelected = state.selectedWireguardPort.isCustom(),
- port = state.customWireguardPort?.toPortOrNull(),
+ isSelected = state.isCustomWireguardPort,
+ port = state.customWireguardPort,
onMainCellClicked = {
if (state.customWireguardPort != null) {
- onWireguardPortSelected(state.customWireguardPort)
+ onWireguardPortSelected(Constraint.Only(state.customWireguardPort))
} else {
navigateToWireguardPortDialog()
}
@@ -582,72 +584,42 @@ fun VpnSettingsScreen(
title = stringResource(R.string.obfuscation_title),
onInfoClicked = navigateToObfuscationInfo,
onCellClicked = navigateToObfuscationInfo,
+ testTag = LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG,
)
}
itemWithDivider {
SelectableCell(
title = stringResource(id = R.string.automatic),
- isSelected = state.selectedObfuscation == SelectedObfuscation.Auto,
- onCellClicked = { onSelectObfuscationSetting(SelectedObfuscation.Auto) },
+ isSelected = state.obfuscationMode == ObfuscationMode.Auto,
+ onCellClicked = { onSelectObfuscationMode(ObfuscationMode.Auto) },
)
}
itemWithDivider {
- SelectableCell(
- title = stringResource(id = R.string.obfuscation_on_udp_over_tcp),
- isSelected = state.selectedObfuscation == SelectedObfuscation.Udp2Tcp,
- onCellClicked = { onSelectObfuscationSetting(SelectedObfuscation.Udp2Tcp) },
+ ObfuscationModeCell(
+ obfuscationMode = ObfuscationMode.Shadowsocks,
+ isSelected = state.obfuscationMode == ObfuscationMode.Shadowsocks,
+ port = state.selectedShadowsSocksObfuscationPort,
+ onSelected = onSelectObfuscationMode,
+ onNavigate = navigateToShadowSocksSettings,
)
}
itemWithDivider {
- SelectableCell(
- title = stringResource(id = R.string.off),
- isSelected = state.selectedObfuscation == SelectedObfuscation.Off,
- onCellClicked = { onSelectObfuscationSetting(SelectedObfuscation.Off) },
+ ObfuscationModeCell(
+ obfuscationMode = ObfuscationMode.Udp2Tcp,
+ isSelected = state.obfuscationMode == ObfuscationMode.Udp2Tcp,
+ port = state.selectedUdp2TcpObfuscationPort,
+ onSelected = onSelectObfuscationMode,
+ onNavigate = navigateToUdp2TcpSettings,
)
}
-
itemWithDivider {
- ExpandableComposeCell(
- title = stringResource(R.string.udp_over_tcp_port_title),
- isExpanded = expandUdp2TcpPortSettings,
- isEnabled = state.selectedObfuscation != SelectedObfuscation.Off,
- onInfoClicked = navigateUdp2TcpInfo,
- onCellClicked = { expandUdp2TcpPortSettings = !expandUdp2TcpPortSettings },
- testTag = LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG,
+ SelectableCell(
+ title = stringResource(id = R.string.off),
+ isSelected = state.obfuscationMode == ObfuscationMode.Off,
+ onCellClicked = { onSelectObfuscationMode(ObfuscationMode.Off) },
)
}
- if (expandUdp2TcpPortSettings) {
- itemWithDivider {
- SelectableCell(
- title = stringResource(id = R.string.automatic),
- isSelected = state.selectedObfuscationPort is Constraint.Any,
- isEnabled = state.selectObfuscationPortEnabled,
- onCellClicked = { onObfuscationPortSelected(Constraint.Any) },
- testTag = LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG,
- )
- }
-
- UDP2TCP_PRESET_PORTS.forEach { port ->
- itemWithDivider {
- SelectableCell(
- title = port.toString(),
- isSelected = state.selectedObfuscationPort.hasValue(port),
- isEnabled = state.selectObfuscationPortEnabled,
- onCellClicked = {
- onObfuscationPortSelected(Constraint.Only(Port(port)))
- },
- testTag =
- String.format(
- null,
- LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG,
- port,
- ),
- )
- }
- }
- }
-
itemWithDivider {
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
InformationComposeCell(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt
new file mode 100644
index 0000000000..7a5a0f86d5
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+
+data class ShadowsocksSettingsState(
+ val port: Constraint<Port> = Constraint.Any,
+ val customPort: Port? = null,
+ val validPortRanges: List<PortRange> = emptyList(),
+) {
+ val isCustom = port is Constraint.Only && port.value == customPort
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt
new file mode 100644
index 0000000000..1eb9c3ebd6
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+
+data class Udp2TcpSettingsState(val port: Constraint<Port> = Constraint.Any)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
index b57de21bc5..7884f199f2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
@@ -3,10 +3,10 @@ package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
data class VpnSettingsUiState(
@@ -17,15 +17,18 @@ data class VpnSettingsUiState(
val isCustomDnsEnabled: Boolean,
val customDnsItems: List<CustomDnsItem>,
val contentBlockersOptions: DefaultDnsOptions,
- val selectedObfuscation: SelectedObfuscation,
- val selectedObfuscationPort: Constraint<Port>,
+ val obfuscationMode: ObfuscationMode,
+ val selectedUdp2TcpObfuscationPort: Constraint<Port>,
+ val selectedShadowsSocksObfuscationPort: Constraint<Port>,
val quantumResistant: QuantumResistantState,
val selectedWireguardPort: Constraint<Port>,
- val customWireguardPort: Constraint<Port>?,
+ val customWireguardPort: Port?,
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
) {
- val selectObfuscationPortEnabled = selectedObfuscation != SelectedObfuscation.Off
+ val isCustomWireguardPort =
+ selectedWireguardPort is Constraint.Only &&
+ selectedWireguardPort.value == customWireguardPort
companion object {
fun createDefault(
@@ -36,11 +39,12 @@ data class VpnSettingsUiState(
isCustomDnsEnabled: Boolean = false,
customDnsItems: List<CustomDnsItem> = emptyList(),
contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(),
- selectedObfuscation: SelectedObfuscation = SelectedObfuscation.Off,
- selectedObfuscationPort: Constraint<Port> = Constraint.Any,
+ obfuscationMode: ObfuscationMode = ObfuscationMode.Off,
+ selectedUdp2TcpObfuscationPort: Constraint<Port> = Constraint.Any,
+ selectedShadowsSocksObfuscationPort: Constraint<Port> = Constraint.Any,
quantumResistant: QuantumResistantState = QuantumResistantState.Off,
selectedWireguardPort: Constraint<Port> = Constraint.Any,
- customWireguardPort: Constraint.Only<Port>? = null,
+ customWireguardPort: Port? = null,
availablePortRanges: List<PortRange> = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
) =
@@ -52,8 +56,9 @@ data class VpnSettingsUiState(
isCustomDnsEnabled,
customDnsItems,
contentBlockersOptions,
- selectedObfuscation,
- selectedObfuscationPort,
+ obfuscationMode,
+ selectedUdp2TcpObfuscationPort,
+ selectedShadowsSocksObfuscationPort,
quantumResistant,
selectedWireguardPort,
customWireguardPort,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
index 47c109d353..299c99190d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
@@ -14,11 +14,9 @@ const val LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG =
"lazy_list_wireguard_custom_port_text_test_tag"
const val LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG =
"lazy_list_wireguard_custom_port_number_test_tag"
-const val LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG = "lazy_list_udp_over_tcp_port_test_tag"
-const val LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG =
- "lazy_list_udp_over_tcp_item_automatic_test_tag"
-const val LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG = "lazy_list_udp_over_tcp_item_%d_test_tag"
const val CUSTOM_PORT_DIALOG_INPUT_TEST_TAG = "custom_port_dialog_input_test_tag"
+const val LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG =
+ "lazy_list_wireguard_obfuscation_title_test_tag"
// SelectLocationScreen, ConnectScreen, CustomListLocationsScreen
const val CIRCULAR_PROGRESS_INDICATOR = "circular_progress_indicator"
@@ -102,3 +100,12 @@ const val API_ACCESS_TEST_METHOD_BUTTON = "api_access_details_test_method_test_t
// EditApiAccessMethodScreen
const val EDIT_API_ACCESS_NAME_INPUT = "edit_api_access_name_input"
+
+// Udp2TcpSettingScreen
+const val UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG = "udp_over_tcp_item_automatic_test_tag"
+const val UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG = "udp_over_tcp_item_%d_test_tag"
+
+// ShadowsocksSettingsScreen
+const val SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG = "shadowsocks_item_automatic_test_tag"
+const val SHADOWSOCKS_PORT_ITEM_X_TEST_TAG = "shadowsocks_item_%d_test_tag"
+const val SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG = "shadowsocks_custom_port_text_test_tag"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt
index 6f6cb5a79b..6f1a753b9d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt
@@ -1,4 +1,11 @@
package net.mullvad.mullvadvpn.constant
-val WIREGUARD_PRESET_PORTS = listOf(51820, 53)
-val UDP2TCP_PRESET_PORTS = listOf(80, 5001)
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+
+val WIREGUARD_PRESET_PORTS = listOf(Port(51820), Port(53))
+val UDP2TCP_PRESET_PORTS = listOf(Port(80), Port(5001))
+val SHADOWSOCKS_PRESET_PORTS = emptyList<Port>()
+val SHADOWSOCKS_AVAILABLE_PORTS =
+ // Currently we consider all ports to be available
+ listOf(PortRange(Port.MIN_VALUE..Port.MAX_VALUE))
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 32fad5614b..b08708826d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -76,8 +76,11 @@ import net.mullvad.mullvadvpn.viewmodel.SaveApiAccessMethodViewModel
import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewModel
import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel
+import net.mullvad.mullvadvpn.viewmodel.ShadowsocksCustomPortDialogViewModel
+import net.mullvad.mullvadvpn.viewmodel.ShadowsocksSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.SplashViewModel
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
+import net.mullvad.mullvadvpn.viewmodel.Udp2TcpSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel
import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
@@ -213,6 +216,9 @@ val uiModule = module {
viewModel { SaveApiAccessMethodViewModel(get(), get()) }
viewModel { ApiAccessMethodDetailsViewModel(get(), get()) }
viewModel { DeleteApiAccessMethodConfirmationViewModel(get(), get()) }
+ viewModel { Udp2TcpSettingsViewModel(get()) }
+ viewModel { ShadowsocksSettingsViewModel(get(), get()) }
+ viewModel { ShadowsocksCustomPortDialogViewModel(get()) }
// This view model must be single so we correctly attach lifecycle and share it with activity
single { NoDaemonViewModel(get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt
index 1dd18fc71a..4bdde9fec5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt
@@ -76,6 +76,9 @@ class RelayListRepository(
val portRanges: Flow<List<PortRange>> =
wireguardEndpointData.map { it.portRanges }.distinctUntilChanged()
+ val shadowsocksPortRanges: Flow<List<PortRange>> =
+ wireguardEndpointData.map { it.shadowsocksPortRanges }.distinctUntilChanged()
+
suspend fun updateSelectedRelayLocation(value: RelayItemId) =
managementService.setRelayLocation(value)
@@ -84,5 +87,5 @@ class RelayListRepository(
fun find(geoLocationId: GeoLocationId) = relayList.value.findByGeoLocationId(geoLocationId)
- private fun defaultWireguardEndpointData() = WireguardEndpointData(emptyList())
+ private fun defaultWireguardEndpointData() = WireguardEndpointData(emptyList(), emptyList())
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
index d66d4a5c00..0fa5e69940 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
@@ -14,11 +14,12 @@ import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.DnsOptions
import net.mullvad.mullvadvpn.lib.model.DnsState
import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.Settings
+@Suppress("TooManyFunctions")
class SettingsRepository(
private val managementService: ManagementService,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
@@ -52,8 +53,11 @@ class SettingsRepository(
suspend fun addCustomDns(address: InetAddress) = managementService.addCustomDns(address)
- suspend fun setCustomObfuscationPort(constraint: Constraint<Port>) =
- managementService.setObfuscationPort(constraint)
+ suspend fun setCustomUdp2TcpObfuscationPort(constraint: Constraint<Port>) =
+ managementService.setUdp2TcpObfuscationPort(constraint)
+
+ suspend fun setCustomShadowsocksObfuscationPort(constraint: Constraint<Port>) =
+ managementService.setShadowsocksObfuscationPort(constraint)
suspend fun setWireguardMtu(mtu: Mtu) = managementService.setWireguardMtu(mtu.value)
@@ -62,7 +66,7 @@ class SettingsRepository(
suspend fun setWireguardQuantumResistant(value: QuantumResistantState) =
managementService.setWireguardQuantumResistant(value)
- suspend fun setObfuscation(value: SelectedObfuscation) = managementService.setObfuscation(value)
+ suspend fun setObfuscation(value: ObfuscationMode) = managementService.setObfuscation(value)
suspend fun setAutoConnect(isEnabled: Boolean) = managementService.setAutoConnect(isEnabled)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt
index ac93b60d00..c32ddf7c31 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt
@@ -1,28 +1,8 @@
package net.mullvad.mullvadvpn.util
-import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
-import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
-fun Constraint<Port>.hasValue(value: Int) =
- when (this) {
- is Constraint.Any -> false
- is Constraint.Only -> this.value.value == value
- }
-
-fun Constraint<Port>.isCustom() =
- when (this) {
- is Constraint.Any -> false
- is Constraint.Only -> !WIREGUARD_PRESET_PORTS.contains(this.value.value)
- }
-
-fun Constraint<Port>.toPortOrNull() =
- when (this) {
- is Constraint.Any -> null
- is Constraint.Only -> this.value
- }
-
fun Port.inAnyOf(portRanges: List<PortRange>): Boolean =
portRanges.any { portRange -> this in portRange }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt
new file mode 100644
index 0000000000..a3ce03428f
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksCustomPortDialogViewModel.kt
@@ -0,0 +1,83 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.composedestinations.generated.destinations.ShadowsocksCustomPortDestination
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.util.inAnyOf
+
+class ShadowsocksCustomPortDialogViewModel(
+ savedStateHandle: SavedStateHandle,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : ViewModel() {
+ private val navArgs = ShadowsocksCustomPortDestination.argsFrom(savedStateHandle).navArg
+
+ private val _portInput = MutableStateFlow(navArgs.customPort?.value?.toString() ?: "")
+ private val _isValidPort = MutableStateFlow(_portInput.value.isValidPort())
+
+ val uiState: StateFlow<ShadowsocksCustomPortDialogUiState> =
+ combine(_portInput, _isValidPort, ::createState)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ createState(_portInput.value, _isValidPort.value),
+ )
+
+ private val _uiSideEffect = Channel<ShadowsocksCustomPortDialogSideEffect>()
+ val uiSideEffect = _uiSideEffect.receiveAsFlow()
+
+ private fun createState(portInput: String, isValidPortInput: Boolean) =
+ ShadowsocksCustomPortDialogUiState(
+ portInput = portInput,
+ isValidInput = isValidPortInput,
+ allowedPortRanges = navArgs.allowedPortRanges,
+ showResetToDefault = navArgs.customPort != null,
+ )
+
+ fun onInputChanged(value: String) {
+ _portInput.value = value
+ _isValidPort.value = value.isValidPort()
+ }
+
+ fun onSaveClick(portValue: String) =
+ viewModelScope.launch(dispatcher) {
+ val port = portValue.parseValidPort() ?: return@launch
+ _uiSideEffect.send(ShadowsocksCustomPortDialogSideEffect.Success(port))
+ }
+
+ fun onResetClick() {
+ viewModelScope.launch(dispatcher) {
+ _uiSideEffect.send(ShadowsocksCustomPortDialogSideEffect.Success(null))
+ }
+ }
+
+ private fun String.isValidPort(): Boolean = parseValidPort() != null
+
+ private fun String.parseValidPort(): Port? =
+ Port.fromString(this).getOrNull()?.takeIf { port ->
+ port.inAnyOf(navArgs.allowedPortRanges)
+ }
+}
+
+sealed interface ShadowsocksCustomPortDialogSideEffect {
+ data class Success(val port: Port?) : ShadowsocksCustomPortDialogSideEffect
+}
+
+data class ShadowsocksCustomPortDialogUiState(
+ val portInput: String,
+ val isValidInput: Boolean,
+ val allowedPortRanges: List<PortRange>,
+ val showResetToDefault: Boolean,
+)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
new file mode 100644
index 0000000000..18197e2e42
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
@@ -0,0 +1,87 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState
+import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_PRESET_PORTS
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
+
+class ShadowsocksSettingsViewModel(
+ private val settingsRepository: SettingsRepository,
+ relayListRepository: RelayListRepository,
+) : ViewModel() {
+
+ private val customPort = MutableStateFlow<Port?>(null)
+
+ val uiState: StateFlow<ShadowsocksSettingsState> =
+ combine(
+ settingsRepository.settingsUpdates.filterNotNull(),
+ customPort,
+ relayListRepository.shadowsocksPortRanges,
+ ) { settings, customPort, portRanges ->
+ ShadowsocksSettingsState(
+ port = settings.getShadowSocksPort(),
+ customPort = customPort,
+ validPortRanges = portRanges,
+ )
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = ShadowsocksSettingsState(),
+ )
+
+ init {
+ viewModelScope.launch {
+ val initialSettings = settingsRepository.settingsUpdates.filterNotNull().first()
+ customPort.update {
+ val initialPort = initialSettings.getShadowSocksPort()
+ if (initialPort.getOrNull() !in SHADOWSOCKS_PRESET_PORTS) {
+ initialPort.getOrNull()
+ } else {
+ null
+ }
+ }
+ }
+ }
+
+ fun onObfuscationPortSelected(port: Constraint<Port>) {
+ viewModelScope.launch {
+ settingsRepository
+ .setCustomShadowsocksObfuscationPort(port)
+ .onLeft { Logger.e("Select shadowsocks port error $it") }
+ .onRight {
+ if (port is Constraint.Only && port.value !in SHADOWSOCKS_PRESET_PORTS) {
+ customPort.update { port.getOrNull() }
+ }
+ }
+ }
+ }
+
+ fun resetCustomPort() {
+ val isCustom = uiState.value.isCustom
+ customPort.update { null }
+ // If custom port was selected, update selection to be any.
+ if (isCustom) {
+ viewModelScope.launch {
+ settingsRepository.setCustomShadowsocksObfuscationPort(Constraint.Any)
+ }
+ }
+ }
+
+ private fun Settings.getShadowSocksPort() = obfuscationSettings.shadowsocks.port
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt
new file mode 100644
index 0000000000..bafe3ff76a
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt
@@ -0,0 +1,37 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.repository.SettingsRepository
+
+class Udp2TcpSettingsViewModel(private val repository: SettingsRepository) : ViewModel() {
+ val uiState: StateFlow<Udp2TcpSettingsState> =
+ repository.settingsUpdates
+ .filterNotNull()
+ .map { settings ->
+ Udp2TcpSettingsState(port = settings.obfuscationSettings.udp2tcp.port)
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = Udp2TcpSettingsState(),
+ )
+
+ fun onObfuscationPortSelected(port: Constraint<Port>) {
+ viewModelScope.launch {
+ repository.setCustomUdp2TcpObfuscationPort(port).onLeft {
+ Logger.e("Select udp to tcp port error $it")
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
index af2ea72e4e..3baeda244a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
@@ -19,18 +19,18 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
+import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.DnsState
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
-import net.mullvad.mullvadvpn.util.isCustom
sealed interface VpnSettingsSideEffect {
sealed interface ShowToast : VpnSettingsSideEffect {
@@ -52,7 +52,7 @@ class VpnSettingsViewModel(
private val _uiSideEffect = Channel<VpnSettingsSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- private val customPort = MutableStateFlow<Constraint<Port>?>(null)
+ private val customPort = MutableStateFlow<Port?>(null)
private val vmState =
combine(repository.settingsUpdates, relayListRepository.portRanges, customPort) {
@@ -68,10 +68,11 @@ class VpnSettingsViewModel(
customDnsList = settings?.addresses()?.asStringAddressList() ?: listOf(),
contentBlockersOptions =
settings?.contentBlockersSettings() ?: DefaultDnsOptions(),
- selectedObfuscation =
- settings?.selectedObfuscationSettings() ?: SelectedObfuscation.Off,
- selectedObfuscationPort =
+ obfuscationMode = settings?.selectedObfuscationMode() ?: ObfuscationMode.Off,
+ selectedUdp2TcpObfuscationPort =
settings?.obfuscationSettings?.udp2tcp?.port ?: Constraint.Any,
+ selectedShadowsocksObfuscationPort =
+ settings?.obfuscationSettings?.shadowsocks?.port ?: Constraint.Any,
quantumResistant = settings?.quantumResistant() ?: QuantumResistantState.Off,
selectedWireguardPort = settings?.getWireguardPort() ?: Constraint.Any,
customWireguardPort = customWgPort,
@@ -99,8 +100,8 @@ class VpnSettingsViewModel(
val initialSettings = repository.settingsUpdates.filterNotNull().first()
customPort.update {
val initialPort = initialSettings.getWireguardPort()
- if (initialPort.isCustom()) {
- initialPort
+ if (initialPort.getOrNull() !in WIREGUARD_PRESET_PORTS) {
+ initialPort.getOrNull()
} else {
null
}
@@ -209,16 +210,16 @@ class VpnSettingsViewModel(
}
}
- fun onSelectObfuscationSetting(selectedObfuscation: SelectedObfuscation) {
+ fun onSelectObfuscationMode(obfuscationMode: ObfuscationMode) {
viewModelScope.launch(dispatcher) {
- repository.setObfuscation(selectedObfuscation).onLeft {
+ repository.setObfuscation(obfuscationMode).onLeft {
_uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
}
}
}
fun onObfuscationPortSelected(port: Constraint<Port>) {
- viewModelScope.launch { repository.setCustomObfuscationPort(port) }
+ viewModelScope.launch { repository.setCustomUdp2TcpObfuscationPort(port) }
}
fun onSelectQuantumResistanceSetting(quantumResistant: QuantumResistantState) {
@@ -230,8 +231,8 @@ class VpnSettingsViewModel(
}
fun onWireguardPortSelected(port: Constraint<Port>) {
- if (port.isCustom()) {
- customPort.update { port }
+ if (port is Constraint.Only && port.value !in WIREGUARD_PRESET_PORTS) {
+ customPort.update { port.value }
}
viewModelScope.launch {
relayListRepository.updateSelectedWireguardConstraints(
@@ -241,9 +242,10 @@ class VpnSettingsViewModel(
}
fun resetCustomPort() {
+ val isCustom = vmState.value.isCustomWireguardPort
customPort.update { null }
// If custom port was selected, update selection to be any.
- if (vmState.value.selectedWireguardPort.isCustom()) {
+ if (isCustom) {
viewModelScope.launch {
relayListRepository.updateSelectedWireguardConstraints(
WireguardConstraints(port = Constraint.Any)
@@ -286,7 +288,7 @@ class VpnSettingsViewModel(
private fun Settings.contentBlockersSettings() = tunnelOptions.dnsOptions.defaultOptions
- private fun Settings.selectedObfuscationSettings() = obfuscationSettings.selectedObfuscation
+ private fun Settings.selectedObfuscationMode() = obfuscationSettings.selectedObfuscationMode
private fun Settings.getWireguardPort() =
relaySettings.relayConstraints.wireguardConstraints.port
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
index 676a10cd70..31d5515a3c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
@@ -4,10 +4,10 @@ import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
data class VpnSettingsViewModelState(
val mtuValue: Mtu?,
@@ -17,14 +17,19 @@ data class VpnSettingsViewModelState(
val isCustomDnsEnabled: Boolean,
val customDnsList: List<CustomDnsItem>,
val contentBlockersOptions: DefaultDnsOptions,
- val selectedObfuscation: SelectedObfuscation,
- val selectedObfuscationPort: Constraint<Port>,
+ val obfuscationMode: ObfuscationMode,
+ val selectedUdp2TcpObfuscationPort: Constraint<Port>,
+ val selectedShadowsocksObfuscationPort: Constraint<Port>,
val quantumResistant: QuantumResistantState,
val selectedWireguardPort: Constraint<Port>,
- val customWireguardPort: Constraint<Port>?,
+ val customWireguardPort: Port?,
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
) {
+ val isCustomWireguardPort =
+ selectedWireguardPort is Constraint.Only &&
+ selectedWireguardPort.value == customWireguardPort
+
fun toUiState(): VpnSettingsUiState =
VpnSettingsUiState(
mtuValue,
@@ -34,8 +39,9 @@ data class VpnSettingsViewModelState(
isCustomDnsEnabled,
customDnsList,
contentBlockersOptions,
- selectedObfuscation,
- selectedObfuscationPort,
+ obfuscationMode,
+ selectedUdp2TcpObfuscationPort,
+ selectedShadowsocksObfuscationPort,
quantumResistant,
selectedWireguardPort,
customWireguardPort,
@@ -53,8 +59,9 @@ data class VpnSettingsViewModelState(
isCustomDnsEnabled = false,
customDnsList = listOf(),
contentBlockersOptions = DefaultDnsOptions(),
- selectedObfuscation = SelectedObfuscation.Auto,
- selectedObfuscationPort = Constraint.Any,
+ obfuscationMode = ObfuscationMode.Auto,
+ selectedUdp2TcpObfuscationPort = Constraint.Any,
+ selectedShadowsocksObfuscationPort = Constraint.Any,
quantumResistant = QuantumResistantState.Off,
selectedWireguardPort = Constraint.Any,
customWireguardPort = null,
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
index f444edce39..514d4f83aa 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
@@ -81,6 +81,7 @@ import net.mullvad.mullvadvpn.lib.model.LoginAccountError
import net.mullvad.mullvadvpn.lib.model.LogoutAccountError
import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
import net.mullvad.mullvadvpn.lib.model.Ownership as ModelOwnership
import net.mullvad.mullvadvpn.lib.model.PlayPurchase
@@ -100,7 +101,6 @@ import net.mullvad.mullvadvpn.lib.model.RelayList
import net.mullvad.mullvadvpn.lib.model.RelaySettings
import net.mullvad.mullvadvpn.lib.model.RemoveApiAccessMethodError
import net.mullvad.mullvadvpn.lib.model.RemoveSplitTunnelingAppError
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.SetAllowLanError
import net.mullvad.mullvadvpn.lib.model.SetApiAccessMethodError
import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError
@@ -129,7 +129,8 @@ import net.mullvad.mullvadvpn.lib.model.location
import net.mullvad.mullvadvpn.lib.model.ownership
import net.mullvad.mullvadvpn.lib.model.providers
import net.mullvad.mullvadvpn.lib.model.relayConstraints
-import net.mullvad.mullvadvpn.lib.model.selectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.selectedObfuscationMode
+import net.mullvad.mullvadvpn.lib.model.shadowsocks
import net.mullvad.mullvadvpn.lib.model.state
import net.mullvad.mullvadvpn.lib.model.udp2tcp
import net.mullvad.mullvadvpn.lib.model.wireguardConstraints
@@ -460,12 +461,10 @@ class ManagementService(
.mapLeft(SetWireguardQuantumResistantError::Unknown)
.mapEmpty()
- suspend fun setObfuscation(
- value: SelectedObfuscation
- ): Either<SetObfuscationOptionsError, Unit> =
+ suspend fun setObfuscation(value: ObfuscationMode): Either<SetObfuscationOptionsError, Unit> =
Either.catch {
val updatedObfuscationSettings =
- ObfuscationSettings.selectedObfuscation.modify(
+ ObfuscationSettings.selectedObfuscationMode.modify(
getSettings().obfuscationSettings
) {
value
@@ -476,7 +475,7 @@ class ManagementService(
.mapLeft(SetObfuscationOptionsError::Unknown)
.mapEmpty()
- suspend fun setObfuscationPort(
+ suspend fun setUdp2TcpObfuscationPort(
portConstraint: Constraint<Port>
): Either<SetObfuscationOptionsError, Unit> =
Either.catch {
@@ -490,6 +489,19 @@ class ManagementService(
.mapLeft(SetObfuscationOptionsError::Unknown)
.mapEmpty()
+ suspend fun setShadowsocksObfuscationPort(
+ portConstraint: Constraint<Port>
+ ): Either<SetObfuscationOptionsError, Unit> =
+ Either.catch {
+ val updatedSettings =
+ ObfuscationSettings.shadowsocks.modify(getSettings().obfuscationSettings) {
+ it.copy(port = portConstraint)
+ }
+ grpc.setObfuscationSettings(updatedSettings.fromDomain())
+ }
+ .mapLeft(SetObfuscationOptionsError::Unknown)
+ .mapEmpty()
+
suspend fun setAutoConnect(isEnabled: Boolean): Either<SetAutoConnectError, Unit> =
Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) }
.onLeft { Logger.e("Set auto connect error") }
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt
index 874783910a..84a826f104 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt
@@ -13,6 +13,7 @@ import net.mullvad.mullvadvpn.lib.model.DnsOptions
import net.mullvad.mullvadvpn.lib.model.DnsState
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.PlayPurchase
@@ -21,7 +22,7 @@ import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.model.RelaySettings
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.ShadowsocksSettings
import net.mullvad.mullvadvpn.lib.model.SocksAuth
import net.mullvad.mullvadvpn.lib.model.TransportProtocol
import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings
@@ -78,18 +79,20 @@ internal fun DefaultDnsOptions.fromDomain(): ManagementInterface.DefaultDnsOptio
internal fun ObfuscationSettings.fromDomain(): ManagementInterface.ObfuscationSettings =
ManagementInterface.ObfuscationSettings.newBuilder()
- .setSelectedObfuscation(selectedObfuscation.fromDomain())
+ .setSelectedObfuscation(selectedObfuscationMode.fromDomain())
.setUdp2Tcp(udp2tcp.fromDomain())
- .setShadowsocks(ManagementInterface.ShadowsocksSettings.newBuilder())
+ .setShadowsocks(shadowsocks.fromDomain())
.build()
-internal fun SelectedObfuscation.fromDomain():
+internal fun ObfuscationMode.fromDomain():
ManagementInterface.ObfuscationSettings.SelectedObfuscation =
when (this) {
- SelectedObfuscation.Udp2Tcp ->
+ ObfuscationMode.Udp2Tcp ->
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP
- SelectedObfuscation.Auto -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO
- SelectedObfuscation.Off -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF
+ ObfuscationMode.Shadowsocks ->
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS
+ ObfuscationMode.Auto -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO
+ ObfuscationMode.Off -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF
}
internal fun Udp2TcpObfuscationSettings.fromDomain():
@@ -236,3 +239,11 @@ internal fun ApiAccessMethodSetting.fromDomain(): ManagementInterface.AccessMeth
.setEnabled(enabled)
.setAccessMethod(apiAccessMethod.fromDomain())
.build()
+
+internal fun ShadowsocksSettings.fromDomain(): ManagementInterface.ShadowsocksSettings =
+ when (val port = port) {
+ is Constraint.Any ->
+ ManagementInterface.ShadowsocksSettings.newBuilder().clearPort().build()
+ is Constraint.Only ->
+ ManagementInterface.ShadowsocksSettings.newBuilder().setPort(port.value.value).build()
+ }
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
index a1020b71d0..a171cff46b 100644
--- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
@@ -39,6 +39,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationEndpoint
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
import net.mullvad.mullvadvpn.lib.model.ObfuscationType
import net.mullvad.mullvadvpn.lib.model.Ownership
@@ -57,8 +58,8 @@ import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.model.RelayList
import net.mullvad.mullvadvpn.lib.model.RelayOverride
import net.mullvad.mullvadvpn.lib.model.RelaySettings
-import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.model.ShadowsocksSettings
import net.mullvad.mullvadvpn.lib.model.SocksAuth
import net.mullvad.mullvadvpn.lib.model.SplitTunnelSettings
import net.mullvad.mullvadvpn.lib.model.TransportProtocol
@@ -172,10 +173,10 @@ internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndp
internal fun ManagementInterface.ObfuscationEndpoint.ObfuscationType.toDomain(): ObfuscationType =
when (this) {
ManagementInterface.ObfuscationEndpoint.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp
+ ManagementInterface.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS ->
+ ObfuscationType.Shadowsocks
ManagementInterface.ObfuscationEndpoint.ObfuscationType.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized obfuscation type")
- ManagementInterface.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS ->
- throw IllegalArgumentException("Shadowsocks is unsupported")
}
internal fun ManagementInterface.TransportProtocol.toDomain(): TransportProtocol =
@@ -334,19 +335,20 @@ internal fun ManagementInterface.Ownership.toDomain(): Constraint<Ownership> =
internal fun ManagementInterface.ObfuscationSettings.toDomain(): ObfuscationSettings =
ObfuscationSettings(
- selectedObfuscation = selectedObfuscation.toDomain(),
+ selectedObfuscationMode = selectedObfuscation.toDomain(),
udp2tcp = udp2Tcp.toDomain(),
+ shadowsocks = shadowsocks.toDomain(),
)
internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomain():
- SelectedObfuscation =
+ ObfuscationMode =
when (this) {
- ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO -> SelectedObfuscation.Auto
- ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF -> SelectedObfuscation.Off
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO -> ObfuscationMode.Auto
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF -> ObfuscationMode.Off
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP ->
- SelectedObfuscation.Udp2Tcp
+ ObfuscationMode.Udp2Tcp
ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS ->
- throw IllegalArgumentException("Shadowsocks is unsupported")
+ ObfuscationMode.Shadowsocks
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized selected obfuscation")
}
@@ -358,6 +360,13 @@ internal fun ManagementInterface.Udp2TcpObfuscationSettings.toDomain(): Udp2TcpO
Udp2TcpObfuscationSettings(Constraint.Any)
}
+internal fun ManagementInterface.ShadowsocksSettings.toDomain(): ShadowsocksSettings =
+ if (hasPort()) {
+ ShadowsocksSettings(Constraint.Only(Port(port)))
+ } else {
+ ShadowsocksSettings(Constraint.Any)
+ }
+
internal fun ManagementInterface.CustomList.toDomain(): CustomList =
CustomList(
id = CustomListId(id),
@@ -443,7 +452,10 @@ internal fun ManagementInterface.RelayList.toDomain(): RelayList =
RelayList(countriesList.toDomain(), wireguard.toDomain())
internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndpointData =
- WireguardEndpointData(portRangesList.map { it.toDomain() })
+ WireguardEndpointData(
+ portRangesList.map { it.toDomain() },
+ shadowsocksPortRangesList.map { it.toDomain() },
+ )
internal fun ManagementInterface.WireguardRelayEndpointData.toDomain(): WireguardRelayEndpointData =
WireguardRelayEndpointData(daita)
@@ -609,9 +621,9 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() =
FeatureIndicator.SERVER_IP_OVERRIDE
ManagementInterface.FeatureIndicator.CUSTOM_MTU -> FeatureIndicator.CUSTOM_MTU
ManagementInterface.FeatureIndicator.DAITA -> FeatureIndicator.DAITA
+ ManagementInterface.FeatureIndicator.SHADOWSOCKS -> FeatureIndicator.SHADOWSOCKS
ManagementInterface.FeatureIndicator.DAITA_SMART_ROUTING,
ManagementInterface.FeatureIndicator.LOCKDOWN_MODE,
- ManagementInterface.FeatureIndicator.SHADOWSOCKS,
ManagementInterface.FeatureIndicator.MULTIHOP,
ManagementInterface.FeatureIndicator.BRIDGE_MODE,
ManagementInterface.FeatureIndicator.CUSTOM_MSS_FIX,
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt
index d11f405869..9b6b5cbf33 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt
@@ -10,9 +10,10 @@ enum class FeatureIndicator {
SERVER_IP_OVERRIDE,
CUSTOM_MTU,
DAITA,
+ SHADOWSOCKS,
// Currently not supported
+ // DAITA_SMART_ROUTING
// LOCKDOWN_MODE,
- // SHADOWSOCKS,
// MULTIHOP,
// BRIDGE_MODE,
// CUSTOM_MSS_FIX,
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt
index 03de12079e..7e4101e973 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt
@@ -1,7 +1,8 @@
package net.mullvad.mullvadvpn.lib.model
-enum class SelectedObfuscation {
+enum class ObfuscationMode {
Auto,
Off,
Udp2Tcp,
+ Shadowsocks,
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt
index 1bf12b2f9b..4425abd39b 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt
@@ -4,8 +4,9 @@ import arrow.optics.optics
@optics
data class ObfuscationSettings(
- val selectedObfuscation: SelectedObfuscation,
+ val selectedObfuscationMode: ObfuscationMode,
val udp2tcp: Udp2TcpObfuscationSettings,
+ val shadowsocks: ShadowsocksSettings,
) {
companion object
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt
index e6ca1e01b9..0f8bf37332 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt
@@ -19,8 +19,8 @@ value class Port(val value: Int) : Parcelable {
Port(number)
}
- private const val MIN_VALUE = 0
- private const val MAX_VALUE = 65535
+ const val MIN_VALUE = 0
+ const val MAX_VALUE = 65535
}
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ShadowsocksSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ShadowsocksSettings.kt
new file mode 100644
index 0000000000..dd20470436
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ShadowsocksSettings.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class ShadowsocksSettings(val port: Constraint<Port>) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt
index 8aff7d2895..358882ddce 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt
@@ -1,3 +1,6 @@
package net.mullvad.mullvadvpn.lib.model
-data class WireguardEndpointData(val portRanges: List<PortRange>)
+data class WireguardEndpointData(
+ val portRanges: List<PortRange>,
+ val shadowsocksPortRanges: List<PortRange>,
+)
diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml
index 637b70f544..65bed61227 100644
--- a/android/lib/resource/src/main/res/values-da/strings.xml
+++ b/android/lib/resource/src/main/res/values-da/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Indtast port</string>
<string name="custom_port_dialog_remove">Fjern brugerdefineret port</string>
<string name="custom_port_dialog_submit">Indstil port</string>
- <string name="custom_port_dialog_title">Brugerdefineret WireGuard-port</string>
<string name="custom_port_dialog_valid_ranges">Gyldige områder: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Kunne ikke fortolke værten for den tilpassede tunnel. Prøv at ændre dine indstillinger.</string>
<string name="daita_info">%1$s (%2$s) skjuler mønstre i din krypterede VPN-trafik. Hvis nogen overvåger din forbindelse, gør dette det betydeligt sværere for dem at identificere, hvilke websteder du besøger. Mønstrene skjules ved omhyggeligt at tilføje netværksstøj og gøre alle netværkspakker lige store.</string>
diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml
index a5f284ac1d..dd4022568f 100644
--- a/android/lib/resource/src/main/res/values-de/strings.xml
+++ b/android/lib/resource/src/main/res/values-de/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Port eingeben</string>
<string name="custom_port_dialog_remove">Eigenen Port entfernen</string>
<string name="custom_port_dialog_submit">Port festlegen</string>
- <string name="custom_port_dialog_title">Eigener WireGuard-Port</string>
<string name="custom_port_dialog_valid_ranges">Gültige Bereiche: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Der Host des benutzerdefinierten Tunnels konnte nicht aufgelöst werden. Versuchen Sie, Ihre Einstellungen zu ändern.</string>
<string name="daita_info">%1$s (%2$s) verbirgt Muster in Ihrem verschlüsselten VPN-Traffic. Wenn jemand Ihre Verbindung überwacht, ist es für ihn wesentlich schwieriger zu erkennen, welche Websites Sie besuchen. Dazu fügt es vorsichtig Netzwerkrauschen hinzu und sorgt dafür, dass alle Netzwerkpakete die gleiche Größe haben.</string>
diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml
index 559618f53f..6545174d98 100644
--- a/android/lib/resource/src/main/res/values-es/strings.xml
+++ b/android/lib/resource/src/main/res/values-es/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Introducir puerto</string>
<string name="custom_port_dialog_remove">Quitar puerto personalizado</string>
<string name="custom_port_dialog_submit">Establecer puerto</string>
- <string name="custom_port_dialog_title">Puerto personalizado de WireGuard</string>
<string name="custom_port_dialog_valid_ranges">Intervalos válidos: %1$s</string>
<string name="custom_tunnel_host_resolution_error">No se puede resolver el host del túnel personalizado. Pruebe a cambiar la configuración.</string>
<string name="daita_info">%1$s (%2$s) oculta los patrones en su tráfico VPN cifrado. Si alguien supervisa su conexión, esto les dificulta notablemente identificar qué sitios web está visitando. Lo realiza añadiendo con cuidado ruido de red y haciendo que todos los paquetes de red tengan el mismo tamaño.</string>
diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml
index a548f3bd60..93d25f7463 100644
--- a/android/lib/resource/src/main/res/values-fi/strings.xml
+++ b/android/lib/resource/src/main/res/values-fi/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Anna portti</string>
<string name="custom_port_dialog_remove">Poista mukautettu portti</string>
<string name="custom_port_dialog_submit">Määritä portti</string>
- <string name="custom_port_dialog_title">Mukautettu WireGuard-portti</string>
<string name="custom_port_dialog_valid_ranges">Kelvolliset portit: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Muokatun tunnelin isännän selvittäminen ei onnistu. Kokeile muuttaa asetuksiasi.</string>
<string name="daita_info">%1$s (%2$s) piilottaa salatussa VPN-liikenteessäsi toistuvat maneerit luomalla tarkoin räätälöityjä häiriöitä verkkoliikenteeseen ja tekemällä kaikista verkkopaketeista samankokoisia. Jos joku siis yrittää tarkkailla yhteyttäsi, hänen on huomattavasti vaikeampi tunnistaa, millä verkkosivustoilla oikein vierailet.</string>
diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml
index b7a09d437d..59340b55b8 100644
--- a/android/lib/resource/src/main/res/values-fr/strings.xml
+++ b/android/lib/resource/src/main/res/values-fr/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Saisir le port</string>
<string name="custom_port_dialog_remove">Supprimer le port personnalisé</string>
<string name="custom_port_dialog_submit">Définir le port</string>
- <string name="custom_port_dialog_title">Port WireGuard personnalisé</string>
<string name="custom_port_dialog_valid_ranges">Plages valides : %1$s</string>
<string name="custom_tunnel_host_resolution_error">Échec de la résolution de l\'hôte du tunnel personnalisé. Essayez de modifier vos paramètres.</string>
<string name="daita_info">%1$s (%2$s) dissimule des motifs dans votre trafic VPN chiffré. Si quelqu\'un surveille votre connexion, il lui sera beaucoup plus difficile d\'identifier les sites Web que vous visitez. Pour ce faire, il ajoute soigneusement du bruit réseau et fait en sorte que tous les paquets de réseau aient la même taille.</string>
diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml
index b9ba1f5a17..a951c8a3fd 100644
--- a/android/lib/resource/src/main/res/values-it/strings.xml
+++ b/android/lib/resource/src/main/res/values-it/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Inserisci porta</string>
<string name="custom_port_dialog_remove">Rimuovi porta personalizzata</string>
<string name="custom_port_dialog_submit">Imposta porta</string>
- <string name="custom_port_dialog_title">Porta personalizzata WireGuard</string>
<string name="custom_port_dialog_valid_ranges">Intervalli validi: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Impossibile risolvere l\'host del tunnel personalizzato. Prova a modificare le impostazioni.</string>
<string name="daita_info">%1$s (%2$s) nasconde i percorsi in un traffico VPN crittografato. Se qualcuno sta monitorando la tua connessione, sarà molto più difficile per lui identificare quali siti web stai visitando. Ciò avviene aggiungendo con attenzione rumore di rete e rendendo tutti i pacchetti di rete della stessa dimensione.</string>
diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml
index 91a72a347f..86506d6005 100644
--- a/android/lib/resource/src/main/res/values-ja/strings.xml
+++ b/android/lib/resource/src/main/res/values-ja/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">ポートを入力</string>
<string name="custom_port_dialog_remove">カスタムポートを削除</string>
<string name="custom_port_dialog_submit">ポートを設定</string>
- <string name="custom_port_dialog_title">WireGuardカスタムポート</string>
<string name="custom_port_dialog_valid_ranges">有効な範囲: %1$s</string>
<string name="custom_tunnel_host_resolution_error">カスタムトンネルのホストを解決できません。設定を変更してみてください。</string>
<string name="daita_info">%1$s (%2$s) を使用すると、暗号化された VPN トラフィックのパターンを隠すことができるようになります。何者かがあなたの接続を監視している場合に、アクセスしているウェブサイトの特定が大幅に難しくなります。ネットワークノイズを慎重に追加し、ネットワークパケットのサイズをすべて同一に揃えることによって実現しています。</string>
diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml
index a9ec5e6604..21b0750a0b 100644
--- a/android/lib/resource/src/main/res/values-ko/strings.xml
+++ b/android/lib/resource/src/main/res/values-ko/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">포트 입력</string>
<string name="custom_port_dialog_remove">사용자 지정 포트 제거</string>
<string name="custom_port_dialog_submit">포트 설정</string>
- <string name="custom_port_dialog_title">WireGuard 사용자 지정 포트</string>
<string name="custom_port_dialog_valid_ranges">유효한 범위: %1$s</string>
<string name="custom_tunnel_host_resolution_error">사용자 지정 터널의 호스트를 확인할 수 없습니다. 설정을 변경해 보세요.</string>
<string name="daita_info">%1$s (%2$s)은(는) 암호화 VPN 트래픽에서 패턴을 숨깁니다. 누군가가 사용자의 연결을 모니터링하고 있다면, 사용자가 방문하고 있는 웹사이트 식별을 훨씬 더 어렵게 만듭니다. 네트워크 노이즈를 세심하게 추가하고, 모든 네트워크 패킷을 같은 크기로 만듭니다.</string>
diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml
index 7a5fb2b8f5..7f7cf869ba 100644
--- a/android/lib/resource/src/main/res/values-my/strings.xml
+++ b/android/lib/resource/src/main/res/values-my/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">ပေါ့တ် ရိုက်ထည့်ရန်</string>
<string name="custom_port_dialog_remove">စိတ်ကြိုက် ပေါ့တ်ကို ဖယ်ရှားရန်</string>
<string name="custom_port_dialog_submit">ပေါ့တ် သတ်မှတ်ရန်</string>
- <string name="custom_port_dialog_title">စိတ်ကြိုက် WireGuard ပေါ့တ်</string>
<string name="custom_port_dialog_valid_ranges">အကျုံးဝင်သည့် အပိုင်းအခြား- %1$s</string>
<string name="custom_tunnel_host_resolution_error">စိတ်ကြိုက်ပြုလုပ်ထားသည့် Tunnel ၏ Host ကို ဖြေရှင်း၍ မရနိုင်ပါ။ သင့်ဆက်တင်ကို ပြောင်းကြည့်ပါ။</string>
<string name="daita_info">%1$s (%2$s) သည် သင်၏ ကုဒ်ပြောင်းဝှက်ထားသော VPN ကူးလူးမှုတွင် ပက်တန်များကို ဝှက်ထားပါသည်။ သင့်ချိတ်ဆက်မှုကို တစ်စုံတစ်ယောက်က စောင့်ကြည့်နေပါက မည်သည့်ဝက်ဘ်ဆိုက်များ သင်ဝင်ရောက်နေသည်ကို ၎င်းတို့က ခွဲခြားဖော်ထုတ်ဖို့ရာ ပို၍ သိသိသာသာ ခက်ခဲသွားအောင် ၎င်းက ပြုလုပ်ပေးပါသည်။ ကွန်ရက် အနှောင့်အယှက်လျှပ်လိုင်းကို ဂရုတစိုက် ထည့်ပြီး ကွန်ရက် ပက်ကက်အားလုံးကို အရွယ်အစားတူညီအောင် ပြုလုပ်ခြင်းဖြင့် ပို၍ခက်ခဲသွားအောင် ဆောင်ရွက်ပါသည်။</string>
diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml
index cb41eaaa33..61ed6229bd 100644
--- a/android/lib/resource/src/main/res/values-nb/strings.xml
+++ b/android/lib/resource/src/main/res/values-nb/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Skriv inn port</string>
<string name="custom_port_dialog_remove">Fjern tilpasset port</string>
<string name="custom_port_dialog_submit">Konfigurer port</string>
- <string name="custom_port_dialog_title">Tilpasset WireGuard-port</string>
<string name="custom_port_dialog_valid_ranges">Gyldige verdiområder: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Kunne ikke løse vert for egendefinert tunnel. Forsøk å endre innstillingene dine.</string>
<string name="daita_info">%1$s (%2$s) skjuler mønstre i den krypterte VPN-trafikken. Hvis noen overvåker tilkoblingen din, gjør dette det betydelig vanskeligere for dem å identifisere hvilke nettsteder du besøker. Dette gjøres ved varsomt å legge til nettverksstøy og gjøre alle nettverkspakker like store.</string>
diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml
index e57e7b96ab..f0ff345e91 100644
--- a/android/lib/resource/src/main/res/values-nl/strings.xml
+++ b/android/lib/resource/src/main/res/values-nl/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Voer poort in</string>
<string name="custom_port_dialog_remove">Aangepaste poort verwijderen</string>
<string name="custom_port_dialog_submit">Poort instellen</string>
- <string name="custom_port_dialog_title">Aangepaste WireGuard-poort</string>
<string name="custom_port_dialog_valid_ranges">Geldige bereiken: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Kan host van aangepaste tunnel niet omzetten. Probeer uw instellingen te wijzigen.</string>
<string name="daita_info">%1$s (%2$s) verbergt patronen in het versleutelde VPN-verkeer. Als iemand de verbinding in de gaten houdt, maakt dit het aanzienlijk moeilijker voor diegene om te zien welke websites u bezoekt. Dit wordt gedaan door zorgvuldig netwerkruis toe te voegen en alle netwerkpakketten even groot te maken.</string>
diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml
index a90f0a95e8..f928845015 100644
--- a/android/lib/resource/src/main/res/values-pl/strings.xml
+++ b/android/lib/resource/src/main/res/values-pl/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Wprowadź port</string>
<string name="custom_port_dialog_remove">Usuń port niestandardowy</string>
<string name="custom_port_dialog_submit">Ustaw port</string>
- <string name="custom_port_dialog_title">Niestandardowy port WireGuard</string>
<string name="custom_port_dialog_valid_ranges">Prawidłowe zakresy: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Nie można rozpoznać hosta tunelu niestandardowego. Spróbuj zmienić ustawienia.</string>
<string name="daita_info">%1$s (%2$s) ukrywa wzorce w zaszyfrowanym ruchu VPN. Jeśli ktoś monitoruje Twoje połączenie, znacznie utrudni to identyfikację odwiedzanych witryn. Odbywa się to poprzez ostrożne dodawanie szumów sieciowych i ustawianie tego samego rozmiaru wszystkich pakietów sieciowych.</string>
diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml
index c179f7d305..35604314fe 100644
--- a/android/lib/resource/src/main/res/values-pt/strings.xml
+++ b/android/lib/resource/src/main/res/values-pt/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Introduzir porta</string>
<string name="custom_port_dialog_remove">Remover porta personalizada</string>
<string name="custom_port_dialog_submit">Definir porta</string>
- <string name="custom_port_dialog_title">Porta personalizada WireGuard</string>
<string name="custom_port_dialog_valid_ranges">Intervalos válidos: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Não foi possível resolver o anfitrião do túnel personalizado. Experimente alterar as suas definições.</string>
<string name="daita_info">%1$s (%2$s) oculta padrões no seu tráfego VPN encriptado. Se alguém estiver a monitorizar a sua ligação, isto dificulta significativamente a identificação dos sites que visita. Para tal, adiciona cuidadosamente ruído de rede e torna todos os pacotes de rede do mesmo tamanho.</string>
diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml
index 1ac114b0c9..fa6edb42bf 100644
--- a/android/lib/resource/src/main/res/values-ru/strings.xml
+++ b/android/lib/resource/src/main/res/values-ru/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Введите порт</string>
<string name="custom_port_dialog_remove">Удалить пользовательский порт</string>
<string name="custom_port_dialog_submit">Установить порт</string>
- <string name="custom_port_dialog_title">Пользовательский порт WireGuard</string>
<string name="custom_port_dialog_valid_ranges">Допустимые диапазоны: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Не удалось преобразовать имя узла пользовательского туннеля. Попробуйте изменить настройки.</string>
<string name="daita_info">%1$s (%2$s) маскирует особенности зашифрованного VPN-трафика. Если кто-то следит за вашим подключением, ему будет значительно сложнее определить, какие сайты вы посещаете. Для этого добавляется сетевой шум, а все сетевые пакеты делаются одинаковыми по размеру.</string>
diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml
index 33f653e5f5..d4c976ffc5 100644
--- a/android/lib/resource/src/main/res/values-sv/strings.xml
+++ b/android/lib/resource/src/main/res/values-sv/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Ange port</string>
<string name="custom_port_dialog_remove">Ta bort anpassad port</string>
<string name="custom_port_dialog_submit">Ställ in port</string>
- <string name="custom_port_dialog_title">Anpassad WireGuard-port</string>
<string name="custom_port_dialog_valid_ranges">Giltiga intervall: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Det går inte att lösa värd för anpassad tunnel. Försök att ändra inställningarna.</string>
<string name="daita_info">%1$s (%2$s) döljer mönster i din krypterade VPN-trafik. Om någon övervakar din anslutning blir det mycket svårare för hen att identifiera vilka webbplatser du besöker. Den gör det genom att noggrant lägga till nätverksbrus och se till så att alla nätverkspaket har samma storlek.</string>
diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml
index 71c424a4b3..c544a7f2f9 100644
--- a/android/lib/resource/src/main/res/values-th/strings.xml
+++ b/android/lib/resource/src/main/res/values-th/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">ป้อนพอร์ต</string>
<string name="custom_port_dialog_remove">นำพอร์ตแบบกำหนดเองออก</string>
<string name="custom_port_dialog_submit">ตั้งค่าพอร์ต</string>
- <string name="custom_port_dialog_title">พอร์ต WireGuard แบบกำหนดเอง</string>
<string name="custom_port_dialog_valid_ranges">ช่วงที่ใช้ได้: %1$s</string>
<string name="custom_tunnel_host_resolution_error">ไม่พบโฮสต์ของช่องทางแบบกำหนดเอง กรุณาลองเปลี่ยนการตั้งค่าของคุณ</string>
<string name="daita_info">%1$s (%2$s) ซ่อนรูปแบบในการรับส่งข้อมูล VPN ที่เข้ารหัสของคุณ หากมีใครกำลังเฝ้าดูการเชื่อมต่อของคุณอยู่ สิ่งนี้จะทำให้การระบุเว็บไซต์ที่คุณกำลังเยี่ยมชมยากขึ้นอย่างมาก ซึ่งทำได้โดยการเพิ่มสัญญาณรบกวนเครือข่ายอย่างระมัดระวัง และทำให้แพ็กเก็ตเครือข่ายทั้งหมดมีขนาดเท่ากันหมด</string>
diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml
index 3970cf50c6..185a7e263e 100644
--- a/android/lib/resource/src/main/res/values-tr/strings.xml
+++ b/android/lib/resource/src/main/res/values-tr/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">Portu girin</string>
<string name="custom_port_dialog_remove">Özel portu kaldır</string>
<string name="custom_port_dialog_submit">Portu ayarla</string>
- <string name="custom_port_dialog_title">WireGuard özel portu</string>
<string name="custom_port_dialog_valid_ranges">Geçerli aralıklar: %1$s</string>
<string name="custom_tunnel_host_resolution_error">Özel tünel ana bilgisayarı çözülemedi. Ayarlarınızı değiştirmeyi deneyin.</string>
<string name="daita_info">%1$s (%2$s), şifrelenmiş VPN trafiğinizdeki kalıpları gizler. Bu sayede, başka biri bağlantınızı izliyorsa ziyaret ettiğiniz web sitelerini tespit etmesi çok daha zor olacaktır. Özellik, dikkatli bir şekilde ağ paraziti ekleyerek ve tüm ağ paketlerini aynı boyuta getirerek sizi izlenmekten korur.</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
index cceec5832e..fb990b02d9 100644
--- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">输入端口</string>
<string name="custom_port_dialog_remove">移除自定义端口</string>
<string name="custom_port_dialog_submit">设置端口</string>
- <string name="custom_port_dialog_title">WireGuard 自定义端口</string>
<string name="custom_port_dialog_valid_ranges">有效范围:%1$s</string>
<string name="custom_tunnel_host_resolution_error">无法解析自定义隧道的主机。请尝试更改您的设置。</string>
<string name="daita_info">%1$s(%2$s)会在您的加密 VPN 流量中隐藏模式。如果有人在监视您的连接,这可以让他们很难识别您正在访问的网站。它的实现方法是小心地添加网络噪声并使所有网络数据包的大小都相同。</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
index 026267db4a..11e90010fd 100644
--- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
@@ -98,7 +98,6 @@
<string name="custom_port_dialog_placeholder">輸入連接埠</string>
<string name="custom_port_dialog_remove">移除自訂連接埠</string>
<string name="custom_port_dialog_submit">設定連接埠</string>
- <string name="custom_port_dialog_title">WireGuard 自訂連接埠</string>
<string name="custom_port_dialog_valid_ranges">有效範圍:%1$s</string>
<string name="custom_tunnel_host_resolution_error">無法解析自訂通道的主機。請嘗試變更您的設定。</string>
<string name="daita_info">%1$s (%2$s) 會在您的加密 VPN 流量中隱藏模式。如果有人正在監視您的連線,這能讓他們難以識別出您正在存取的網站。此實現方式係謹慎加入網路噪音,並使所有網路資料包大小皆相同。</string>
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 7eaa3b8f3e..cf57f937c5 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -252,7 +252,7 @@
<string name="wireguard_port_info_port_range">The custom port can be any value inside the valid ranges: %s.</string>
<string name="wireguard_custon_port_title">Custom</string>
<string name="port">Port</string>
- <string name="custom_port_dialog_title">WireGuard custom port</string>
+ <string name="custom_port_dialog_title">%s custom port</string>
<string name="custom_port_dialog_submit">Set port</string>
<string name="custom_port_dialog_remove">Remove custom port</string>
<string name="custom_port_dialog_valid_ranges">Valid ranges: %s</string>
@@ -391,4 +391,6 @@
<string name="setting_chip">Setting: %s</string>
<string name="enable_anyway">Enable anyway</string>
<string name="daita_relay_subset_warning">This feature isn’t available on all servers. You might need to change location after enabling.</string>
+ <string name="upd_over_tcp">UDP-over-TCP</string>
+ <string name="port_x">Port: %s</string>
</resources>
diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
index 922e97073b..4ce5c8b57c 100644
--- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
+++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
@@ -62,6 +62,7 @@ data class Dimensions(
val notificationBannerStartPadding: Dp = 16.dp,
val notificationEndIconPadding: Dp = 4.dp,
val notificationStatusIconSize: Dp = 10.dp,
+ val obfuscationNavigationPadding: Dp = 24.dp,
val problemReportIconToTitlePadding: Dp = 60.dp,
val progressIndicatorSize: Dp = 48.dp,
val reconnectButtonMinInteractiveComponentSize: Dp = 40.dp,