diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 5da4872c3e05..63a16e0ab9fc 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -24,6 +24,7 @@ Line wrap the file at 100 chars. Th ## [Unreleased] ### Added - Add support for predictive back. +- Add WireGuard over Shadowsocks. ### Changed - Update colors in the app to be more in line with material design. diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt index a073bc60ff55..fa24c504af05 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt @@ -62,7 +62,9 @@ private val DUMMY_RELAY_COUNTRY_2 = ) private val DUMMY_WIREGUARD_PORT_RANGES = ArrayList() -private val DUMMY_WIREGUARD_ENDPOINT_DATA = WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES) +private val DUMMY_SHADOWSOCKS_PORT_RANGES = emptyList() +private val DUMMY_WIREGUARD_ENDPOINT_DATA = + WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES, DUMMY_SHADOWSOCKS_PORT_RANGES) val DUMMY_RELAY_COUNTRIES = listOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt index ec304952651c..7ca4d7f3d4b0 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt @@ -21,7 +21,6 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG import net.mullvad.mullvadvpn.lib.model.PortRange import net.mullvad.mullvadvpn.onNodeWithTagAndText -import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogUiState import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -40,6 +39,7 @@ class CustomPortDialogTest { @SuppressLint("ComposableNaming") @Composable private fun testWireguardCustomPortDialog( + title: String = "", portInput: String = "", isValidInput: Boolean = false, showResetToDefault: Boolean = false, @@ -49,16 +49,12 @@ class CustomPortDialogTest { onResetPort: () -> Unit = {}, onDismiss: () -> Unit = {}, ) { - val state = - WireguardCustomPortDialogUiState( - portInput = portInput, - isValidInput = isValidInput, - allowedPortRanges = allowedPortRanges, - showResetToDefault = showResetToDefault, - ) - - WireguardCustomPortDialog( - state, + CustomPortDialog( + title = title, + portInput = portInput, + isValidInput = isValidInput, + showResetToDefault = showResetToDefault, + allowedPortRanges = allowedPortRanges, onInputChanged = onInputChanged, onSavePort = onSavePort, onDismiss = onDismiss, diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt new file mode 100644 index 000000000000..23d94f58c6a3 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt @@ -0,0 +1,56 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState +import net.mullvad.mullvadvpn.compose.test.SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.Port +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class ShadowsocksSettingsScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @Test + fun testShowShadowsocksCustomPort() = + composeExtension.use { + // Arrange + setContentWithTheme { + ShadowsocksSettingsScreen(state = ShadowsocksSettingsState(customPort = Port(4000))) + } + + // Assert + onNodeWithText("4000").assertExists() + } + + @Test + fun testSelectShadowsocksCustomPort() = + composeExtension.use { + // Arrange + val onObfuscationPortSelected: (Constraint) -> Unit = mockk(relaxed = true) + setContentWithTheme { + ShadowsocksSettingsScreen( + state = + ShadowsocksSettingsState( + port = Constraint.Only(Port(4000)), + customPort = Port(4000), + ), + onObfuscationPortSelected = onObfuscationPortSelected, + ) + } + + // Act + onNodeWithTag(testTag = SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG).performClick() + + // Assert + verify { onObfuscationPortSelected.invoke(Constraint.Only(Port(4000))) } + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt new file mode 100644 index 000000000000..77b9965b26b0 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt @@ -0,0 +1,44 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.performClick +import io.mockk.coVerify +import io.mockk.mockk +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState +import net.mullvad.mullvadvpn.compose.test.UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.onNodeWithTagAndText +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class Udp2TcpSettingsScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @Test + fun testSelectTcpOverUdpPortOption() = + composeExtension.use { + // Arrange + val onObfuscationPortSelected: (Constraint) -> Unit = mockk(relaxed = true) + setContentWithTheme { + Udp2TcpSettingsScreen( + state = Udp2TcpSettingsState(port = Constraint.Any), + onObfuscationPortSelected = onObfuscationPortSelected, + ) + } + + // Act + onNodeWithTagAndText( + testTag = String.format(UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, 5001), + text = "5001", + ) + .assertExists() + .performClick() + + // Assert + coVerify(exactly = 1) { onObfuscationPortSelected.invoke(Constraint.Only(Port(5001))) } + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt index b2079e0e782e..a15875477a07 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode import io.mockk.MockKAnnotations -import io.mockk.coVerify import io.mockk.mockk import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension @@ -18,17 +17,15 @@ 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_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.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Mtu 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.onNodeWithTagAndText import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem import org.junit.jupiter.api.BeforeEach @@ -206,74 +203,6 @@ class VpnSettingsScreenTest { onNodeWithContentDescription(LOCAL_DNS_SERVER_WARNING).assertExists() } - @Test - fun testSelectTcpOverUdpPortOption() = - composeExtension.use { - // Arrange - val onObfuscationPortSelected: (Constraint) -> Unit = mockk(relaxed = true) - setContentWithTheme { - VpnSettingsScreen( - state = - VpnSettingsUiState.createDefault( - selectedObfuscation = SelectedObfuscation.Udp2Tcp, - selectedObfuscationPort = Constraint.Only(Port(5001)), - ), - onObfuscationPortSelected = onObfuscationPortSelected, - ) - } - - // Act - onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG)) - onNodeWithText("UDP-over-TCP port").performClick() - onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode( - hasTestTag(String.format(LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, 5001)) - ) - - // Assert - onNodeWithTagAndText( - testTag = String.format(LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, 5001), - text = "5001", - ) - .assertExists() - .performClick() - - coVerify(exactly = 1) { onObfuscationPortSelected.invoke(Constraint.Only(Port(5001))) } - } - - @Test - fun testAttemptSelectTcpOverUdpPortOption() = - composeExtension.use { - // Arrange - val onObfuscationPortSelected: (Constraint) -> Unit = mockk(relaxed = true) - setContentWithTheme { - VpnSettingsScreen( - state = - VpnSettingsUiState.createDefault( - selectedObfuscation = SelectedObfuscation.Off - ), - onObfuscationPortSelected = onObfuscationPortSelected, - ) - } - - // Act - onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG)) - onNodeWithText("UDP-over-TCP port").performClick() - onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode( - hasTestTag(String.format(LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, 5001)) - ) - - // Assert - onNodeWithTag(String.format(LAZY_LIST_UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, 5001)) - .assertExists() - .performClick() - - verify(exactly = 0) { onObfuscationPortSelected.invoke(any()) } - } - @Test fun testShowSelectedTunnelQuantumOption() = composeExtension.use { @@ -386,10 +315,7 @@ class VpnSettingsScreenTest { // Arrange setContentWithTheme { VpnSettingsScreen( - state = - VpnSettingsUiState.createDefault( - customWireguardPort = Constraint.Only(Port(4000)) - ) + state = VpnSettingsUiState.createDefault(customWireguardPort = Port(4000)) ) } @@ -411,7 +337,7 @@ class VpnSettingsScreenTest { state = VpnSettingsUiState.createDefault( selectedWireguardPort = Constraint.Only(Port(4000)), - customWireguardPort = Constraint.Only(Port(4000)), + customWireguardPort = Port(4000), ), onWireguardPortSelected = onWireguardPortSelected, ) @@ -483,9 +409,8 @@ class VpnSettingsScreenTest { } // Act - onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_UDP_OVER_TCP_PORT_TEST_TAG)) + .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG)) onNodeWithText("WireGuard obfuscation").performClick() // Assert 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 230f28a1944f..adfa64585cb8 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 000000000000..495b9d61b39c --- /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, Boolean> +) { + AppTheme { + ObfuscationModeCell( + obfuscationMode = selectedObfuscationCellData.first, + port = selectedObfuscationCellData.second, + isSelected = selectedObfuscationCellData.third, + onSelected = {}, + onNavigate = {}, + ) + } +} + +@Composable +fun ObfuscationModeCell( + obfuscationMode: ObfuscationMode, + port: Constraint, + 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.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 0864f99cb15a..f958bec319ec 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 b14063a1ea92..4c86a33452b1 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 000000000000..b71e8d677433 --- /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) : + Parcelable + +@Composable +fun CustomPortDialog( + title: String, + portInput: String, + isValidInput: Boolean, + allowedPortRanges: List, + 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 000000000000..a19d89ed8b08 --- /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(style = DestinationStyle.Dialog::class) +@Composable +fun ShadowsocksCustomPort( + @Suppress("UNUSED_PARAMETER") navArg: CustomPortNavArgs, + backNavigator: ResultBackNavigator, +) { + val viewModel = koinViewModel() + + 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 9bf8b8bf3b0d..7d0db3c37b40 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, -) : Parcelable - @Destination(style = DestinationStyle.Dialog::class) @Composable fun WireguardCustomPort( - @Suppress("UNUSED_PARAMETER") navArg: WireguardCustomPortNavArgs, + @Suppress("UNUSED_PARAMETER") navArg: CustomPortNavArgs, backNavigator: ResultBackNavigator, ) { val viewModel = koinViewModel() @@ -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 000000000000..646d4eb6e401 --- /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, Boolean>> { + override val values: Sequence, 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 48b2d62839c6..56219e62d366 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 000000000000..0ea4b7fbb024 --- /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(style = SlideInFromRightTransition::class) +@Composable +fun ShadowsocksSettings( + navigator: DestinationsNavigator, + customPortResult: ResultRecipient, +) { + val viewModel = koinViewModel() + 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) -> 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 000000000000..b77a8016bf96 --- /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(style = SlideInFromRightTransition::class) +@Composable +fun Udp2TcpSettings(navigator: DestinationsNavigator) { + val viewModel = koinViewModel() + 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) -> 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 e77dbf00eff1..d5416ad0e74a 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 -> 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) -> 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) -> Unit = {}, - onObfuscationPortSelected: (port: Constraint) -> 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 000000000000..7a5a0f86d5fd --- /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 = Constraint.Any, + val customPort: Port? = null, + val validPortRanges: List = 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 000000000000..1eb9c3ebd633 --- /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 = 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 b57de21bc5b3..7884f199f2c4 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, val contentBlockersOptions: DefaultDnsOptions, - val selectedObfuscation: SelectedObfuscation, - val selectedObfuscationPort: Constraint, + val obfuscationMode: ObfuscationMode, + val selectedUdp2TcpObfuscationPort: Constraint, + val selectedShadowsSocksObfuscationPort: Constraint, val quantumResistant: QuantumResistantState, val selectedWireguardPort: Constraint, - val customWireguardPort: Constraint?, + val customWireguardPort: Port?, val availablePortRanges: List, 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 = emptyList(), contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(), - selectedObfuscation: SelectedObfuscation = SelectedObfuscation.Off, - selectedObfuscationPort: Constraint = Constraint.Any, + obfuscationMode: ObfuscationMode = ObfuscationMode.Off, + selectedUdp2TcpObfuscationPort: Constraint = Constraint.Any, + selectedShadowsSocksObfuscationPort: Constraint = Constraint.Any, quantumResistant: QuantumResistantState = QuantumResistantState.Off, selectedWireguardPort: Constraint = Constraint.Any, - customWireguardPort: Constraint.Only? = null, + customWireguardPort: Port? = null, availablePortRanges: List = 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 47c109d35364..299c99190d1a 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 6f6cb5a79b0b..6f1a753b9d87 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() +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 32fad5614b45..b08708826d35 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 1dd18fc71a70..4bdde9fec554 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> = wireguardEndpointData.map { it.portRanges }.distinctUntilChanged() + val shadowsocksPortRanges: Flow> = + 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 d66d4a5c009c..0fa5e69940dd 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) = - managementService.setObfuscationPort(constraint) + suspend fun setCustomUdp2TcpObfuscationPort(constraint: Constraint) = + managementService.setUdp2TcpObfuscationPort(constraint) + + suspend fun setCustomShadowsocksObfuscationPort(constraint: Constraint) = + 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 ac93b60d001d..c32ddf7c31d3 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.hasValue(value: Int) = - when (this) { - is Constraint.Any -> false - is Constraint.Only -> this.value.value == value - } - -fun Constraint.isCustom() = - when (this) { - is Constraint.Any -> false - is Constraint.Only -> !WIREGUARD_PRESET_PORTS.contains(this.value.value) - } - -fun Constraint.toPortOrNull() = - when (this) { - is Constraint.Any -> null - is Constraint.Only -> this.value - } - fun Port.inAnyOf(portRanges: List): 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 000000000000..a3ce03428f20 --- /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 = + combine(_portInput, _isValidPort, ::createState) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + createState(_portInput.value, _isValidPort.value), + ) + + private val _uiSideEffect = Channel() + 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, + 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 000000000000..18197e2e4263 --- /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(null) + + val uiState: StateFlow = + 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) { + 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 000000000000..bafe3ff76a0f --- /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 = + repository.settingsUpdates + .filterNotNull() + .map { settings -> + Udp2TcpSettingsState(port = settings.obfuscationSettings.udp2tcp.port) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = Udp2TcpSettingsState(), + ) + + fun onObfuscationPortSelected(port: Constraint) { + 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 af2ea72e4e79..3baeda244a10 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() val uiSideEffect = _uiSideEffect.receiveAsFlow() - private val customPort = MutableStateFlow?>(null) + private val customPort = MutableStateFlow(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) { - viewModelScope.launch { repository.setCustomObfuscationPort(port) } + viewModelScope.launch { repository.setCustomUdp2TcpObfuscationPort(port) } } fun onSelectQuantumResistanceSetting(quantumResistant: QuantumResistantState) { @@ -230,8 +231,8 @@ class VpnSettingsViewModel( } fun onWireguardPortSelected(port: Constraint) { - 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 676a10cd70bf..31d5515a3c90 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, val contentBlockersOptions: DefaultDnsOptions, - val selectedObfuscation: SelectedObfuscation, - val selectedObfuscationPort: Constraint, + val obfuscationMode: ObfuscationMode, + val selectedUdp2TcpObfuscationPort: Constraint, + val selectedShadowsocksObfuscationPort: Constraint, val quantumResistant: QuantumResistantState, val selectedWireguardPort: Constraint, - val customWireguardPort: Constraint?, + val customWireguardPort: Port?, val availablePortRanges: List, 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/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt new file mode 100644 index 000000000000..5340914aaf76 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt @@ -0,0 +1,122 @@ +package net.mullvad.mullvadvpn.viewmodel + +import app.cash.turbine.test +import arrow.core.right +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.lib.common.test.assertLists +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.lib.model.PortRange +import net.mullvad.mullvadvpn.lib.model.Settings +import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(TestCoroutineRule::class) +class ShadowsocksSettingsViewModelTest { + + private val mockSettingsRepository: SettingsRepository = mockk() + private val mockRelayListRepository: RelayListRepository = mockk() + + private val settingsFlow = MutableStateFlow(null) + private val portRangesFlow = MutableStateFlow>(emptyList()) + + private lateinit var viewModel: ShadowsocksSettingsViewModel + + @BeforeEach + fun setUp() { + every { mockSettingsRepository.settingsUpdates } returns settingsFlow + every { mockRelayListRepository.shadowsocksPortRanges } returns portRangesFlow + + viewModel = + ShadowsocksSettingsViewModel( + settingsRepository = mockSettingsRepository, + relayListRepository = mockRelayListRepository, + ) + } + + @Test + fun `uiState should reflect latest port value from settings`() = runTest { + // Arrange + val mockSettings: Settings = mockk() + val port = Port(123) + every { mockSettings.obfuscationSettings.shadowsocks.port } returns Constraint.Only(port) + + settingsFlow.update { mockSettings } + + // Act, Assert + viewModel.uiState.test { + // Check result + val result = awaitItem().port + assertEquals(Constraint.Only(port), result) + } + } + + @Test + fun `uiState should reflect latest port range value from relay list`() = runTest { + // Arrange + val mockSettings: Settings = mockk() + val port = Port(123) + every { mockSettings.obfuscationSettings.shadowsocks.port } returns Constraint.Only(port) + val mockPortRange: List = listOf(mockk()) + + portRangesFlow.update { mockPortRange } + settingsFlow.update { mockSettings } + + // Act, Assert + viewModel.uiState.test { + // Check result + val result = awaitItem().validPortRanges + assertLists(mockPortRange, result) + } + } + + @Test + fun `when onObfuscationPortSelected is called should call repository`() { + // Arrange + val port = Constraint.Only(Port(123)) + coEvery { mockSettingsRepository.setCustomShadowsocksObfuscationPort(port) } returns + Unit.right() + + // Act + viewModel.onObfuscationPortSelected(port) + + // Assert + coVerify { mockSettingsRepository.setCustomShadowsocksObfuscationPort(port) } + } + + @Test + fun `when reset custom port is called should reset custom port`() = runTest { + // Arrange + val mockSettings: Settings = mockk() + val port = Port(123) // Needs to be not in SHADOWSOCKS_PRESET_PORTS + every { mockSettings.obfuscationSettings.shadowsocks.port } returns Constraint.Only(port) + coEvery { + mockSettingsRepository.setCustomShadowsocksObfuscationPort(Constraint.Any) + } returns Unit.right() + + settingsFlow.update { mockSettings } + + // Act, Assert + viewModel.uiState.test { + val startState = awaitItem() + assertEquals(port, startState.customPort) + + viewModel.resetCustomPort() + + val updatedState = awaitItem() + assertEquals(null, updatedState.customPort) + coVerify { mockSettingsRepository.setCustomShadowsocksObfuscationPort(Constraint.Any) } + } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt new file mode 100644 index 000000000000..05114cd4fa8c --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt @@ -0,0 +1,68 @@ +package net.mullvad.mullvadvpn.viewmodel + +import app.cash.turbine.test +import arrow.core.right +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +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.SettingsRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(TestCoroutineRule::class) +class Udp2TcpSettingsViewModelTest { + + private val mockSettingsRepository: SettingsRepository = mockk() + + private val settingsFlow = MutableStateFlow(null) + + private lateinit var viewModel: Udp2TcpSettingsViewModel + + @BeforeEach + fun setUp() { + every { mockSettingsRepository.settingsUpdates } returns settingsFlow + + viewModel = Udp2TcpSettingsViewModel(repository = mockSettingsRepository) + } + + @Test + fun `uiState should reflect latest value from settings`() = runTest { + // Arrange + val mockSettings: Settings = mockk() + val port = Port(123) + every { mockSettings.obfuscationSettings.udp2tcp.port } returns Constraint.Only(port) + + settingsFlow.update { mockSettings } + + // Act, Assert + viewModel.uiState.test { + // Check result + val result = awaitItem().port + assertEquals(Constraint.Only(port), result) + } + } + + @Test + fun `when onObfuscationPortSelected is called should call repository`() { + // Arrange + val port = Constraint.Only(Port(123)) + coEvery { mockSettingsRepository.setCustomUdp2TcpObfuscationPort(port) } returns + Unit.right() + + // Act + viewModel.onObfuscationPortSelected(port) + + // Assert + coVerify { mockSettingsRepository.setCustomUdp2TcpObfuscationPort(port) } + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt index 89456c1d028a..1e15b1956a31 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt @@ -72,11 +72,11 @@ class VpnSettingsViewModelTest { runTest { val customPort = Port(5001) coEvery { - mockSettingsRepository.setCustomObfuscationPort(Constraint.Only(customPort)) + mockSettingsRepository.setCustomUdp2TcpObfuscationPort(Constraint.Only(customPort)) } returns Unit.right() viewModel.onObfuscationPortSelected(Constraint.Only(customPort)) coVerify(exactly = 1) { - mockSettingsRepository.setCustomObfuscationPort(Constraint.Only(customPort)) + mockSettingsRepository.setCustomUdp2TcpObfuscationPort(Constraint.Only(customPort)) } } @@ -122,6 +122,8 @@ class VpnSettingsViewModelTest { every { mockSettings.tunnelOptions } returns mockTunnelOptions every { mockTunnelOptions.wireguard } returns mockWireguardTunnelOptions every { mockSettings.relaySettings } returns mockk(relaxed = true) + every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.port } returns + Constraint.Any viewModel.uiState.test { assertEquals(defaultResistantState, awaitItem().quantumResistant) @@ -134,7 +136,7 @@ class VpnSettingsViewModelTest { fun `when SettingsRepository emits Constraint Only then uiState should emit custom and selectedWireguardPort with port of Constraint`() = runTest { // Arrange - val expectedPort: Constraint = Constraint.Only(Port(99)) + val expectedPort = Constraint.Only(Port(99)) val mockSettings: Settings = mockk(relaxed = true) val mockRelaySettings: RelaySettings = mockk() val mockRelayConstraints: RelayConstraints = mockk() @@ -159,7 +161,7 @@ class VpnSettingsViewModelTest { viewModel.uiState.test { assertIs(awaitItem().selectedWireguardPort) mockSettingsUpdate.value = mockSettings - assertEquals(expectedPort, awaitItem().customWireguardPort) + assertEquals(expectedPort.value, awaitItem().customWireguardPort) assertEquals(expectedPort, awaitItem().selectedWireguardPort) } } 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 f444edce39f3..514d4f83aa6b 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 = + suspend fun setObfuscation(value: ObfuscationMode): Either = 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 ): Either = Either.catch { @@ -490,6 +489,19 @@ class ManagementService( .mapLeft(SetObfuscationOptionsError::Unknown) .mapEmpty() + suspend fun setShadowsocksObfuscationPort( + portConstraint: Constraint + ): Either = + 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 = 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 874783910aeb..84a826f10452 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 a1020b71d0d8..a171cff46bae 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 = 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 d11f40586950..9b6b5cbf33f1 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 similarity index 62% rename from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt rename to android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt index 03de12079e0f..7e4101e97313 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 1bf12b2f9b8a..4425abd39b7c 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 e6ca1e01b990..0f8bf37332be 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 000000000000..dd20470436a2 --- /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) { + 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 8aff7d2895bd..358882ddceef 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) +data class WireguardEndpointData( + val portRanges: List, + val shadowsocksPortRanges: List, +) 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 637b70f5444f..65bed612278a 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 @@ Indtast port Fjern brugerdefineret port Indstil port - Brugerdefineret WireGuard-port Gyldige områder: %1$s Kunne ikke fortolke værten for den tilpassede tunnel. Prøv at ændre dine indstillinger. %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. 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 a5f284ac1d70..dd4022568fc4 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 @@ Port eingeben Eigenen Port entfernen Port festlegen - Eigener WireGuard-Port Gültige Bereiche: %1$s Der Host des benutzerdefinierten Tunnels konnte nicht aufgelöst werden. Versuchen Sie, Ihre Einstellungen zu ändern. %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. 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 559618f53f36..6545174d98c1 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 @@ Introducir puerto Quitar puerto personalizado Establecer puerto - Puerto personalizado de WireGuard Intervalos válidos: %1$s No se puede resolver el host del túnel personalizado. Pruebe a cambiar la configuración. %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. 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 a548f3bd6039..93d25f7463b2 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 @@ Anna portti Poista mukautettu portti Määritä portti - Mukautettu WireGuard-portti Kelvolliset portit: %1$s Muokatun tunnelin isännän selvittäminen ei onnistu. Kokeile muuttaa asetuksiasi. %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. 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 b7a09d437d21..59340b55b8cd 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 @@ Saisir le port Supprimer le port personnalisé Définir le port - Port WireGuard personnalisé Plages valides : %1$s Échec de la résolution de l\'hôte du tunnel personnalisé. Essayez de modifier vos paramètres. %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. 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 b9ba1f5a171c..a951c8a3fdac 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 @@ Inserisci porta Rimuovi porta personalizzata Imposta porta - Porta personalizzata WireGuard Intervalli validi: %1$s Impossibile risolvere l\'host del tunnel personalizzato. Prova a modificare le impostazioni. %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. 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 91a72a347f5f..86506d600595 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 @@ ポートを入力 カスタムポートを削除 ポートを設定 - WireGuardカスタムポート 有効な範囲: %1$s カスタムトンネルのホストを解決できません。設定を変更してみてください。 %1$s (%2$s) を使用すると、暗号化された VPN トラフィックのパターンを隠すことができるようになります。何者かがあなたの接続を監視している場合に、アクセスしているウェブサイトの特定が大幅に難しくなります。ネットワークノイズを慎重に追加し、ネットワークパケットのサイズをすべて同一に揃えることによって実現しています。 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 a9ec5e6604fc..21b0750a0bba 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 @@ 포트 입력 사용자 지정 포트 제거 포트 설정 - WireGuard 사용자 지정 포트 유효한 범위: %1$s 사용자 지정 터널의 호스트를 확인할 수 없습니다. 설정을 변경해 보세요. %1$s (%2$s)은(는) 암호화 VPN 트래픽에서 패턴을 숨깁니다. 누군가가 사용자의 연결을 모니터링하고 있다면, 사용자가 방문하고 있는 웹사이트 식별을 훨씬 더 어렵게 만듭니다. 네트워크 노이즈를 세심하게 추가하고, 모든 네트워크 패킷을 같은 크기로 만듭니다. 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 7a5fb2b8f59b..7f7cf869ba85 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 @@ ပေါ့တ် ရိုက်ထည့်ရန် စိတ်ကြိုက် ပေါ့တ်ကို ဖယ်ရှားရန် ပေါ့တ် သတ်မှတ်ရန် - စိတ်ကြိုက် WireGuard ပေါ့တ် အကျုံးဝင်သည့် အပိုင်းအခြား- %1$s စိတ်ကြိုက်ပြုလုပ်ထားသည့် Tunnel ၏ Host ကို ဖြေရှင်း၍ မရနိုင်ပါ။ သင့်ဆက်တင်ကို ပြောင်းကြည့်ပါ။ %1$s (%2$s) သည် သင်၏ ကုဒ်ပြောင်းဝှက်ထားသော VPN ကူးလူးမှုတွင် ပက်တန်များကို ဝှက်ထားပါသည်။ သင့်ချိတ်ဆက်မှုကို တစ်စုံတစ်ယောက်က စောင့်ကြည့်နေပါက မည်သည့်ဝက်ဘ်ဆိုက်များ သင်ဝင်ရောက်နေသည်ကို ၎င်းတို့က ခွဲခြားဖော်ထုတ်ဖို့ရာ ပို၍ သိသိသာသာ ခက်ခဲသွားအောင် ၎င်းက ပြုလုပ်ပေးပါသည်။ ကွန်ရက် အနှောင့်အယှက်လျှပ်လိုင်းကို ဂရုတစိုက် ထည့်ပြီး ကွန်ရက် ပက်ကက်အားလုံးကို အရွယ်အစားတူညီအောင် ပြုလုပ်ခြင်းဖြင့် ပို၍ခက်ခဲသွားအောင် ဆောင်ရွက်ပါသည်။ 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 cb41eaaa3352..61ed6229bd3f 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 @@ Skriv inn port Fjern tilpasset port Konfigurer port - Tilpasset WireGuard-port Gyldige verdiområder: %1$s Kunne ikke løse vert for egendefinert tunnel. Forsøk å endre innstillingene dine. %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. 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 e57e7b96aba3..f0ff345e91ba 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 @@ Voer poort in Aangepaste poort verwijderen Poort instellen - Aangepaste WireGuard-poort Geldige bereiken: %1$s Kan host van aangepaste tunnel niet omzetten. Probeer uw instellingen te wijzigen. %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. 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 a90f0a95e888..f928845015ac 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 @@ Wprowadź port Usuń port niestandardowy Ustaw port - Niestandardowy port WireGuard Prawidłowe zakresy: %1$s Nie można rozpoznać hosta tunelu niestandardowego. Spróbuj zmienić ustawienia. %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. 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 c179f7d30574..35604314fedb 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 @@ Introduzir porta Remover porta personalizada Definir porta - Porta personalizada WireGuard Intervalos válidos: %1$s Não foi possível resolver o anfitrião do túnel personalizado. Experimente alterar as suas definições. %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. 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 1ac114b0c978..fa6edb42bfa1 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 @@ Введите порт Удалить пользовательский порт Установить порт - Пользовательский порт WireGuard Допустимые диапазоны: %1$s Не удалось преобразовать имя узла пользовательского туннеля. Попробуйте изменить настройки. %1$s (%2$s) маскирует особенности зашифрованного VPN-трафика. Если кто-то следит за вашим подключением, ему будет значительно сложнее определить, какие сайты вы посещаете. Для этого добавляется сетевой шум, а все сетевые пакеты делаются одинаковыми по размеру. 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 33f653e5f511..d4c976ffc54c 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 @@ Ange port Ta bort anpassad port Ställ in port - Anpassad WireGuard-port Giltiga intervall: %1$s Det går inte att lösa värd för anpassad tunnel. Försök att ändra inställningarna. %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. 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 71c424a4b31c..c544a7f2f93e 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 @@ ป้อนพอร์ต นำพอร์ตแบบกำหนดเองออก ตั้งค่าพอร์ต - พอร์ต WireGuard แบบกำหนดเอง ช่วงที่ใช้ได้: %1$s ไม่พบโฮสต์ของช่องทางแบบกำหนดเอง กรุณาลองเปลี่ยนการตั้งค่าของคุณ %1$s (%2$s) ซ่อนรูปแบบในการรับส่งข้อมูล VPN ที่เข้ารหัสของคุณ หากมีใครกำลังเฝ้าดูการเชื่อมต่อของคุณอยู่ สิ่งนี้จะทำให้การระบุเว็บไซต์ที่คุณกำลังเยี่ยมชมยากขึ้นอย่างมาก ซึ่งทำได้โดยการเพิ่มสัญญาณรบกวนเครือข่ายอย่างระมัดระวัง และทำให้แพ็กเก็ตเครือข่ายทั้งหมดมีขนาดเท่ากันหมด 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 3970cf50c626..185a7e263e78 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 @@ Portu girin Özel portu kaldır Portu ayarla - WireGuard özel portu Geçerli aralıklar: %1$s Özel tünel ana bilgisayarı çözülemedi. Ayarlarınızı değiştirmeyi deneyin. %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. 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 cceec5832ec2..fb990b02d9ce 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 @@ 输入端口 移除自定义端口 设置端口 - WireGuard 自定义端口 有效范围:%1$s 无法解析自定义隧道的主机。请尝试更改您的设置。 %1$s(%2$s)会在您的加密 VPN 流量中隐藏模式。如果有人在监视您的连接,这可以让他们很难识别您正在访问的网站。它的实现方法是小心地添加网络噪声并使所有网络数据包的大小都相同。 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 026267db4af6..11e90010fd1c 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 @@ 輸入連接埠 移除自訂連接埠 設定連接埠 - WireGuard 自訂連接埠 有效範圍:%1$s 無法解析自訂通道的主機。請嘗試變更您的設定。 %1$s (%2$s) 會在您的加密 VPN 流量中隱藏模式。如果有人正在監視您的連線,這能讓他們難以識別出您正在存取的網站。此實現方式係謹慎加入網路噪音,並使所有網路資料包大小皆相同。 diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 7eaa3b8f3e24..cf57f937c56f 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 @@ The custom port can be any value inside the valid ranges: %s. Custom Port - WireGuard custom port + %s custom port Set port Remove custom port Valid ranges: %s @@ -391,4 +391,6 @@ Setting: %s Enable anyway This feature isn’t available on all servers. You might need to change location after enabling. + UDP-over-TCP + Port: %s 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 922e97073b11..4ce5c8b57c00 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, diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 730b75aacaa7..7013ba14482d 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -2186,6 +2186,9 @@ msgstr "" msgid "%s (added)" msgstr "" +msgid "%s custom port" +msgstr "" + msgid "%s using %s" msgstr "" @@ -2663,9 +2666,6 @@ msgstr "" msgid "WireGuard MTU" msgstr "" -msgid "WireGuard custom port" -msgstr "" - msgid "WireGuard obfuscation" msgstr ""