summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-09-01 10:42:49 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-09-03 15:52:43 +0200
commite266d72875224a0522d50e55f0555a38deb45ff3 (patch)
treed5d07d35aab1c90f094fba9d77b935ce23071b8b /android
parentf9693d2fe31c0c50027f69f5bd930d30dfa5c764 (diff)
downloadmullvadvpn-e266d72875224a0522d50e55f0555a38deb45ff3.tar.xz
mullvadvpn-e266d72875224a0522d50e55f0555a38deb45ff3.zip
Add UI support for QUIC setting
Diffstat (limited to 'android')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterRow.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ObfuscationModeCell.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt2
-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/relaylist/RelayItemExtensions.kt50
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt48
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Filter.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt)7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Settings.kt36
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/LocationUtil.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SettingsUtil.kt10
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt146
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt4
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt47
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt1
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt22
-rw-r--r--android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt40
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt1
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt1
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt1
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Quic.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt2
-rw-r--r--android/lib/resource/src/main/res/values-da/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-de/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-es/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fi/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-it/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ja/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ko/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-my/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nb/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pt/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ru/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-sv/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-th/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-tr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values/strings_non_translatable.xml1
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt2
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt51
59 files changed, 532 insertions, 229 deletions
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 27b5951cea..8e46a555c9 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
@@ -22,7 +22,7 @@ private val DUMMY_RELAY_1 =
provider = ProviderId("PROVIDER RENTED"),
ownership = Ownership.Rented,
daita = false,
- quic = false,
+ quic = null,
)
private val DUMMY_RELAY_2 =
RelayItem.Location.Relay(
@@ -35,7 +35,7 @@ private val DUMMY_RELAY_2 =
provider = ProviderId("PROVIDER OWNED"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
private val DUMMY_RELAY_CITY_1 =
RelayItem.Location.City(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterRow.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterRow.kt
index 5ac7c765f0..710c4a5cac 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterRow.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterRow.kt
@@ -65,6 +65,7 @@ fun FilterRow(
is FilterChip.Daita -> DaitaFilterChip()
is FilterChip.Entry -> EntryFilterChip()
is FilterChip.Exit -> ExitFilterChip()
+ is FilterChip.Quic -> QuicFilterChip()
}
}
}
@@ -115,6 +116,15 @@ fun ExitFilterChip() {
)
}
+@Composable
+fun QuicFilterChip() {
+ MullvadFilterChip(
+ text = stringResource(id = R.string.quic),
+ onRemoveClick = {},
+ enabled = false,
+ )
+}
+
private fun Ownership.stringResources(): Int =
when (this) {
Ownership.MullvadOwned -> R.string.owned
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
index aa7416218a..bc0c52db83 100644
--- 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
@@ -122,6 +122,7 @@ private fun ObfuscationMode.toTitle() =
ObfuscationMode.Off -> stringResource(id = R.string.off)
ObfuscationMode.Udp2Tcp -> stringResource(id = R.string.upd_over_tcp)
ObfuscationMode.Shadowsocks -> stringResource(id = R.string.shadowsocks)
+ ObfuscationMode.Quic -> stringResource(id = R.string.quic)
}
@Composable
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt
index a485dbe9d9..98abee9589 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt
@@ -130,7 +130,8 @@ private fun FeatureIndicator.text(): String {
FeatureIndicator.QUANTUM_RESISTANCE -> R.string.feature_quantum_resistant
FeatureIndicator.SPLIT_TUNNELING -> R.string.split_tunneling
FeatureIndicator.SHADOWSOCKS,
- FeatureIndicator.UDP_2_TCP -> R.string.feature_udp_2_tcp
+ FeatureIndicator.UDP_2_TCP,
+ FeatureIndicator.QUIC -> R.string.feature_obfuscation
FeatureIndicator.LAN_SHARING -> R.string.local_network_sharing
FeatureIndicator.DNS_CONTENT_BLOCKERS -> R.string.dns_content_blockers
FeatureIndicator.CUSTOM_DNS -> R.string.feature_custom_dns
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 8f7775c613..8cb0bde801 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -791,6 +791,6 @@ private fun FeatureIndicator.destination() =
FeatureIndicator.LAN_SHARING,
FeatureIndicator.DNS_CONTENT_BLOCKERS,
FeatureIndicator.CUSTOM_DNS,
- FeatureIndicator.CUSTOM_MTU ->
- VpnSettingsDestination(scrollToFeature = this, isModal = true)
+ FeatureIndicator.CUSTOM_MTU,
+ FeatureIndicator.QUIC -> VpnSettingsDestination(scrollToFeature = this, isModal = true)
}
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 ea45861c96..6aa1788ae9 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
@@ -475,7 +475,8 @@ fun VpnSettingsContent(
val initialIndexFocus =
when (initialScrollToFeature) {
FeatureIndicator.UDP_2_TCP,
- FeatureIndicator.SHADOWSOCKS -> VpnSettingItem.ObfuscationHeader::class
+ FeatureIndicator.SHADOWSOCKS,
+ FeatureIndicator.QUIC -> VpnSettingItem.ObfuscationHeader::class
FeatureIndicator.LAN_SHARING -> VpnSettingItem.LocalNetworkSharingSetting::class
FeatureIndicator.QUANTUM_RESISTANCE -> VpnSettingItem.QuantumResistanceHeader::class
FeatureIndicator.DNS_CONTENT_BLOCKERS -> VpnSettingItem.DnsContentBlockersHeader::class
@@ -852,6 +853,16 @@ fun VpnSettingsContent(
)
}
+ is VpnSettingItem.ObfuscationItem.Quic ->
+ item(key = it::class.simpleName) {
+ SelectableCell(
+ title = stringResource(id = R.string.quic),
+ isSelected = it.selected,
+ modifier = Modifier.animateItem(),
+ onCellClicked = { onSelectObfuscationMode(ObfuscationMode.Quic) },
+ )
+ }
+
is VpnSettingItem.QuantumItem ->
item(key = it::class.simpleName + it.quantumResistantState) {
SelectableCell(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt
index 517959e1a2..3ce6637115 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt
@@ -104,6 +104,8 @@ sealed interface VpnSettingItem {
data class UdpOverTcp(override val selected: Boolean, val port: Constraint<Port>) :
ObfuscationItem
+ data class Quic(override val selected: Boolean) : ObfuscationItem
+
data class Off(override val selected: Boolean) : ObfuscationItem
}
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 bca87015b4..08fafc56c0 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
@@ -205,6 +205,10 @@ data class VpnSettingsUiState(val settings: List<VpnSettingItem>, val isModal: B
)
)
add(VpnSettingItem.Divider)
+ add(
+ VpnSettingItem.ObfuscationItem.Quic(obfuscationMode == ObfuscationMode.Quic)
+ )
+ add(VpnSettingItem.Divider)
add(VpnSettingItem.ObfuscationItem.Off(obfuscationMode == ObfuscationMode.Off))
add(VpnSettingItem.Spacer)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
index f493aa97cb..c59b0124c3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
@@ -2,8 +2,10 @@ package net.mullvad.mullvadvpn.relaylist
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.Quic
import net.mullvad.mullvadvpn.lib.model.RelayItem
fun RelayItem.children(): List<RelayItem> {
@@ -57,13 +59,18 @@ fun RelayItem.CustomList.filter(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
daita: Boolean,
+ quic: Boolean,
+ ipVersion: Constraint<IpVersion>,
): RelayItem.CustomList {
val newLocations =
locations.mapNotNull {
when (it) {
- is RelayItem.Location.Country -> it.filter(ownership, providers, daita)
- is RelayItem.Location.City -> it.filter(ownership, providers, daita)
- is RelayItem.Location.Relay -> it.filter(ownership, providers, daita)
+ is RelayItem.Location.Country ->
+ it.filter(ownership, providers, daita, quic, ipVersion)
+ is RelayItem.Location.City ->
+ it.filter(ownership, providers, daita, quic, ipVersion)
+ is RelayItem.Location.Relay ->
+ it.filter(ownership, providers, daita, quic, ipVersion)
}
}
return copy(locations = newLocations)
@@ -73,8 +80,10 @@ fun RelayItem.Location.Country.filter(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
daita: Boolean,
+ quic: Boolean,
+ ipVersion: Constraint<IpVersion>,
): RelayItem.Location.Country? {
- val cities = cities.mapNotNull { it.filter(ownership, providers, daita) }
+ val cities = cities.mapNotNull { it.filter(ownership, providers, daita, quic, ipVersion) }
return if (cities.isNotEmpty()) {
this.copy(cities = cities)
} else {
@@ -86,8 +95,10 @@ private fun RelayItem.Location.City.filter(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
daita: Boolean,
+ quic: Boolean,
+ ipVersion: Constraint<IpVersion>,
): RelayItem.Location.City? {
- val relays = relays.mapNotNull { it.filter(ownership, providers, daita) }
+ val relays = relays.mapNotNull { it.filter(ownership, providers, daita, quic, ipVersion) }
return if (relays.isNotEmpty()) {
this.copy(relays = relays)
} else {
@@ -95,15 +106,38 @@ private fun RelayItem.Location.City.filter(
}
}
-private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(filterDaita: Boolean): Boolean =
- if (filterDaita) daita else true
+private fun RelayItem.Location.Relay.requiredFeatures(
+ requireDaita: Boolean,
+ requireQuic: Boolean,
+ ipVersion: Constraint<IpVersion>,
+): Boolean =
+ when {
+ requireDaita && requireQuic -> daita && quic?.supports(ipVersion) == true
+ requireDaita -> daita
+ requireQuic -> quic?.supports(ipVersion) == true
+ else -> true
+ }
+
+private fun Quic.supports(ipVersion: Constraint<IpVersion>) =
+ when (ipVersion.getOrNull()) {
+ IpVersion.IPV4 -> supportsIpv4
+ IpVersion.IPV6 -> supportsIpv6
+ else -> inAddresses.isNotEmpty()
+ }
private fun RelayItem.Location.Relay.filter(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
daita: Boolean,
+ quic: Boolean,
+ ipVersion: Constraint<IpVersion>,
): RelayItem.Location.Relay? =
- if (hasMatchingDaitaSetting(daita) && hasOwnership(ownership) && hasProvider(providers)) this
+ if (
+ requiredFeatures(daita, quic, ipVersion) &&
+ hasOwnership(ownership) &&
+ hasProvider(providers)
+ )
+ this
else null
fun List<RelayItem.Location.Country>.findByGeoLocationId(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
index 5e7a82d9ed..12ec3a296c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
@@ -10,7 +10,10 @@ import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
+import net.mullvad.mullvadvpn.util.isDaitaAndDirectOnly
+import net.mullvad.mullvadvpn.util.isQuicEnabled
import net.mullvad.mullvadvpn.util.shouldFilterByDaita
+import net.mullvad.mullvadvpn.util.shouldFilterByQuic
typealias ModelOwnership = Ownership
@@ -30,7 +33,7 @@ class FilterChipUseCase(
selectedOwnership = selectedOwnership,
selectedConstraintProviders = selectedConstraintProviders,
providerToOwnerships = providerOwnership,
- daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
+ settings = settings,
relayListType = relayListType,
)
}
@@ -39,7 +42,7 @@ class FilterChipUseCase(
selectedOwnership: Constraint<Ownership>,
selectedConstraintProviders: Constraint<Providers>,
providerToOwnerships: Map<ProviderId, Set<Ownership>>,
- daitaDirectOnly: Boolean,
+ settings: Settings?,
relayListType: RelayListType,
): List<FilterChip> {
val ownershipFilter = selectedOwnership.getOrNull()
@@ -70,18 +73,19 @@ class FilterChipUseCase(
}
if (
shouldFilterByDaita(
- daitaDirectOnly = daitaDirectOnly,
+ daitaDirectOnly = settings?.isDaitaAndDirectOnly() == true,
relayListType = relayListType,
)
) {
add(FilterChip.Daita)
}
+ if (
+ shouldFilterByQuic(settings?.isQuicEnabled() == true, relayListType = relayListType)
+ ) {
+ add(FilterChip.Quic)
+ }
}
}
-
- private fun Settings.daitaAndDirectOnly() =
- tunnelOptions.wireguard.daitaSettings.enabled &&
- tunnelOptions.wireguard.daitaSettings.directOnly
}
sealed interface FilterChip {
@@ -94,4 +98,6 @@ sealed interface FilterChip {
data object Entry : FilterChip
data object Exit : FilterChip
+
+ data object Quic : FilterChip
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
index 875d63c1ce..2681cfa3ad 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
@@ -3,16 +3,20 @@ package net.mullvad.mullvadvpn.usecase
import kotlinx.coroutines.flow.combine
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.relaylist.filter
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
+import net.mullvad.mullvadvpn.util.ipVersionConstraint
+import net.mullvad.mullvadvpn.util.isDaitaAndDirectOnly
+import net.mullvad.mullvadvpn.util.isQuicEnabled
import net.mullvad.mullvadvpn.util.shouldFilterByDaita
+import net.mullvad.mullvadvpn.util.shouldFilterByQuic
class FilteredRelayListUseCase(
private val relayListRepository: RelayListRepository,
@@ -33,9 +37,15 @@ class FilteredRelayListUseCase(
providers = selectedProviders,
shouldFilterByDaita =
shouldFilterByDaita(
- daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
+ daitaDirectOnly = settings?.isDaitaAndDirectOnly() == true,
relayListType = relayListType,
),
+ shouldFilterByQuic =
+ shouldFilterByQuic(
+ settings?.isQuicEnabled() == true,
+ relayListType = relayListType,
+ ),
+ constraintIpVersion = settings?.ipVersionConstraint() ?: Constraint.Any,
)
}
@@ -43,9 +53,15 @@ class FilteredRelayListUseCase(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
shouldFilterByDaita: Boolean,
- ) = mapNotNull { it.filter(ownership, providers, shouldFilterByDaita) }
-
- private fun Settings.daitaAndDirectOnly() =
- tunnelOptions.wireguard.daitaSettings.enabled &&
- tunnelOptions.wireguard.daitaSettings.directOnly
+ shouldFilterByQuic: Boolean,
+ constraintIpVersion: Constraint<IpVersion>,
+ ) = mapNotNull {
+ it.filter(
+ ownership,
+ providers,
+ shouldFilterByDaita,
+ shouldFilterByQuic,
+ constraintIpVersion,
+ )
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
index 7357068a56..ae2cea104d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
@@ -22,31 +23,42 @@ class RecentsUseCase(
private val settingsRepository: SettingsRepository,
) {
- operator fun invoke(): Flow<List<Hop>?> =
+ operator fun invoke(isMultihop: Boolean): Flow<List<Hop>?> =
+ if (isMultihop) {
+ multiHopRecents()
+ } else {
+ singleHopRecents()
+ }
+
+ private fun singleHopRecents(): Flow<List<Hop.Single<RelayItem>>?> =
combine(
- recents(),
+ recents().map { it?.filterIsInstance<Recent.Singlehop>() },
+ filteredRelayListUseCase(RelayListType.Single),
+ customListsRelayItemUseCase(RelayListType.Single),
+ ) { recents, relayList, customList ->
+ recents?.mapNotNull { recent ->
+ val relayListItem = recent.location.findItem(customList, relayList)
+
+ relayListItem?.let { Hop.Single(it) }
+ }
+ }
+
+ private fun multiHopRecents(): Flow<List<Hop.Multi>?> =
+ combine(
+ recents().map { it?.filterIsInstance<Recent.Multihop>() },
filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY)),
customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY)),
filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)),
customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)),
) { recents, entryRelayList, entryCustomLists, exitRelayList, exitCustomLists ->
recents?.mapNotNull { recent ->
- when (recent) {
- is Recent.Multihop -> {
- val entry = recent.entry.findItem(entryCustomLists, entryRelayList)
- val exit = recent.exit.findItem(exitCustomLists, exitRelayList)
-
- if (entry != null && exit != null) {
- Hop.Multi(entry, exit)
- } else {
- null
- }
- }
- is Recent.Singlehop -> {
- val relayListItem = recent.location.findItem(exitCustomLists, exitRelayList)
+ val entry = recent.entry.findItem(entryCustomLists, entryRelayList)
+ val exit = recent.exit.findItem(exitCustomLists, exitRelayList)
- relayListItem?.let { Hop.Single(it) }
- }
+ if (entry != null && exit != null) {
+ Hop.Multi(entry, exit)
+ } else {
+ null
}
}
}
@@ -66,7 +78,7 @@ class RecentsUseCase(
relayList: List<RelayItem.Location.Country>,
): RelayItem? =
when (this) {
- is CustomListId -> customLists.firstOrNull { this == it.id }
+ is CustomListId -> customLists.firstOrNull { this == it.id && it.hasChildren }
is GeoLocationId -> relayList.findByGeoLocationId(this)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
index 243c0c643a..6604a76805 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
@@ -4,15 +4,19 @@ import kotlin.collections.mapNotNull
import kotlinx.coroutines.flow.combine
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.relaylist.filter
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
+import net.mullvad.mullvadvpn.util.ipVersionConstraint
+import net.mullvad.mullvadvpn.util.isDaitaAndDirectOnly
+import net.mullvad.mullvadvpn.util.isQuicEnabled
import net.mullvad.mullvadvpn.util.shouldFilterByDaita
+import net.mullvad.mullvadvpn.util.shouldFilterByQuic
class FilterCustomListsRelayItemUseCase(
private val customListsRelayItemUseCase: CustomListsRelayItemUseCase,
@@ -34,9 +38,15 @@ class FilterCustomListsRelayItemUseCase(
providers = selectedProviders,
daita =
shouldFilterByDaita(
- daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
+ daitaDirectOnly = settings?.isDaitaAndDirectOnly() == true,
relayListType = relayListType,
),
+ quic =
+ shouldFilterByQuic(
+ settings?.isQuicEnabled() == true,
+ relayListType = relayListType,
+ ),
+ ipVersion = settings?.ipVersionConstraint() ?: Constraint.Any,
)
}
@@ -44,9 +54,9 @@ class FilterCustomListsRelayItemUseCase(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>,
daita: Boolean,
- ) = mapNotNull { it.filter(ownership, providers, daita = daita) }
-
- private fun Settings.daitaAndDirectOnly() =
- tunnelOptions.wireguard.daitaSettings.enabled &&
- tunnelOptions.wireguard.daitaSettings.directOnly
+ quic: Boolean,
+ ipVersion: Constraint<IpVersion>,
+ ) = mapNotNull {
+ it.filter(ownership, providers, daita = daita, quic = quic, ipVersion = ipVersion)
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Filter.kt
index e4a0a9957a..4fa7ba231c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Filter.kt
@@ -9,3 +9,10 @@ fun shouldFilterByDaita(daitaDirectOnly: Boolean, relayListType: RelayListType)
is RelayListType.Multihop ->
daitaDirectOnly && relayListType.multihopRelayListType == MultihopRelayListType.ENTRY
}
+
+fun shouldFilterByQuic(isQuicEnabled: Boolean, relayListType: RelayListType) =
+ when (relayListType) {
+ RelayListType.Single -> isQuicEnabled
+ is RelayListType.Multihop ->
+ isQuicEnabled && relayListType.multihopRelayListType == MultihopRelayListType.ENTRY
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Settings.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Settings.kt
new file mode 100644
index 0000000000..4da463ab51
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Settings.kt
@@ -0,0 +1,36 @@
+package net.mullvad.mullvadvpn.util
+
+import net.mullvad.mullvadvpn.lib.model.DnsState
+import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
+import net.mullvad.mullvadvpn.lib.model.Settings
+
+fun Settings.quantumResistant() = tunnelOptions.wireguard.quantumResistant
+
+fun Settings.isCustomDnsEnabled() = tunnelOptions.dnsOptions.state == DnsState.Custom
+
+fun Settings.customDnsAddresses() = tunnelOptions.dnsOptions.customOptions.addresses
+
+fun Settings.contentBlockersSettings() = tunnelOptions.dnsOptions.defaultOptions
+
+fun Settings.selectedObfuscationMode() = obfuscationSettings.selectedObfuscationMode
+
+fun Settings.wireguardPort() = relaySettings.relayConstraints.wireguardConstraints.port
+
+fun Settings.deviceIpVersion() = relaySettings.relayConstraints.wireguardConstraints.ipVersion
+
+fun Settings.isDaitaAndDirectOnly() = isDaitaEnabled() && isDaitaDirectOnly()
+
+fun Settings.isQuicEnabled() = obfuscationSettings.selectedObfuscationMode == ObfuscationMode.Quic
+
+fun Settings.ipVersionConstraint() = relaySettings.relayConstraints.wireguardConstraints.ipVersion
+
+fun Settings.isDaitaEnabled() = daitaSettings().enabled
+
+fun Settings.isDaitaDirectOnly() = daitaSettings().directOnly
+
+fun Settings.shadowSocksPort() = obfuscationSettings.shadowsocks.port
+
+fun Settings.isMultihopEnabled() =
+ relaySettings.relayConstraints.wireguardConstraints.isMultihopEnabled
+
+private fun Settings.daitaSettings() = tunnelOptions.wireguard.daitaSettings
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
index f941b26455..fdaa9c7eb6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt
@@ -10,9 +10,10 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DaitaUiState
-import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.util.Lc
+import net.mullvad.mullvadvpn.util.isDaitaDirectOnly
+import net.mullvad.mullvadvpn.util.isDaitaEnabled
import net.mullvad.mullvadvpn.util.toLc
class DaitaViewModel(
@@ -27,8 +28,8 @@ class DaitaViewModel(
.filterNotNull()
.map { settings ->
DaitaUiState(
- daitaEnabled = settings.daitaSettings().enabled,
- directOnly = settings.daitaSettings().directOnly,
+ daitaEnabled = settings.isDaitaEnabled(),
+ directOnly = settings.isDaitaDirectOnly(),
navArgs.isModal,
)
.toLc<Boolean, DaitaUiState>()
@@ -46,6 +47,4 @@ class DaitaViewModel(
fun setDirectOnly(enable: Boolean) {
viewModelScope.launch { settingsRepository.setDaitaDirectOnly(enable) }
}
-
- private fun Settings.daitaSettings() = tunnelOptions.wireguard.daitaSettings
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
index c79682039b..d5d733e8da 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.DeleteCustomDnsUseCase
+import net.mullvad.mullvadvpn.util.customDnsAddresses
import org.apache.commons.validator.routines.InetAddressValidator
sealed interface DnsDialogSideEffect {
@@ -80,7 +81,9 @@ class DnsDialogViewModel(
DnsDialogViewState(
input = input,
validationError =
- input.validateDnsEntry(currentIndex, settings.addresses()).leftOrNull(),
+ input
+ .validateDnsEntry(currentIndex, settings.customDnsAddresses())
+ .leftOrNull(),
isAllowLanEnabled = settings.allowLan,
isIpv6Enabled = settings.tunnelOptions.genericOptions.enableIpv6,
index = currentIndex,
@@ -169,8 +172,6 @@ class DnsDialogViewModel(
}
}
- private fun Settings.addresses() = tunnelOptions.dnsOptions.customOptions.addresses
-
companion object {
private const val EMPTY_STRING = ""
}
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
index fa0e886fee..c7c9e8d900 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt
@@ -16,9 +16,9 @@ import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState
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.SettingsRepository
import net.mullvad.mullvadvpn.util.Lc
+import net.mullvad.mullvadvpn.util.shadowSocksPort
import net.mullvad.mullvadvpn.util.toLc
class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepository) :
@@ -31,7 +31,7 @@ class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepos
settings,
customPort ->
ShadowsocksSettingsUiState(
- port = settings.getShadowSocksPort(),
+ port = settings.shadowSocksPort(),
customPort = customPort,
)
.toLc<Unit, ShadowsocksSettingsUiState>()
@@ -46,7 +46,7 @@ class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepos
viewModelScope.launch {
val initialSettings = settingsRepository.settingsUpdates.filterNotNull().first()
customPort.update {
- val initialPort = initialSettings.getShadowSocksPort()
+ val initialPort = initialSettings.shadowSocksPort()
if (initialPort.getOrNull() !in SHADOWSOCKS_PRESET_PORTS) {
initialPort.getOrNull()
} else {
@@ -79,6 +79,4 @@ class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepos
}
}
}
-
- private fun Settings.getShadowSocksPort() = obfuscationSettings.shadowsocks.port
}
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 3f9a727b38..9ba3e00995 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
@@ -33,15 +33,21 @@ 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
-import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
import net.mullvad.mullvadvpn.util.Lc
+import net.mullvad.mullvadvpn.util.contentBlockersSettings
+import net.mullvad.mullvadvpn.util.customDnsAddresses
+import net.mullvad.mullvadvpn.util.deviceIpVersion
+import net.mullvad.mullvadvpn.util.isCustomDnsEnabled
import net.mullvad.mullvadvpn.util.onFirst
+import net.mullvad.mullvadvpn.util.quantumResistant
+import net.mullvad.mullvadvpn.util.selectedObfuscationMode
import net.mullvad.mullvadvpn.util.toLc
+import net.mullvad.mullvadvpn.util.wireguardPort
sealed interface VpnSettingsSideEffect {
sealed interface ShowToast : VpnSettingsSideEffect {
@@ -75,7 +81,7 @@ class VpnSettingsViewModel(
combine(
settingsRepository.settingsUpdates.filterNotNull().onFirst {
// Initialize wg port and content blockers state expand state
- val initialPort = it.getWireguardPort().getOrNull()
+ val initialPort = it.wireguardPort().getOrNull()
customPort.value =
Some(
if (initialPort !in WIREGUARD_PRESET_PORTS) {
@@ -101,19 +107,19 @@ class VpnSettingsViewModel(
mtu = settings.tunnelOptions.wireguard.mtu,
isLocalNetworkSharingEnabled = settings.allowLan,
isCustomDnsEnabled = settings.isCustomDnsEnabled(),
- customDnsItems = settings.addresses().asStringAddressList(),
+ customDnsItems = settings.customDnsAddresses().asStringAddressList(),
contentBlockersOptions = settings.contentBlockersSettings(),
obfuscationMode = settings.selectedObfuscationMode(),
selectedUdp2TcpObfuscationPort = settings.obfuscationSettings.udp2tcp.port,
selectedShadowsocksObfuscationPort =
settings.obfuscationSettings.shadowsocks.port,
quantumResistant = settings.quantumResistant(),
- selectedWireguardPort = settings.getWireguardPort(),
+ selectedWireguardPort = settings.wireguardPort(),
customWireguardPort = customWgPort,
availablePortRanges = portRanges,
systemVpnSettingsAvailable = systemVpnSettingsUseCase(),
autoStartAndConnectOnBoot = autoStartAndConnectOnBoot,
- deviceIpVersion = settings.getDeviceIpVersion(),
+ deviceIpVersion = settings.deviceIpVersion(),
isIpv6Enabled = settings.tunnelOptions.genericOptions.enableIpv6,
isContentBlockersExpanded = isContentBlockersExpanded,
isModal = navArgs.isModal,
@@ -138,7 +144,7 @@ class VpnSettingsViewModel(
return@launch
}
- val hasDnsEntries = settings.addresses().isNotEmpty()
+ val hasDnsEntries = settings.customDnsAddresses().isNotEmpty()
if (hasDnsEntries) {
settingsRepository
@@ -258,22 +264,6 @@ class VpnSettingsViewModel(
)
}
- private fun Settings.quantumResistant() = tunnelOptions.wireguard.quantumResistant
-
- private fun Settings.isCustomDnsEnabled() = tunnelOptions.dnsOptions.state == DnsState.Custom
-
- private fun Settings.addresses() = tunnelOptions.dnsOptions.customOptions.addresses
-
- private fun Settings.contentBlockersSettings() = tunnelOptions.dnsOptions.defaultOptions
-
- private fun Settings.selectedObfuscationMode() = obfuscationSettings.selectedObfuscationMode
-
- private fun Settings.getWireguardPort() =
- relaySettings.relayConstraints.wireguardConstraints.port
-
- private fun Settings.getDeviceIpVersion() =
- relaySettings.relayConstraints.wireguardConstraints.ipVersion
-
private fun InetAddress.isLocalAddress(): Boolean = isLinkLocalAddress || isSiteLocalAddress
fun showApplySettingChangesWarningToast() =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/LocationUtil.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/LocationUtil.kt
new file mode 100644
index 0000000000..2561f7f007
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/LocationUtil.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.viewmodel.location
+
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
+import net.mullvad.mullvadvpn.compose.state.RelayListType
+import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.util.isDaitaDirectOnly
+import net.mullvad.mullvadvpn.util.isDaitaEnabled
+import net.mullvad.mullvadvpn.util.isMultihopEnabled
+
+// If Daita is enabled without direct only we should block selection, search and hide filters for
+// the multihop enry list
+internal fun RelayListType.isEntryAndBlocked(settings: Settings?): Boolean {
+ val isMultihopEntry = isMultihopEntry()
+
+ if (!isMultihopEntry) {
+ return false
+ }
+
+ return settings?.entryBlocked() == true
+}
+
+private fun Settings.entryBlocked() =
+ isDaitaEnabled() && !isDaitaDirectOnly() && isMultihopEnabled()
+
+private fun RelayListType.isMultihopEntry() =
+ this is RelayListType.Multihop && multihopRelayListType == MultihopRelayListType.ENTRY
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
index 1d6691b755..d1c8a6ba61 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
@@ -121,17 +121,8 @@ private fun createRecentsSection(
): List<RelayListItem> = buildList {
add(RelayListItem.RecentsListHeader)
- val selectionIsSingle = itemSelection is RelayItemSelection.Single
- val selectionIsMulti = itemSelection is RelayItemSelection.Multiple
-
val shown =
recents
- .filter { recent ->
- when (recent) {
- is Hop.Multi -> selectionIsMulti
- is Hop.Single<*> -> selectionIsSingle
- }
- }
.map { recent ->
val isSelected = recent.matches(itemSelection, isEntryBlocked)
if (isEntryBlocked) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
index 30e45d05cf..ff67946361 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
@@ -13,7 +13,6 @@ import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItemId
-import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
@@ -44,7 +43,7 @@ class SelectLocationListViewModel(
customListsRelayItemUseCase(),
settingsRepository.settingsUpdates,
) { relayListItems, customLists, settings ->
- if (settings.isBlocked()) {
+ if (relayListType.isEntryAndBlocked(settings)) {
Lce.Error(Unit)
} else {
Lce.Content(
@@ -62,19 +61,11 @@ class SelectLocationListViewModel(
_expandedItems.onToggleExpandSet(item, parent, expand)
}
- private fun Settings?.isBlocked(): Boolean =
- when (relayListType) {
- RelayListType.Single -> false
- is RelayListType.Multihop ->
- relayListType.multihopRelayListType == MultihopRelayListType.ENTRY &&
- this?.entryBlocked() == true
- }
-
private fun relayListItems() =
combine(
filteredRelayListUseCase(relayListType = relayListType),
filteredCustomListRelayItemsUseCase(relayListType = relayListType),
- recentsUseCase(),
+ recentsUseCase(isMultihop = relayListType is RelayListType.Multihop),
selectedLocationUseCase(),
_expandedItems,
) { relayCountries, customLists, recents, selectedItem, expandedItems ->
@@ -103,7 +94,7 @@ class SelectLocationListViewModel(
selectedItem.selectedByOtherEntryExitList(relayListType, customLists),
expandedItems = expandedItems,
isEntryBlocked =
- settingsRepository.settingsUpdates.value?.entryBlocked() == true,
+ relayListType.isEntryAndBlocked(settingsRepository.settingsUpdates.value),
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
index 768cf794eb..bf3abeaf91 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
@@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.Hop
import net.mullvad.mullvadvpn.lib.model.Recents
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
@@ -69,13 +70,20 @@ class SelectLocationViewModel(
) { filterChips, wireguardConstraints, relayListSelection, relayList, settings ->
Lc.Content(
SelectLocationUiState(
- filterChips = filterChips,
+ filterChips =
+ // Hide filter chips when entry and blocked
+ if (relayListSelection.isEntryAndBlocked(settings)) {
+ emptyList()
+ } else {
+ filterChips
+ },
multihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
relayListType = relayListSelection,
isSearchButtonEnabled =
searchButtonEnabled(
relayList = relayList,
relayListSelection = relayListSelection,
+ settings = settings,
),
isFilterButtonEnabled = relayList.isNotEmpty(),
isRecentsEnabled = settings?.recents is Recents.Enabled,
@@ -92,13 +100,11 @@ class SelectLocationViewModel(
private fun searchButtonEnabled(
relayList: List<RelayItem.Location.Country>,
relayListSelection: RelayListType,
+ settings: Settings?,
): Boolean {
val hasRelayListItems = relayList.isNotEmpty()
- val isMultihopEntry =
- relayListSelection is RelayListType.Multihop &&
- relayListSelection.multihopRelayListType == MultihopRelayListType.ENTRY
- val isEntryBlocked = settingsRepository.settingsUpdates.value?.entryBlocked() == true
- return hasRelayListItems && !(isMultihopEntry && isEntryBlocked)
+ val isEntryAndBlocked = relayListSelection.isEntryAndBlocked(settings = settings)
+ return hasRelayListItems && !isEntryAndBlocked
}
fun selectRelayList(multihopRelayListType: MultihopRelayListType) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SettingsUtil.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SettingsUtil.kt
deleted file mode 100644
index 05245f2503..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SettingsUtil.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.viewmodel.location
-
-import net.mullvad.mullvadvpn.lib.model.Settings
-
-// If Daita is enabled without direct only, it is not possible to manually select the entry
-// location.
-internal fun Settings.entryBlocked() =
- tunnelOptions.wireguard.daitaSettings.enabled &&
- !tunnelOptions.wireguard.daitaSettings.directOnly &&
- relaySettings.relayConstraints.wireguardConstraints.isMultihopEnabled
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
index 9623fffc7c..09759116a4 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
@@ -47,67 +47,65 @@ class RecentsUseCaseTest {
@Test
fun `given null settings when invoke then emit null`() = runTest {
+ // Arrange
settingsFlow.value = null
every { customListsRelayItemUseCase(any()) } returns flowOf(emptyList())
every { filteredRelayListUseCase(any()) } returns flowOf(emptyList())
- useCase().test { assertNull(awaitItem()) }
+ // Act, Assert
+ useCase(isMultihop = false).test { assertNull(awaitItem()) }
}
@Test
fun `given recents disabled when invoke then emit null`() = runTest {
+ // Arrange
settingsFlow.value = mockk<Settings> { every { recents } returns Recents.Disabled }
every { customListsRelayItemUseCase(any()) } returns flowOf(emptyList())
every { filteredRelayListUseCase(any()) } returns flowOf(emptyList())
- useCase().test { assertNull(awaitItem()) }
+ // Act, Assert
+ useCase(isMultihop = false).test { assertNull(awaitItem()) }
}
@Test
fun `given recents enabled but empty when invoke then emit empty list`() = runTest {
+ // Arrange
settingsFlow.value =
mockk<Settings> { every { recents } returns Recents.Enabled(emptyList()) }
every { customListsRelayItemUseCase(any()) } returns flowOf(emptyList())
every { filteredRelayListUseCase(any()) } returns flowOf(emptyList())
- useCase().test { assertEquals(emptyList(), awaitItem()) }
+ // Act, Assert
+ useCase(isMultihop = false).test { assertEquals(emptyList(), awaitItem()) }
}
@Test
- fun `given recents enabled when invoke then emit hops based on the relay item filters`() =
- runTest {
- val swedenId = GeoLocationId.Country("se")
- val stockholmId = GeoLocationId.City(swedenId, "sto")
- val sweden =
- RelayItem.Location.Country(
- id = swedenId,
- name = "Sweden",
- cities =
- listOf(
- RelayItem.Location.City(
- id = stockholmId,
- name = "Stockholm",
- relays = emptyList(),
- )
- ),
- )
-
- val norwayId = GeoLocationId.Country("no")
- val norway =
- RelayItem.Location.Country(id = norwayId, name = "Norway", cities = emptyList())
+ fun `given recent custom list with no children should not emit that recent`() = runTest {
+ // Arrange
+ val id = CustomListId("id")
+ val customList =
+ RelayItem.CustomList(
+ customList =
+ CustomList(
+ id = id,
+ name = CustomListName.fromString("name"),
+ locations = emptyList(),
+ ),
+ locations = emptyList(),
+ )
+ val recent = Recent.Singlehop(location = id)
+ settingsFlow.value =
+ mockk<Settings> { every { recents } returns Recents.Enabled(listOf(recent)) }
+ every { customListsRelayItemUseCase(any()) } returns flowOf(listOf(customList))
+ every { filteredRelayListUseCase(any()) } returns flowOf(emptyList())
- val entryCustomListId = CustomListId("custom")
- val customList =
- CustomList(
- id = entryCustomListId,
- name = CustomListName.fromString("Custom"),
- locations = listOf(swedenId, norwayId),
- )
- val entryCustomList =
- RelayItem.CustomList(customList = customList, locations = emptyList())
+ useCase(isMultihop = false).test { assertEquals(emptyList(), awaitItem()) }
+ }
- val singleHopRecent = Recent.Singlehop(stockholmId)
- val multiHopRecent = Recent.Multihop(entry = entryCustomListId, exit = norwayId)
+ @Test
+ fun `given recents enabled when invoke then emit hops based on the relay item filters`() =
+ runTest {
+ val singleHopRecent = Recent.Singlehop(STOCKHOLM_ID)
val filteredOutRecent =
Recent.Singlehop(
GeoLocationId.City(country = GeoLocationId.Country("xx"), code = "xx-xxx-xx")
@@ -116,30 +114,72 @@ class RecentsUseCaseTest {
settingsFlow.value =
mockk<Settings> {
every { recents } returns
- Recents.Enabled(listOf(singleHopRecent, multiHopRecent, filteredOutRecent))
+ Recents.Enabled(listOf(singleHopRecent, filteredOutRecent))
}
- every {
- customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
- } returns flowOf(listOf(entryCustomList))
- every {
- customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
- } returns flowOf(emptyList())
- every {
- filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
- } returns flowOf(listOf(sweden, norway))
- every {
- filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
- } returns flowOf(listOf(sweden, norway))
+ every { customListsRelayItemUseCase(RelayListType.Single) } returns flowOf(emptyList())
+ every { filteredRelayListUseCase(RelayListType.Single) } returns
+ flowOf(listOf(SWEDEN, NORWAY))
- useCase().test {
+ useCase(isMultihop = false).test {
val hops = awaitItem()
- val stockholmCity = sweden.cities.first()
-
- val expectedHops =
- listOf(Hop.Single(stockholmCity), Hop.Multi(entryCustomList, norway))
+ val expectedHops = listOf(Hop.Single(STOCKHOLM))
assertEquals(expectedHops, hops)
}
}
+
+ @Test
+ fun `given multihop true should filter out singlehop recents`() = runTest {
+ val singleHopRecent = Recent.Singlehop(STOCKHOLM_ID)
+ val multiHopRecent = Recent.Multihop(entry = CUSTOM_LIST_ID, exit = NORWAY_ID)
+
+ settingsFlow.value =
+ mockk<Settings> {
+ every { recents } returns Recents.Enabled(listOf(singleHopRecent, multiHopRecent))
+ }
+
+ every {
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
+ } returns flowOf(listOf(CUSTOM_LIST_SWE_NO))
+ every {
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
+ } returns flowOf(emptyList())
+ every {
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
+ } returns flowOf(listOf(SWEDEN, NORWAY))
+ every {
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
+ } returns flowOf(listOf(SWEDEN, NORWAY))
+
+ useCase(isMultihop = true).test {
+ val hops = awaitItem()
+
+ val expectedHops = listOf(Hop.Multi(CUSTOM_LIST_SWE_NO, NORWAY))
+ assertEquals(expectedHops, hops)
+ }
+ }
+
+ companion object {
+ private val SWEDEN_ID = GeoLocationId.Country("se")
+ private val STOCKHOLM_ID = GeoLocationId.City(SWEDEN_ID, "sto")
+ private val STOCKHOLM =
+ RelayItem.Location.City(id = STOCKHOLM_ID, name = "Stockholm", relays = emptyList())
+ private val SWEDEN =
+ RelayItem.Location.Country(id = SWEDEN_ID, name = "Sweden", cities = listOf(STOCKHOLM))
+ private val NORWAY_ID = GeoLocationId.Country("no")
+ private val NORWAY =
+ RelayItem.Location.Country(id = NORWAY_ID, name = "Norway", cities = emptyList())
+ private val CUSTOM_LIST_ID = CustomListId("custom")
+ private val CUSTOM_LIST_SWE_NO =
+ RelayItem.CustomList(
+ customList =
+ CustomList(
+ id = CUSTOM_LIST_ID,
+ name = CustomListName.fromString("Custom"),
+ locations = listOf(SWEDEN_ID, NORWAY_ID),
+ ),
+ locations = listOf(SWEDEN, NORWAY),
+ )
+ }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
index a3257f04d9..fc6dcf79c0 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
@@ -364,7 +364,7 @@ class CustomListLocationsViewModelTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
),
)
@@ -382,7 +382,7 @@ class CustomListLocationsViewModelTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
index 3f9bfe751a..8c01d592e5 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
@@ -67,7 +67,7 @@ class SelectLocationListViewModelTest {
filteredCustomListRelayItems
every { mockCustomListRelayItemsUseCase() } returns customListRelayItems
every { mockSettingsRepository.settingsUpdates } returns settings
- every { recentsUseCase() } returns recentsRelayItems
+ every { recentsUseCase(any()) } returns recentsRelayItems
}
@Test
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
index e50cfb48a2..b633e3402f 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
@@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
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.CustomList
import net.mullvad.mullvadvpn.lib.model.CustomListId
@@ -40,6 +41,7 @@ import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
+import net.mullvad.mullvadvpn.usecase.ModelOwnership
import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
import net.mullvad.mullvadvpn.usecase.MultihopChange
import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
@@ -300,6 +302,51 @@ class SelectLocationViewModelTest {
}
}
+ @Test
+ fun `given entry blocked should not emit any filter if in entry list`() = runTest {
+ // Arrange
+ val mockSettings = mockk<Settings>(relaxed = true)
+ settings.value = mockSettings
+ every { mockSettings.tunnelOptions.wireguard.daitaSettings.enabled } returns true
+ every { mockSettings.tunnelOptions.wireguard.daitaSettings.directOnly } returns false
+ every {
+ mockSettings.relaySettings.relayConstraints.wireguardConstraints.isMultihopEnabled
+ } returns true
+ filterChips.value = listOf(FilterChip.Quic, FilterChip.Daita)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Initial state
+ viewModel.selectRelayList(MultihopRelayListType.ENTRY)
+ val state = awaitItem()
+ assertIs<Lc.Content<SelectLocationUiState>>(state)
+ assert(state.value.filterChips.isEmpty())
+ }
+ }
+
+ @Test
+ fun `given entry blocked should emit filters if in exit list`() = runTest {
+ // Arrange
+ val mockSettings = mockk<Settings>(relaxed = true)
+ val expectedFilters = listOf(FilterChip.Ownership(ModelOwnership.MullvadOwned))
+ settings.value = mockSettings
+ filterChips.value = expectedFilters
+ every { mockSettings.tunnelOptions.wireguard.daitaSettings.enabled } returns true
+ every { mockSettings.tunnelOptions.wireguard.daitaSettings.directOnly } returns false
+ every {
+ mockSettings.relaySettings.relayConstraints.wireguardConstraints.isMultihopEnabled
+ } returns true
+
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Initial state
+ viewModel.selectRelayList(MultihopRelayListType.EXIT)
+ val state = awaitItem()
+ assertIs<Lc.Content<SelectLocationUiState>>(state)
+ assertLists(expectedFilters, state.value.filterChips)
+ }
+ }
+
companion object {
private const val RELAY_LIST_EXTENSIONS =
"net.mullvad.mullvadvpn.relaylist.RelayListExtensionsKt"
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 113a5fc847..2424ff00a7 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
@@ -92,6 +92,7 @@ internal fun ObfuscationMode.fromDomain():
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP
ObfuscationMode.Shadowsocks ->
ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS
+ ObfuscationMode.Quic -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.QUIC
ObfuscationMode.Auto -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO
ObfuscationMode.Off -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF
}
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 140cf5aafb..9b4dd07056 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
@@ -59,6 +59,7 @@ import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.model.ProviderId
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.Quic
import net.mullvad.mullvadvpn.lib.model.Recent
import net.mullvad.mullvadvpn.lib.model.Recents
import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess
@@ -207,6 +208,8 @@ internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndp
obfuscationType = obfuscationType.toDomain(),
)
+private fun String.toInetAddress(): InetAddress = InetAddress.getByName(this)
+
private fun String.toInetSocketAddress(): InetSocketAddress {
val indexOfSeparator = indexOfLast { it == ':' }
val ipPart = substring(0, indexOfSeparator).filter { it !in listOf('[', ']') }
@@ -219,8 +222,7 @@ internal fun ManagementInterface.ObfuscationEndpoint.ObfuscationType.toDomain():
ManagementInterface.ObfuscationEndpoint.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp
ManagementInterface.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS ->
ObfuscationType.Shadowsocks
- ManagementInterface.ObfuscationEndpoint.ObfuscationType.QUIC ->
- throw IllegalArgumentException("Unsupported obfuscation type")
+ ManagementInterface.ObfuscationEndpoint.ObfuscationType.QUIC -> ObfuscationType.Quic
ManagementInterface.ObfuscationEndpoint.ObfuscationType.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized obfuscation type")
}
@@ -426,8 +428,7 @@ internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomai
ObfuscationMode.Udp2Tcp
ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS ->
ObfuscationMode.Shadowsocks
- ManagementInterface.ObfuscationSettings.SelectedObfuscation.QUIC ->
- throw IllegalArgumentException("Unsupported obfuscation type")
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.QUIC -> ObfuscationMode.Quic
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized selected obfuscation")
}
@@ -592,9 +593,17 @@ internal fun ManagementInterface.Relay.toDomain(
provider = ProviderId(provider),
ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented,
daita = endpointData.wireguard.daita,
- quic = endpointData.wireguard.hasQuic(),
+ quic =
+ if (endpointData.wireguard.hasQuic()) {
+ endpointData.wireguard.quic.toDomain()
+ } else {
+ null
+ },
)
+private fun ManagementInterface.Relay.RelayData.Wireguard.Quic.toDomain(): Quic =
+ Quic(inAddresses = addrInList.map { it.toInetAddress() })
+
private fun Instant.atDefaultZone() = atZone(ZoneId.systemDefault())
internal fun ManagementInterface.Device.toDomain(): Device =
@@ -695,6 +704,7 @@ internal fun ManagementInterface.FeatureIndicators.toDomain(): List<FeatureIndic
internal fun ManagementInterface.TunnelOptions.GenericOptions.toDomain(): GenericOptions =
GenericOptions(enableIpv6 = enableIpv6)
+@Suppress("ComplexMethod")
internal fun ManagementInterface.FeatureIndicator.toDomain() =
when (this) {
ManagementInterface.FeatureIndicator.QUANTUM_RESISTANCE ->
@@ -712,7 +722,7 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() =
ManagementInterface.FeatureIndicator.SHADOWSOCKS -> FeatureIndicator.SHADOWSOCKS
ManagementInterface.FeatureIndicator.MULTIHOP -> FeatureIndicator.MULTIHOP
ManagementInterface.FeatureIndicator.DAITA_MULTIHOP -> FeatureIndicator.DAITA_MULTIHOP
- ManagementInterface.FeatureIndicator.QUIC,
+ ManagementInterface.FeatureIndicator.QUIC -> FeatureIndicator.QUIC
ManagementInterface.FeatureIndicator.LOCKDOWN_MODE,
ManagementInterface.FeatureIndicator.BRIDGE_MODE,
ManagementInterface.FeatureIndicator.CUSTOM_MSS_FIX,
diff --git a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt
index e45c8087fd..310b2ab1fd 100644
--- a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt
+++ b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt
@@ -26,7 +26,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay10 =
RelayItem.Location.Relay(
@@ -35,7 +35,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relay9 assertOrderBothDirection relay10
@@ -50,7 +50,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay9b =
RelayItem.Location.Relay(
@@ -59,7 +59,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0)
@@ -75,7 +75,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay1 =
RelayItem.Location.Relay(
@@ -84,7 +84,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay3 =
RelayItem.Location.Relay(
@@ -93,7 +93,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay100 =
RelayItem.Location.Relay(
@@ -102,7 +102,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relay001 assertOrderBothDirection relay1
@@ -120,7 +120,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay9b =
RelayItem.Location.Relay(
@@ -129,7 +129,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0)
@@ -145,7 +145,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay005 =
RelayItem.Location.Relay(
@@ -154,7 +154,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relay001 assertOrderBothDirection relay005
@@ -169,7 +169,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relayAr8 =
RelayItem.Location.Relay(
@@ -178,7 +178,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relaySe5 =
RelayItem.Location.Relay(
@@ -187,7 +187,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relaySe10 =
RelayItem.Location.Relay(
@@ -196,7 +196,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relayAr2 assertOrderBothDirection relayAr8
@@ -213,7 +213,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay2w =
RelayItem.Location.Relay(
@@ -222,7 +222,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relay2c assertOrderBothDirection relay2w
@@ -237,7 +237,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
val relay22b =
RelayItem.Location.Relay(
@@ -246,7 +246,7 @@ class RelayNameComparatorTest {
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = false,
- quic = false,
+ quic = null,
)
relay22a assertOrderBothDirection relay22b
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 0213c06cef..6d7951749b 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
@@ -9,6 +9,7 @@ enum class FeatureIndicator {
SPLIT_TUNNELING,
UDP_2_TCP,
SHADOWSOCKS,
+ QUIC,
LAN_SHARING,
DNS_CONTENT_BLOCKERS,
CUSTOM_DNS,
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt
index 7e4101e973..8926ded829 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationMode.kt
@@ -5,4 +5,5 @@ enum class ObfuscationMode {
Off,
Udp2Tcp,
Shadowsocks,
+ Quic,
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
index 80c2f70e13..5eb0ad5548 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
@@ -3,4 +3,5 @@ package net.mullvad.mullvadvpn.lib.model
enum class ObfuscationType {
Udp2Tcp,
Shadowsocks,
+ Quic,
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Quic.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Quic.kt
new file mode 100644
index 0000000000..01ebd96d3e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Quic.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import java.net.InetAddress
+
+data class Quic(val inAddresses: List<InetAddress>) {
+ val supportsIpv4 = inAddresses.any { it is java.net.Inet4Address }
+ val supportsIpv6 = inAddresses.any { it is java.net.Inet6Address }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
index b1df67fea6..197e8e95ce 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
@@ -85,7 +85,7 @@ sealed interface RelayItem {
val ownership: Ownership,
override val active: Boolean,
val daita: Boolean,
- val quic: Boolean,
+ val quic: Quic?,
) : Location {
override val name: String = id.code
override val hasChildren: Boolean = false
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 be26ab6e10..a395a1917d 100644
--- a/android/lib/resource/src/main/res/values-da/strings.xml
+++ b/android/lib/resource/src/main/res/values-da/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Brugerdefineret DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s mere ...</string>
+ <string name="feature_obfuscation">Tilsløring</string>
<string name="feature_quantum_resistant">Kvantemodstand</string>
- <string name="feature_udp_2_tcp">Tilsløring</string>
<string name="filter">Filter</string>
<string name="filters">Filtre:</string>
<string name="foreground_notification_channel_description">Viser den aktuelle VPN-tunnelstatus</string>
diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml
index 12e43b5d9f..9070ee0abe 100644
--- a/android/lib/resource/src/main/res/values-de/strings.xml
+++ b/android/lib/resource/src/main/res/values-de/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Eigenes DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s weitere …</string>
+ <string name="feature_obfuscation">Verschleierung</string>
<string name="feature_quantum_resistant">Quantenresistenz</string>
- <string name="feature_udp_2_tcp">Verschleierung</string>
<string name="filter">Filter</string>
<string name="filters">Filter:</string>
<string name="foreground_notification_channel_description">Zeigt den aktuellen Status des VPN-Tunnels an</string>
diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml
index b76ec33c1d..0a35370514 100644
--- a/android/lib/resource/src/main/res/values-es/strings.xml
+++ b/android/lib/resource/src/main/res/values-es/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">DNS personalizado</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s más...</string>
+ <string name="feature_obfuscation">Ofuscación</string>
<string name="feature_quantum_resistant">Resistencia cuántica</string>
- <string name="feature_udp_2_tcp">Ofuscación</string>
<string name="filter">Filtrar</string>
<string name="filters">Filtros:</string>
<string name="foreground_notification_channel_description">Muestra el estado actual del túnel VPN</string>
diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml
index aca2052934..b90c8f021e 100644
--- a/android/lib/resource/src/main/res/values-fi/strings.xml
+++ b/android/lib/resource/src/main/res/values-fi/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Mukautettu DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s lisää...</string>
+ <string name="feature_obfuscation">Hämäysteknologia</string>
<string name="feature_quantum_resistant">Kvanttihyökkäysten esto</string>
- <string name="feature_udp_2_tcp">Hämäysteknologia</string>
<string name="filter">Suodatin</string>
<string name="filters">Suodattimet:</string>
<string name="foreground_notification_channel_description">Näyttää VPN-tunnelin nykyisen tilan</string>
diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml
index e48f6b8bde..d03e810d5a 100644
--- a/android/lib/resource/src/main/res/values-fr/strings.xml
+++ b/android/lib/resource/src/main/res/values-fr/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">DNS personnalisé</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s de plus…</string>
+ <string name="feature_obfuscation">Dissimulation</string>
<string name="feature_quantum_resistant">Résistance quantique</string>
- <string name="feature_udp_2_tcp">Dissimulation</string>
<string name="filter">Filtrer</string>
<string name="filters">Filtres :</string>
<string name="foreground_notification_channel_description">Affiche l\'état actuel du tunnel VPN</string>
diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml
index 3c42bff12a..323129f829 100644
--- a/android/lib/resource/src/main/res/values-it/strings.xml
+++ b/android/lib/resource/src/main/res/values-it/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">DNS personalizzato</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">Altri %1$s...</string>
+ <string name="feature_obfuscation">Offuscamento</string>
<string name="feature_quantum_resistant">Resistenza ad attacchi quantistici</string>
- <string name="feature_udp_2_tcp">Offuscamento</string>
<string name="filter">Filtra</string>
<string name="filters">Filtri:</string>
<string name="foreground_notification_channel_description">Mostra lo stato attuale del tunnel VPN</string>
diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml
index 0019bd8cf6..782968ccd7 100644
--- a/android/lib/resource/src/main/res/values-ja/strings.xml
+++ b/android/lib/resource/src/main/res/values-ja/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">カスタムDNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">その他%1$s件…</string>
+ <string name="feature_obfuscation">難読化</string>
<string name="feature_quantum_resistant">耐量子</string>
- <string name="feature_udp_2_tcp">難読化</string>
<string name="filter">絞り込み</string>
<string name="filters">絞り込み:</string>
<string name="foreground_notification_channel_description">現在のVPNトンネルのステータスを表示します</string>
diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml
index 4d5e18e0cf..f39d896349 100644
--- a/android/lib/resource/src/main/res/values-ko/strings.xml
+++ b/android/lib/resource/src/main/res/values-ko/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">사용자 지정 DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s개 더 보기...</string>
+ <string name="feature_obfuscation">난독 처리</string>
<string name="feature_quantum_resistant">양자 저항</string>
- <string name="feature_udp_2_tcp">난독 처리</string>
<string name="filter">필터</string>
<string name="filters">필터:</string>
<string name="foreground_notification_channel_description">현재 VPN 터널 상태 표시</string>
diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml
index 98b9982cc1..b8fe2478e8 100644
--- a/android/lib/resource/src/main/res/values-my/strings.xml
+++ b/android/lib/resource/src/main/res/values-my/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">စိတ်ကြိုက် DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">နောက်ထပ် %1$s ...</string>
+ <string name="feature_obfuscation">Obfuscation</string>
<string name="feature_quantum_resistant">Quantum ခုခံမှု</string>
- <string name="feature_udp_2_tcp">Obfuscation</string>
<string name="filter">စစ်ထုတ်မှု</string>
<string name="filters">စစ်ထုတ်မှုများ-</string>
<string name="foreground_notification_channel_description">လက်ရှိ VPN Tunnel အခြေအနေကို ပြသပေးပါသည်</string>
diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml
index c0d6622923..ea93ba5503 100644
--- a/android/lib/resource/src/main/res/values-nb/strings.xml
+++ b/android/lib/resource/src/main/res/values-nb/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Tilpasset DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s til …</string>
+ <string name="feature_obfuscation">Tilsløring</string>
<string name="feature_quantum_resistant">Kvantemotstand</string>
- <string name="feature_udp_2_tcp">Tilsløring</string>
<string name="filter">Filter</string>
<string name="filters">Filtre:</string>
<string name="foreground_notification_channel_description">Viser gjeldende VPN-tunnelstatus</string>
diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml
index e1d1619d8e..77d03bc2ea 100644
--- a/android/lib/resource/src/main/res/values-nl/strings.xml
+++ b/android/lib/resource/src/main/res/values-nl/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Aangepaste DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">Nog %1$s...</string>
+ <string name="feature_obfuscation">Obfuscatie</string>
<string name="feature_quantum_resistant">Kwantumbestendigheid</string>
- <string name="feature_udp_2_tcp">Obfuscatie</string>
<string name="filter">Filter</string>
<string name="filters">Filters:</string>
<string name="foreground_notification_channel_description">Toont de huidige status van de VPN-tunnel</string>
diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml
index e9f255c4e2..cd48028048 100644
--- a/android/lib/resource/src/main/res/values-pl/strings.xml
+++ b/android/lib/resource/src/main/res/values-pl/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Niestandardowy serwer DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">Jeszcze %1$s...</string>
+ <string name="feature_obfuscation">Zaciemnianie</string>
<string name="feature_quantum_resistant">Odporność na ataki z użyciem komputerów kwantowych</string>
- <string name="feature_udp_2_tcp">Zaciemnianie</string>
<string name="filter">Filtruj</string>
<string name="filters">Filtry:</string>
<string name="foreground_notification_channel_description">Pokazuje bieżący status tunelu VPN</string>
diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml
index ee85151bc8..f5f6e733a4 100644
--- a/android/lib/resource/src/main/res/values-pt/strings.xml
+++ b/android/lib/resource/src/main/res/values-pt/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">DNS personalizado</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">Mais %1$s...</string>
+ <string name="feature_obfuscation">Ofuscação</string>
<string name="feature_quantum_resistant">Resistência quântica</string>
- <string name="feature_udp_2_tcp">Ofuscação</string>
<string name="filter">Filtrar</string>
<string name="filters">Filtros:</string>
<string name="foreground_notification_channel_description">Indica o estado atual do túnel VPN</string>
diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml
index ce118dc517..2a0d879d57 100644
--- a/android/lib/resource/src/main/res/values-ru/strings.xml
+++ b/android/lib/resource/src/main/res/values-ru/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Пользовательский DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">Еще %1$s...</string>
+ <string name="feature_obfuscation">Обфускация</string>
<string name="feature_quantum_resistant">Квантовая устойчивость</string>
- <string name="feature_udp_2_tcp">Обфускация</string>
<string name="filter">Фильтр</string>
<string name="filters">Фильтры:</string>
<string name="foreground_notification_channel_description">Показывает текущее состояние VPN-туннеля</string>
diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml
index 8656246738..265d7b6716 100644
--- a/android/lib/resource/src/main/res/values-sv/strings.xml
+++ b/android/lib/resource/src/main/res/values-sv/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Anpassad DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s till ...</string>
+ <string name="feature_obfuscation">Obfuskering</string>
<string name="feature_quantum_resistant">Kvantresistens</string>
- <string name="feature_udp_2_tcp">Obfuskering</string>
<string name="filter">Filtrera</string>
<string name="filters">Filter:</string>
<string name="foreground_notification_channel_description">Visar nuvarande status för VPN-tunnel</string>
diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml
index ec273c5e66..13f27815d3 100644
--- a/android/lib/resource/src/main/res/values-th/strings.xml
+++ b/android/lib/resource/src/main/res/values-th/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">DNS แบบกำหนดเอง</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">อีก %1$s...</string>
+ <string name="feature_obfuscation">การทำให้ข้อมูลยุ่งเหยิง</string>
<string name="feature_quantum_resistant">การต่อต้านควอนตัม</string>
- <string name="feature_udp_2_tcp">การทำให้ข้อมูลยุ่งเหยิง</string>
<string name="filter">ตัวกรอง</string>
<string name="filters">ตัวกรอง:</string>
<string name="foreground_notification_channel_description">แสดงสถานะอุโมงค์ VPN ในปัจจุบัน</string>
diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml
index 6a802fe7dc..bf1296c709 100644
--- a/android/lib/resource/src/main/res/values-tr/strings.xml
+++ b/android/lib/resource/src/main/res/values-tr/strings.xml
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">Özel DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">%1$s tane daha...</string>
+ <string name="feature_obfuscation">Gizleme</string>
<string name="feature_quantum_resistant">Kuantum direnci</string>
- <string name="feature_udp_2_tcp">Gizleme</string>
<string name="filter">Filtrele</string>
<string name="filters">Filtreler:</string>
<string name="foreground_notification_channel_description">Mevcut VPN tünelinin durumunu gösterir</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
index fac8961cee..d2ed8c3aff 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
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">自定义 DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">其他 %1$s 个…</string>
+ <string name="feature_obfuscation">混淆</string>
<string name="feature_quantum_resistant">量子阻力</string>
- <string name="feature_udp_2_tcp">混淆</string>
<string name="filter">筛选</string>
<string name="filters">筛选器:</string>
<string name="foreground_notification_channel_description">显示当前的 VPN 隧道状态</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
index d53584ca36..37acbc0a3f 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
@@ -191,8 +191,8 @@
<string name="feature_custom_dns">自訂 DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="feature_indicators_show_more">其他 %1$s 個…</string>
+ <string name="feature_obfuscation">混淆</string>
<string name="feature_quantum_resistant">抗量子</string>
- <string name="feature_udp_2_tcp">混淆</string>
<string name="filter">篩選</string>
<string name="filters">篩選器:</string>
<string name="foreground_notification_channel_description">顯示目前的 VPN 通道狀態</string>
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 44b41aa99a..a6029aa1f2 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -351,7 +351,7 @@
<string name="connect_panel_active_features">Active features</string>
<string name="connect_panel_connection_details">Connection details</string>
<string name="feature_quantum_resistant">Quantum resistance</string>
- <string name="feature_udp_2_tcp">Obfuscation</string>
+ <string name="feature_obfuscation">Obfuscation</string>
<string name="feature_custom_dns">Custom DNS</string>
<string name="feature_custom_mtu">MTU</string>
<string name="dns_content_blockers">DNS content blockers</string>
diff --git a/android/lib/resource/src/main/res/values/strings_non_translatable.xml b/android/lib/resource/src/main/res/values/strings_non_translatable.xml
index 837740aa47..3f2c8b5c29 100644
--- a/android/lib/resource/src/main/res/values/strings_non_translatable.xml
+++ b/android/lib/resource/src/main/res/values/strings_non_translatable.xml
@@ -11,6 +11,7 @@
<string name="wireguard" translatable="false">WireGuard</string>
<string name="socks5_remote">SOCKS5</string>
<string name="shadowsocks">Shadowsocks</string>
+ <string name="quic">QUIC</string>
<string name="local_network_sharing_ip_ranges">
<![CDATA[<ul><li>10.0.0.0/8</li><li>172.16.0.0/12</li><li>192.168.0.0/16</li><li>169.254.0.0/16</li><li>fe80::/10</li><li>fc00::/7</li></ul>]]>
</string>
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
index 724c567595..2e4e228439 100644
--- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
@@ -51,7 +51,7 @@ private fun generateRelayItemRelay(
provider = ProviderId("Provider"),
ownership = Ownership.MullvadOwned,
daita = daita,
- quic = false,
+ quic = null,
)
private fun String.generateCountryCode() =
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
index 2d7757ace1..e9c503e5b6 100644
--- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
@@ -241,6 +241,57 @@ class ConnectionTest : EndToEndTest() {
@Test
@HasDependencyOnLocalAPI
@ClearFirewallRules
+ fun testQuic() = runTest {
+ app.launchAndLogIn(accountTestRule.validAccountNumber)
+ on<ConnectPage> { enableLocalNetworkSharingStory() }
+
+ on<ConnectPage> { clickSelectLocation() }
+
+ on<SelectLocationPage> {
+ val quicRelay = relayProvider.getQuicRelay()
+ clickLocationExpandButton(quicRelay.country)
+ clickLocationExpandButton(quicRelay.city)
+ scrollUntilCell(quicRelay.relay)
+ clickLocationCell(quicRelay.relay)
+ }
+
+ device.acceptVpnPermissionDialog()
+
+ var relayIpAddress: String? = null
+
+ on<ConnectPage> {
+ waitForConnectedLabel()
+ relayIpAddress = extractInIpv4Address()
+ clickDisconnect()
+ }
+
+ // Block UDP traffic to the relay
+ val firewallRule = DropRule.blockWireGuardTrafficRule(relayIpAddress!!)
+ firewallClient.createRule(firewallRule)
+
+ // Enable QUIC
+ on<ConnectPage> { clickSettings() }
+
+ on<SettingsPage> { clickVpnSettings() }
+
+ on<VpnSettingsPage> {
+ scrollUntilWireGuardObfuscationQuicCell()
+ clickWireguardObfuscationQuicCell()
+ }
+
+ device.pressBack()
+ device.pressBack()
+
+ on<ConnectPage> {
+ clickConnect()
+ waitForConnectedLabel(timeout = EXTREMELY_LONG_TIMEOUT)
+ clickDisconnect()
+ }
+ }
+
+ @Test
+ @HasDependencyOnLocalAPI
+ @ClearFirewallRules
fun testShadowsocks() = runTest {
app.launchAndLogIn(accountTestRule.validAccountNumber)
on<ConnectPage> { enableLocalNetworkSharingStory() }