diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-03-07 11:33:11 +0100 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-03-07 11:33:11 +0100 |
| commit | a6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1 (patch) | |
| tree | a14c4a13d4e5d7ecac41a3d0f9f2e2110370c70d /android/app/src | |
| parent | cae71a342274fbe39a39283d6a833e8671b017c8 (diff) | |
| parent | c17b6fa0d465dd39c9cefa7fe69af23031b67189 (diff) | |
| download | mullvadvpn-a6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1.tar.xz mullvadvpn-a6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1.zip | |
Merge branch 'implement-device-ip-setting'
Diffstat (limited to 'android/app/src')
10 files changed, 105 insertions, 11 deletions
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 13ef28dd23..073c81b6f8 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 @@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_ 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.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port @@ -72,6 +73,7 @@ class VpnSettingsScreenTest { navigateToShadowSocksSettings: () -> Unit = {}, navigateToUdp2TcpSettings: () -> Unit = {}, onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {}, + onSelectDeviceIpVersion: (Constraint<IpVersion>) -> Unit = {}, ) { setContentWithTheme { VpnSettingsScreen( @@ -103,6 +105,7 @@ class VpnSettingsScreenTest { navigateToShadowSocksSettings = navigateToShadowSocksSettings, navigateToUdp2TcpSettings = navigateToUdp2TcpSettings, onToggleAutoStartAndConnectOnBoot = onToggleAutoStartAndConnectOnBoot, + onSelectDeviceIpVersion = onSelectDeviceIpVersion, ) } } 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 779b4792b4..2989dba047 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 @@ -93,6 +93,7 @@ import net.mullvad.mullvadvpn.compose.util.OnNavResultValue import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port @@ -140,6 +141,7 @@ private fun PreviewVpnSettings( navigateToLocalNetworkSharingInfo = {}, navigateToWireguardPortDialog = {}, navigateToServerIpOverrides = {}, + onSelectDeviceIpVersion = {}, ) } } @@ -268,6 +270,7 @@ fun VpnSettings( navigateToUdp2TcpSettings = dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) }, onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot, + onSelectDeviceIpVersion = vm::onDeviceIpVersionSelected, ) } @@ -304,6 +307,7 @@ fun VpnSettingsScreen( navigateToShadowSocksSettings: () -> Unit, navigateToUdp2TcpSettings: () -> Unit, onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit, + onSelectDeviceIpVersion: (ipVersion: Constraint<IpVersion>) -> Unit, ) { var expandContentBlockersState by rememberSaveable { mutableStateOf(false) } val biggerPadding = 54.dp @@ -651,6 +655,32 @@ fun VpnSettingsScreen( Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } + itemWithDivider { + InformationComposeCell(title = stringResource(R.string.device_ip_version_title)) + } + itemWithDivider { + SelectableCell( + title = stringResource(id = R.string.automatic), + isSelected = state.deviceIpVersion == Constraint.Any, + onCellClicked = { onSelectDeviceIpVersion(Constraint.Any) }, + ) + } + itemWithDivider { + SelectableCell( + title = stringResource(id = R.string.device_ip_version_ipv4), + isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV4, + onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV4)) }, + ) + } + item { + SelectableCell( + title = stringResource(id = R.string.device_ip_version_ipv6), + isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV6, + onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV6)) }, + ) + Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) + } + item { MtuComposeCell(mtuValue = state.mtu, onEditMtu = { navigateToMtuDialog(state.mtu) }) } 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 c9fd0257c0..ec069001cc 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 @@ -2,6 +2,7 @@ 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.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port @@ -24,6 +25,7 @@ data class VpnSettingsUiState( val availablePortRanges: List<PortRange>, val systemVpnSettingsAvailable: Boolean, val autoStartAndConnectOnBoot: Boolean, + val deviceIpVersion: Constraint<IpVersion>, ) { val isCustomWireguardPort = selectedWireguardPort is Constraint.Only && @@ -48,6 +50,7 @@ data class VpnSettingsUiState( availablePortRanges: List<PortRange> = emptyList(), systemVpnSettingsAvailable: Boolean = false, autoStartAndConnectOnBoot: Boolean = false, + deviceIpVersion: Constraint<IpVersion> = Constraint.Any, ) = VpnSettingsUiState( mtu, @@ -64,6 +67,7 @@ data class VpnSettingsUiState( availablePortRanges, systemVpnSettingsAvailable, autoStartAndConnectOnBoot, + deviceIpVersion, ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt index 093b87cafc..f92367bcf6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.IpVersion import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.RelayItemId @@ -26,4 +27,7 @@ class WireguardConstraintsRepository( suspend fun setEntryLocation(relayItemId: RelayItemId) = managementService.setEntryLocation(relayItemId) + + suspend fun setDeviceIpVersion(ipVersion: Constraint<IpVersion>) = + managementService.setDeviceIpVersion(ipVersion) } 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 90f98fceaa..2273ada5e0 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 @@ -23,6 +23,7 @@ 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.IpVersion import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.QuantumResistantState @@ -46,7 +47,7 @@ sealed interface VpnSettingsSideEffect { @Suppress("TooManyFunctions") class VpnSettingsViewModel( private val repository: SettingsRepository, - private val relayListRepository: RelayListRepository, + relayListRepository: RelayListRepository, private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase, private val autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository, private val wireguardConstraintsRepository: WireguardConstraintsRepository, @@ -83,6 +84,7 @@ class VpnSettingsViewModel( availablePortRanges = portRanges, systemVpnSettingsAvailable = systemVpnSettingsUseCase(), autoStartAndConnectOnBoot = autoStartAndConnectOnBoot, + deviceIpVersion = settings?.getDeviceIpVersion() ?: Constraint.Any, ) } .stateIn( @@ -122,14 +124,6 @@ class VpnSettingsViewModel( } } - fun onToggleDaita(enable: Boolean) { - viewModelScope.launch(dispatcher) { - repository.setDaitaEnabled(enable).onLeft { - _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) - } - } - } - fun onDnsDialogDismissed() { if (vmState.value.customDnsList.isEmpty()) { onToggleCustomDns(enable = false) @@ -251,6 +245,14 @@ class VpnSettingsViewModel( } } + fun onDeviceIpVersionSelected(ipVersion: Constraint<IpVersion>) { + viewModelScope.launch(dispatcher) { + wireguardConstraintsRepository.setDeviceIpVersion(ipVersion).onLeft { + _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) + } + } + } + private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) = viewModelScope.launch(dispatcher) { repository @@ -265,7 +267,7 @@ class VpnSettingsViewModel( private fun List<String>.asInetAddressList(): List<InetAddress> { return try { map { InetAddress.getByName(it) } - } catch (ex: UnknownHostException) { + } catch (_: UnknownHostException) { Logger.e("Error parsing the DNS address list.") emptyList() } @@ -290,6 +292,9 @@ class VpnSettingsViewModel( private fun Settings.getWireguardPort() = relaySettings.relayConstraints.wireguardConstraints.port + private fun Settings.getDeviceIpVersion() = + relaySettings.relayConstraints.wireguardConstraints.ipVersion + private fun InetAddress.isLocalAddress(): Boolean { return isLinkLocalAddress || isSiteLocalAddress } 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 e8ccf8f4a0..6e91294257 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 @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel 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.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port @@ -24,6 +25,7 @@ data class VpnSettingsViewModelState( val availablePortRanges: List<PortRange>, val systemVpnSettingsAvailable: Boolean, val autoStartAndConnectOnBoot: Boolean, + val deviceIpVersion: Constraint<IpVersion>, ) { val isCustomWireguardPort = selectedWireguardPort is Constraint.Only && @@ -45,6 +47,7 @@ data class VpnSettingsViewModelState( availablePortRanges, systemVpnSettingsAvailable, autoStartAndConnectOnBoot, + deviceIpVersion, ) companion object { @@ -64,6 +67,7 @@ data class VpnSettingsViewModelState( availablePortRanges = emptyList(), systemVpnSettingsAvailable = false, autoStartAndConnectOnBoot = false, + deviceIpVersion = Constraint.Any, ) } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt index deef7b7ab9..9974da6a17 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt @@ -48,6 +48,7 @@ class SelectedLocationUseCaseTest { isMultihopEnabled = true, entryLocation = entryLocation, port = Constraint.Any, + ipVersion = Constraint.Any, ) selectedLocation.value = exitLocation diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt index 34cb1353bb..c51d7e9f48 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt @@ -48,6 +48,7 @@ class MultihopViewModelTest { isMultihopEnabled = true, entryLocation = Constraint.Any, port = Constraint.Any, + ipVersion = Constraint.Any, ) // Act, Assert diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt index 7bc05df05c..b71d217408 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt @@ -108,6 +108,7 @@ class SettingsViewModelTest { isMultihopEnabled = true, entryLocation = Constraint.Any, port = Constraint.Any, + ipVersion = Constraint.Any, ) // Act, Assert 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 aae283e91e..9d7ed39622 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 @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.viewModelScope import app.cash.turbine.test import arrow.core.right +import io.mockk.Awaits import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify @@ -18,10 +19,10 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import mullvad_daemon.management_interface.daitaSettings import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DaitaSettings +import net.mullvad.mullvadvpn.lib.model.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.PortRange @@ -163,6 +164,7 @@ class VpnSettingsViewModelTest { every { mockRelaySettings.relayConstraints } returns mockRelayConstraints every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints every { mockWireguardConstraints.port } returns expectedPort + every { mockWireguardConstraints.ipVersion } returns Constraint.Any every { mockSettings.tunnelOptions } returns TunnelOptions( wireguard = @@ -193,6 +195,7 @@ class VpnSettingsViewModelTest { port = wireguardPort, isMultihopEnabled = false, entryLocation = Constraint.Any, + ipVersion = Constraint.Any, ) coEvery { mockWireguardConstraintsRepository.setWireguardPort(any()) } returns Unit.right() @@ -249,4 +252,42 @@ class VpnSettingsViewModelTest { mockAutoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState) } } + + @Test + fun `when device ip version is IPv6 then UiState should be IPv6`() = runTest { + // Arrange + val ipVersion = Constraint.Only(IpVersion.IPV6) + val mockSettings = mockk<Settings>(relaxed = true) + every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.ipVersion } returns + ipVersion + every { mockSettings.tunnelOptions.wireguard } returns + WireguardTunnelOptions( + mtu = Mtu(0), + quantumResistant = QuantumResistantState.Off, + daitaSettings = DaitaSettings(enabled = false, directOnly = false), + ) + every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.port } returns + Constraint.Any + + // Act, Assert + viewModel.uiState.test { + // Default value + awaitItem() + mockSettingsUpdate.value = mockSettings + assertEquals(ipVersion, awaitItem().deviceIpVersion) + } + } + + @Test + fun `calling onDeviceIpVersionSelected should call setDeviceIpVersion`() = runTest { + // Arrange + val targetState = Constraint.Only(IpVersion.IPV4) + coEvery { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) } just Awaits + + // Act + viewModel.onDeviceIpVersionSelected(targetState) + + // Assert + coVerify(exactly = 1) { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) } + } } |
