summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-03-07 11:33:11 +0100
committerDavid Göransson <david.goransson@mullvad.net>2025-03-07 11:33:11 +0100
commita6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1 (patch)
treea14c4a13d4e5d7ecac41a3d0f9f2e2110370c70d /android/app/src
parentcae71a342274fbe39a39283d6a833e8671b017c8 (diff)
parentc17b6fa0d465dd39c9cefa7fe69af23031b67189 (diff)
downloadmullvadvpn-a6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1.tar.xz
mullvadvpn-a6543aa58a0fefaa52a78e87d5ddbd3112c7e0c1.zip
Merge branch 'implement-device-ip-setting'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt4
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt1
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt1
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt1
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt43
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) }
+ }
}