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 052f2d897aa2..86906189d923 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 @@ -24,7 +24,8 @@ private val DUMMY_RELAY_1 = Provider( providerId = ProviderId("PROVIDER RENTED"), ownership = Ownership.Rented, - ) + ), + daita = false ) private val DUMMY_RELAY_2 = RelayItem.Location.Relay( @@ -35,7 +36,8 @@ private val DUMMY_RELAY_2 = ), active = true, provider = - Provider(providerId = ProviderId("PROVIDER OWNED"), ownership = Ownership.MullvadOwned) + Provider(providerId = ProviderId("PROVIDER OWNED"), ownership = Ownership.MullvadOwned), + daita = false ) 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 39f639613227..3e6e7c0660f7 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 @@ -59,6 +59,7 @@ fun FilterRow( is FilterChip.Ownership -> OwnershipFilterChip(it.ownership, onRemoveOwnershipFilter) is FilterChip.Provider -> ProviderFilterChip(it.count, onRemoveProviderFilter) + is FilterChip.Daita -> DaitaFilterChip() } } } @@ -68,7 +69,8 @@ fun FilterRow( fun ProviderFilterChip(providers: Int, onRemoveClick: () -> Unit) { MullvadFilterChip( text = stringResource(id = R.string.number_of_providers, providers), - onRemoveClick = onRemoveClick + onRemoveClick = onRemoveClick, + showIcon = true ) } @@ -76,7 +78,17 @@ fun ProviderFilterChip(providers: Int, onRemoveClick: () -> Unit) { fun OwnershipFilterChip(ownership: Ownership, onRemoveClick: () -> Unit) { MullvadFilterChip( text = stringResource(ownership.stringResources()), - onRemoveClick = onRemoveClick + onRemoveClick = onRemoveClick, + showIcon = true + ) +} + +@Composable +fun DaitaFilterChip() { + MullvadFilterChip( + text = stringResource(id = R.string.setting_chip, stringResource(id = R.string.daita)), + onRemoveClick = {}, + showIcon = false ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FilterChip.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FilterChip.kt index 9f835944a72e..9697060897e5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FilterChip.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FilterChip.kt @@ -23,7 +23,8 @@ private fun PreviewMullvadFilterChip() { AppTheme { MullvadFilterChip( text = stringResource(id = R.string.number_of_providers), - onRemoveClick = {} + onRemoveClick = {}, + showIcon = true ) } } @@ -35,7 +36,8 @@ fun MullvadFilterChip( labelColor: Color = MaterialTheme.colorScheme.onPrimary, iconColor: Color = MaterialTheme.colorScheme.onPrimary, text: String, - onRemoveClick: () -> Unit + onRemoveClick: () -> Unit, + showIcon: Boolean ) { InputChip( shape = MaterialTheme.shapes.chipShape, @@ -55,11 +57,13 @@ fun MullvadFilterChip( onClick = onRemoveClick, label = { Text(text = text, style = MaterialTheme.typography.labelMedium) }, trailingIcon = { - Icon( - painter = painterResource(id = R.drawable.icon_close), - contentDescription = null, - modifier = Modifier.size(Dimens.smallIconSize) - ) + if (showIcon) { + Icon( + painter = painterResource(id = R.drawable.icon_close), + contentDescription = null, + modifier = Modifier.size(Dimens.smallIconSize) + ) + } } ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt index 158e23e15118..e0da5f1b620b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt @@ -33,6 +33,7 @@ private fun PreviewLocationInfo() { isVisible = true, isExpanded = true, location = null, + isUsingDaita = false, inAddress = null, outAddress = "" ) @@ -48,6 +49,7 @@ fun LocationInfo( isVisible: Boolean, isExpanded: Boolean, location: GeoIpLocation?, + isUsingDaita: Boolean, inAddress: Triple?, outAddress: String ) { @@ -61,15 +63,12 @@ fun LocationInfo( .then(modifier) ) { Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = location?.hostname ?: "", - color = - if (isExpanded) { - colorExpanded - } else { - colorCollapsed - }, - style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold) + RelayHostname( + hostname = location?.hostname, + isUsingDaita = isUsingDaita, + isExpanded = isExpanded, + colorExpanded = colorExpanded, + colorCollapsed = colorCollapsed, ) Chevron( isExpanded = isExpanded, @@ -119,3 +118,38 @@ fun LocationInfo( ) } } + +@Composable +private fun RelayHostname( + hostname: String?, + isUsingDaita: Boolean, + isExpanded: Boolean, + colorExpanded: Color, + colorCollapsed: Color +) { + val hostnameTitle = + when { + hostname != null && isUsingDaita -> { + stringResource( + id = R.string.connected_using_daita, + hostname, + stringResource( + id = R.string.daita, + ) + ) + } + hostname != null -> hostname + else -> "" + } + + Text( + text = hostnameTitle, + color = + if (isExpanded) { + colorExpanded + } else { + colorCollapsed + }, + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaConfirmationDialog.kt new file mode 100644 index 000000000000..15b21e70d340 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaConfirmationDialog.kt @@ -0,0 +1,103 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.result.EmptyResultBackNavigator +import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.ramcosta.composedestinations.spec.DestinationStyle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.PrimaryButton +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar + +@Preview +@Composable +private fun PreviewDaitaConfirmationDialog() { + AppTheme { DaitaConfirmation(EmptyResultBackNavigator()) } +} + +@Destination(style = DestinationStyle.Dialog::class) +@Composable +fun DaitaConfirmation(navigator: ResultBackNavigator) { + AlertDialog( + onDismissRequest = dropUnlessResumed { navigator.navigateBack(false) }, + icon = { + Icon( + modifier = Modifier.fillMaxWidth().height(Dimens.dialogIconHeight), + painter = painterResource(id = R.drawable.icon_alert), + contentDescription = "", + tint = MaterialTheme.colorScheme.onSurface + ) + }, + text = { + val scrollState = rememberScrollState() + Column( + Modifier.drawVerticalScrollbar( + scrollState, + MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar) + ) + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.daita_relay_subset_warning), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(Dimens.verticalSpace)) + + Text( + text = + stringResource( + id = R.string.daita_warning, + stringResource( + id = R.string.daita, + ), + ), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + confirmButton = { + Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.enable_anyway), + onClick = { navigator.navigateBack(true) }, + ) + + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.back), + onClick = dropUnlessResumed { navigator.navigateBack(false) }, + ) + } + }, + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt new file mode 100644 index 000000000000..a7e2b0b78ebd --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt @@ -0,0 +1,35 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import com.ramcosta.composedestinations.spec.DestinationStyle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.lib.theme.AppTheme + +@Preview +@Composable +private fun PreviewDaitaInfoDialog() { + AppTheme { DaitaInfo(EmptyDestinationsNavigator) } +} + +@Destination(style = DestinationStyle.Dialog::class) +@Composable +fun DaitaInfo(navigator: DestinationsNavigator) { + InfoDialog( + message = + stringResource( + id = R.string.daita_info, + stringResource(id = R.string.daita), + stringResource(id = R.string.daita_full), + ), + additionalInfo = + stringResource(id = R.string.daita_warning, stringResource(id = R.string.daita)), + onDismiss = dropUnlessResumed { navigator.navigateUp() }, + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt index c1b42c9415e3..417ddbef978e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt @@ -51,6 +51,7 @@ private fun generateRelayItemRelay( cityCode: GeoLocationId.City, hostName: String, active: Boolean = true, + daita: Boolean = true, ) = RelayItem.Location.Relay( id = @@ -60,6 +61,7 @@ private fun generateRelayItemRelay( ), active = active, provider = Provider(ProviderId("Provider"), Ownership.MullvadOwned), + daita = daita ) private fun String.generateCountryCode() = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt index 7045cc45dcf6..050ae32d8262 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt @@ -18,14 +18,14 @@ object TunnelStatePreviewData { fun generateConnectingState(featureIndicators: Int, quantumResistant: Boolean) = TunnelState.Connecting( - endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant), + endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant, daita = false), location = generateLocation(), featureIndicators = generateFeatureIndicators(featureIndicators) ) fun generateConnectedState(featureIndicators: Int, quantumResistant: Boolean) = TunnelState.Connected( - endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant), + endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant, daita = true), location = generateLocation(), featureIndicators = generateFeatureIndicators(featureIndicators) ) @@ -39,7 +39,7 @@ object TunnelStatePreviewData { ) } -private fun generateTunnelEndpoint(quantumResistant: Boolean): TunnelEndpoint = +private fun generateTunnelEndpoint(quantumResistant: Boolean, daita: Boolean): TunnelEndpoint = TunnelEndpoint( endpoint = generateEndpoint(TransportProtocol.Udp), quantumResistant = quantumResistant, @@ -47,7 +47,8 @@ private fun generateTunnelEndpoint(quantumResistant: Boolean): TunnelEndpoint = ObfuscationEndpoint( endpoint = generateEndpoint(TransportProtocol.Tcp), ObfuscationType.Udp2Tcp - ) + ), + daita = daita ) private fun generateEndpoint(transportProtocol: TransportProtocol) = 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 466aa0580b0b..5a34cd812295 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 @@ -365,6 +365,7 @@ private fun ConnectionInfo(state: ConnectUiState) { isVisible = state.showLocationInfo, isExpanded = expanded, location = state.location, + isUsingDaita = state.tunnelState.isUsingDaita(), inAddress = state.inAddress, outAddress = state.outAddress, modifier = 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 2a5157319807..11363d297e74 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 @@ -36,6 +36,8 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AutoConnectAndLockdownModeDestination import com.ramcosta.composedestinations.generated.destinations.ContentBlockersInfoDestination import com.ramcosta.composedestinations.generated.destinations.CustomDnsInfoDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaConfirmationDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaInfoDestination import com.ramcosta.composedestinations.generated.destinations.DnsDestination import com.ramcosta.composedestinations.generated.destinations.LocalNetworkSharingInfoDestination import com.ramcosta.composedestinations.generated.destinations.MalwareInfoDestination @@ -147,6 +149,7 @@ fun VpnSettings( dnsDialogResult: ResultRecipient, customWgPortResult: ResultRecipient, mtuDialogResult: ResultRecipient, + daitaConfirmationDialogResult: ResultRecipient, ) { val vm = koinViewModel() val state by vm.uiState.collectAsStateWithLifecycle() @@ -176,6 +179,12 @@ fun VpnSettings( } } + daitaConfirmationDialogResult.OnNavResultValue { doEnableDaita -> + if (doEnableDaita) { + vm.onToggleDaita(true) + } + } + val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current LaunchedEffectCollect(vm.uiSideEffect) { @@ -224,6 +233,9 @@ fun VpnSettings( }, navigateToLocalNetworkSharingInfo = dropUnlessResumed { navigator.navigate(LocalNetworkSharingInfoDestination) }, + navigateToDaitaInfo = dropUnlessResumed { navigator.navigate(DaitaInfoDestination) }, + navigateToDaitaConfirmation = + dropUnlessResumed { navigator.navigate(DaitaConfirmationDestination) }, navigateToServerIpOverrides = dropUnlessResumed { navigator.navigate(ServerIpOverridesDestination) }, onToggleBlockTrackers = vm::onToggleBlockTrackers, @@ -231,6 +243,7 @@ fun VpnSettings( onToggleBlockMalware = vm::onToggleBlockMalware, onToggleAutoConnect = vm::onToggleAutoConnect, onToggleLocalNetworkSharing = vm::onToggleLocalNetworkSharing, + onDisableDaita = { vm.onToggleDaita(false) }, onToggleBlockAdultContent = vm::onToggleBlockAdultContent, onToggleBlockGambling = vm::onToggleBlockGambling, onToggleBlockSocialMedia = vm::onToggleBlockSocialMedia, @@ -273,6 +286,8 @@ fun VpnSettingsScreen( navigateUdp2TcpInfo: () -> Unit = {}, navigateToWireguardPortInfo: (availablePortRanges: List) -> Unit = {}, navigateToLocalNetworkSharingInfo: () -> Unit = {}, + navigateToDaitaInfo: () -> Unit = {}, + navigateToDaitaConfirmation: () -> Unit = {}, navigateToWireguardPortDialog: () -> Unit = {}, navigateToServerIpOverrides: () -> Unit = {}, onToggleBlockTrackers: (Boolean) -> Unit = {}, @@ -280,6 +295,7 @@ fun VpnSettingsScreen( onToggleBlockMalware: (Boolean) -> Unit = {}, onToggleAutoConnect: (Boolean) -> Unit = {}, onToggleLocalNetworkSharing: (Boolean) -> Unit = {}, + onDisableDaita: () -> Unit = {}, onToggleBlockAdultContent: (Boolean) -> Unit = {}, onToggleBlockGambling: (Boolean) -> Unit = {}, onToggleBlockSocialMedia: (Boolean) -> Unit = {}, @@ -497,8 +513,24 @@ fun VpnSettingsScreen( ) } - itemWithDivider { + item { + Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + HeaderSwitchComposeCell( + title = stringResource(id = R.string.daita), + isToggled = state.isDaitaEnabled, + onCellClicked = { newValueIsEnable -> + if (newValueIsEnable) { + navigateToDaitaConfirmation() + } else { + onDisableDaita() + } + }, + onInfoClicked = navigateToDaitaInfo, + ) Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + } + + itemWithDivider { InformationComposeCell( title = stringResource(id = R.string.wireguard_port_title), onInfoClicked = { navigateToWireguardPortInfo(state.availablePortRanges) }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt index 88d557c5327c..08ab93ac3cac 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt @@ -22,6 +22,8 @@ sealed interface FilterChip { data class Ownership(val ownership: ModelOwnership) : FilterChip data class Provider(val count: Int) : FilterChip + + object Daita : FilterChip } enum class RelayListItemContentType { 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 17eb69d380e8..00b86b3a4f60 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 @@ -13,6 +13,7 @@ data class VpnSettingsUiState( val mtu: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, + val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsItems: List, val contentBlockersOptions: DefaultDnsOptions, @@ -31,6 +32,7 @@ data class VpnSettingsUiState( mtu: Mtu? = null, isAutoConnectEnabled: Boolean = false, isLocalNetworkSharingEnabled: Boolean = false, + isDaitaEnabled: Boolean = false, isCustomDnsEnabled: Boolean = false, customDnsItems: List = emptyList(), contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(), @@ -46,6 +48,7 @@ data class VpnSettingsUiState( mtu, isAutoConnectEnabled, isLocalNetworkSharingEnabled, + isDaitaEnabled, isCustomDnsEnabled, customDnsItems, contentBlockersOptions, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 1bfd860b01e6..4264aff1fa5e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -134,10 +134,10 @@ val uiModule = module { single { CustomListActionUseCase(get(), get()) } single { SelectedLocationTitleUseCase(get(), get()) } single { AvailableProvidersUseCase(get()) } - single { FilterCustomListsRelayItemUseCase(get(), get()) } + single { FilterCustomListsRelayItemUseCase(get(), get(), get()) } single { CustomListsRelayItemUseCase(get(), get()) } single { CustomListRelayItemsUseCase(get(), get()) } - single { FilteredRelayListUseCase(get(), get()) } + single { FilteredRelayListUseCase(get(), get(), get()) } single { LastKnownLocationUseCase(get()) } single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } @@ -184,7 +184,9 @@ val uiModule = module { viewModel { DnsDialogViewModel(get(), get(), get()) } viewModel { LoginViewModel(get(), get(), get()) } viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) } - viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { + SelectLocationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) + } viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) } viewModel { SplashViewModel(get(), get(), get(), get()) } viewModel { VoucherDialogViewModel(get()) } 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 a3758b25fef9..96991d84b934 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 @@ -53,27 +53,28 @@ private fun RelayItem.Location.hasProvider(providersConstraint: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.CustomList { val newLocations = locations.mapNotNull { when (it) { - is RelayItem.Location.Country -> - it.filterOnOwnershipAndProvider(ownership, providers) - is RelayItem.Location.City -> it.filterOnOwnershipAndProvider(ownership, providers) - is RelayItem.Location.Relay -> it.filterOnOwnershipAndProvider(ownership, providers) + is RelayItem.Location.Country -> it.filter(ownership, providers, isDaitaEnabled) + is RelayItem.Location.City -> it.filter(ownership, providers, isDaitaEnabled) + is RelayItem.Location.Relay -> it.filter(ownership, providers, isDaitaEnabled) } } return copy(locations = newLocations) } -fun RelayItem.Location.Country.filterOnOwnershipAndProvider( +fun RelayItem.Location.Country.filter( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.Country? { - val cities = cities.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + val cities = cities.mapNotNull { it.filter(ownership, providers, isDaitaEnabled) } return if (cities.isNotEmpty()) { this.copy(cities = cities) } else { @@ -81,11 +82,12 @@ fun RelayItem.Location.Country.filterOnOwnershipAndProvider( } } -private fun RelayItem.Location.City.filterOnOwnershipAndProvider( +private fun RelayItem.Location.City.filter( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.City? { - val relays = relays.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + val relays = relays.mapNotNull { it.filter(ownership, providers, isDaitaEnabled) } return if (relays.isNotEmpty()) { this.copy(relays = relays) } else { @@ -93,11 +95,18 @@ private fun RelayItem.Location.City.filterOnOwnershipAndProvider( } } -private fun RelayItem.Location.Relay.filterOnOwnershipAndProvider( +private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(isDaitaEnabled: Boolean): Boolean { + return if (isDaitaEnabled) daita else true +} + +private fun RelayItem.Location.Relay.filter( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.Relay? { - return if (hasOwnership(ownership) && hasProvider(providers)) { + return if ( + hasMatchingDaitaSetting(isDaitaEnabled) && hasOwnership(ownership) && hasProvider(providers) + ) { this } else { null diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt index 7a9be0303a13..2c3ae5b8d845 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt @@ -70,4 +70,6 @@ class SettingsRepository( suspend fun setLocalNetworkSharing(isEnabled: Boolean) = managementService.setAllowLan(isEnabled) + + suspend fun setDaitaEnabled(enabled: Boolean) = managementService.setDaitaEnabled(enabled) } 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 c4e05ccc92cc..a7a7f3bbe52b 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 @@ -5,28 +5,33 @@ import net.mullvad.mullvadvpn.lib.model.Constraint 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.relaylist.filterOnOwnershipAndProvider +import net.mullvad.mullvadvpn.relaylist.filter import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository class FilteredRelayListUseCase( private val relayListRepository: RelayListRepository, - private val relayListFilterRepository: RelayListFilterRepository + private val relayListFilterRepository: RelayListFilterRepository, + private val settingsRepository: SettingsRepository ) { operator fun invoke() = combine( relayListRepository.relayList, relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, - ) { relayList, selectedOwnership, selectedProviders -> - relayList.filterOnOwnershipAndProvider( + settingsRepository.settingsUpdates + ) { relayList, selectedOwnership, selectedProviders, settings -> + relayList.filter( selectedOwnership, selectedProviders, + isDaitaEnabled = settings?.isDaitaEnabled() ?: false ) } - private fun List.filterOnOwnershipAndProvider( + private fun List.filter( ownership: Constraint, - providers: Constraint - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + providers: Constraint, + isDaitaEnabled: Boolean + ) = mapNotNull { it.filter(ownership, providers, isDaitaEnabled) } } 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 f82d9eed5e20..fcd5e60a9e51 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 @@ -6,12 +6,14 @@ import net.mullvad.mullvadvpn.lib.model.Constraint 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.relaylist.filterOnOwnershipAndProvider +import net.mullvad.mullvadvpn.relaylist.filter import net.mullvad.mullvadvpn.repository.RelayListFilterRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository class FilterCustomListsRelayItemUseCase( private val customListsRelayItemUseCase: CustomListsRelayItemUseCase, - private val relayListFilterRepository: RelayListFilterRepository + private val relayListFilterRepository: RelayListFilterRepository, + private val settingsRepository: SettingsRepository ) { operator fun invoke() = @@ -19,12 +21,18 @@ class FilterCustomListsRelayItemUseCase( customListsRelayItemUseCase(), relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, - ) { customLists, selectedOwnership, selectedProviders -> - customLists.filterOnOwnershipAndProvider(selectedOwnership, selectedProviders) + settingsRepository.settingsUpdates + ) { customLists, selectedOwnership, selectedProviders, settings -> + customLists.filterOnOwnershipAndProvider( + selectedOwnership, + selectedProviders, + isDaitaEnabled = settings?.isDaitaEnabled() ?: false + ) } private fun List.filterOnOwnershipAndProvider( ownership: Constraint, - providers: Constraint - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + providers: Constraint, + isDaitaEnabled: Boolean + ) = mapNotNull { it.filter(ownership, providers, isDaitaEnabled = isDaitaEnabled) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt index dae0071bc71e..e601e68f333e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt @@ -36,6 +36,7 @@ import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase @@ -50,7 +51,8 @@ class SelectLocationViewModel( private val customListsRepository: CustomListsRepository, private val customListActionUseCase: CustomListActionUseCase, private val filteredRelayListUseCase: FilteredRelayListUseCase, - private val relayListRepository: RelayListRepository + private val relayListRepository: RelayListRepository, + private val settingsRepository: SettingsRepository ) : ViewModel() { private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM) @@ -117,7 +119,8 @@ class SelectLocationViewModel( relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, availableProvidersUseCase(), - ) { selectedOwnership, selectedConstraintProviders, allProviders, + settingsRepository.settingsUpdates + ) { selectedOwnership, selectedConstraintProviders, allProviders, settings, -> val ownershipFilter = selectedOwnership.getOrNull() val providerCountFilter = @@ -130,7 +133,6 @@ class SelectLocationViewModel( ) .size } - buildList { if (ownershipFilter != null) { add(FilterChip.Ownership(ownershipFilter)) @@ -138,6 +140,9 @@ class SelectLocationViewModel( if (providerCountFilter != null) { add(FilterChip.Provider(providerCountFilter)) } + if (settings?.isDaitaEnabled() == true) { + add(FilterChip.Daita) + } } } 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 1e9a335951d3..3bc09e317a9b 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 @@ -64,6 +64,7 @@ class VpnSettingsViewModel( mtuValue = settings?.tunnelOptions?.wireguard?.mtu, isAutoConnectEnabled = settings?.autoConnect ?: false, isLocalNetworkSharingEnabled = settings?.allowLan ?: false, + isDaitaEnabled = settings?.isDaitaEnabled() ?: false, isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false, customDnsList = settings?.addresses()?.asStringAddressList() ?: listOf(), contentBlockersOptions = @@ -124,6 +125,14 @@ 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) 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 d8be8d1cf27e..38979fd4b955 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 @@ -13,6 +13,7 @@ data class VpnSettingsViewModelState( val mtuValue: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, + val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsList: List, val contentBlockersOptions: DefaultDnsOptions, @@ -29,6 +30,7 @@ data class VpnSettingsViewModelState( mtuValue, isAutoConnectEnabled, isLocalNetworkSharingEnabled, + isDaitaEnabled, isCustomDnsEnabled, customDnsList, contentBlockersOptions, @@ -47,6 +49,7 @@ data class VpnSettingsViewModelState( mtuValue = null, isAutoConnectEnabled = false, isLocalNetworkSharingEnabled = false, + isDaitaEnabled = false, isCustomDnsEnabled = false, customDnsList = listOf(), contentBlockersOptions = DefaultDnsOptions(), 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 f453223ec709..aaf00e63bb25 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 @@ -324,7 +324,8 @@ class CustomListLocationsViewModelTest { Provider( ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) ) ) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt index 993190ba2c74..a69d123e2ef2 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt @@ -32,10 +32,12 @@ import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayItemId +import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.relaylist.descendants import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase @@ -58,6 +60,9 @@ class SelectLocationViewModelTest { private val mockCustomListsRepository: CustomListsRepository = mockk() private val mockCustomListsRelayItemUseCase: CustomListsRelayItemUseCase = mockk() + private val mockSettingsRepository: SettingsRepository = mockk() + private val settingsFlow = MutableStateFlow(mockk(relaxed = true)) + private lateinit var viewModel: SelectLocationViewModel private val allProviders = MutableStateFlow>(emptyList()) @@ -79,6 +84,7 @@ class SelectLocationViewModelTest { every { mockFilteredRelayListUseCase() } returns filteredRelayList every { mockFilteredCustomListRelayItemsUseCase() } returns filteredCustomRelayListItems every { mockCustomListsRelayItemUseCase() } returns customListsRelayItem + every { mockSettingsRepository.settingsUpdates } returns settingsFlow mockkStatic(RELAY_LIST_EXTENSIONS) mockkStatic(RELAY_ITEM_EXTENSIONS) @@ -93,6 +99,7 @@ class SelectLocationViewModelTest { relayListRepository = mockRelayListRepository, customListsRepository = mockCustomListsRepository, customListsRelayItemUseCase = mockCustomListsRelayItemUseCase, + settingsRepository = mockSettingsRepository, ) } 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 add2ee8580d5..85a41272eeb5 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 @@ -113,7 +113,11 @@ class VpnSettingsViewModelTest { val mockTunnelOptions: TunnelOptions = mockk(relaxed = true) // Can not use a mock here since mocking a value class val leads to class cast exception val mockWireguardTunnelOptions = - WireguardTunnelOptions(mtu = Mtu(0), quantumResistant = expectedResistantState) + WireguardTunnelOptions( + mtu = Mtu(0), + quantumResistant = expectedResistantState, + daita = false + ) every { mockSettings.tunnelOptions } returns mockTunnelOptions every { mockTunnelOptions.wireguard } returns mockWireguardTunnelOptions @@ -145,7 +149,8 @@ class VpnSettingsViewModelTest { wireguard = WireguardTunnelOptions( mtu = null, - quantumResistant = QuantumResistantState.Off + quantumResistant = QuantumResistantState.Off, + daita = false ), dnsOptions = mockk(relaxed = true) ) diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt index c10f3b58e661..6da80f9b0170 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt @@ -104,6 +104,7 @@ import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation import net.mullvad.mullvadvpn.lib.model.SetAllowLanError import net.mullvad.mullvadvpn.lib.model.SetApiAccessMethodError import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError +import net.mullvad.mullvadvpn.lib.model.SetDaitaSettingsError import net.mullvad.mullvadvpn.lib.model.SetDnsOptionsError import net.mullvad.mullvadvpn.lib.model.SetObfuscationOptionsError import net.mullvad.mullvadvpn.lib.model.SetRelayLocationError @@ -501,6 +502,15 @@ class ManagementService( .mapLeft(SetAllowLanError::Unknown) .mapEmpty() + suspend fun setDaitaEnabled(enabled: Boolean): Either = + Either.catch { + val daitaSettings = + ManagementInterface.DaitaSettings.newBuilder().setEnabled(enabled).build() + grpc.setDaitaSettings(daitaSettings) + } + .mapLeft(SetDaitaSettingsError::Unknown) + .mapEmpty() + suspend fun setRelayLocation(location: ModelRelayItemId): Either = Either.catch { val currentRelaySettings = getSettings().relaySettings 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 ca4e924b6cdf..fb43f8e50742 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 @@ -68,6 +68,7 @@ import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings import net.mullvad.mullvadvpn.lib.model.WireguardConstraints import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData +import net.mullvad.mullvadvpn.lib.model.WireguardRelayEndpointData import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions import org.joda.time.Instant @@ -159,7 +160,8 @@ internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint = obfuscation.toDomain() } else { null - } + }, + daita = daita ) internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndpoint = @@ -372,6 +374,7 @@ internal fun ManagementInterface.TunnelOptions.WireguardOptions.toDomain(): Wire WireguardTunnelOptions( mtu = if (hasMtu()) Mtu(mtu) else null, quantumResistant = quantumResistant.toDomain(), + daita = daita.enabled ) internal fun ManagementInterface.QuantumResistantState.toDomain(): QuantumResistantState = @@ -442,7 +445,12 @@ internal fun ManagementInterface.RelayList.toDomain(): RelayList = RelayList(countriesList.toDomain(), wireguard.toDomain()) internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndpointData = - WireguardEndpointData(portRangesList.map { it.toDomain() }) + WireguardEndpointData( + portRangesList.map { it.toDomain() }, + ) + +internal fun ManagementInterface.WireguardRelayEndpointData.toDomain(): WireguardRelayEndpointData = + WireguardRelayEndpointData(daita) internal fun ManagementInterface.PortRange.toDomain(): PortRange = PortRange(first..last) @@ -494,7 +502,13 @@ internal fun ManagementInterface.Relay.toDomain( Provider( ProviderId(provider), ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented - ) + ), + daita = + if ( + hasEndpointData() && endpointType == ManagementInterface.Relay.RelayType.WIREGUARD + ) { + ManagementInterface.WireguardRelayEndpointData.parseFrom(endpointData.value).daita + } else false ) internal fun ManagementInterface.Device.toDomain(): Device = @@ -598,11 +612,11 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() = ManagementInterface.FeatureIndicator.SERVER_IP_OVERRIDE -> FeatureIndicator.SERVER_IP_OVERRIDE ManagementInterface.FeatureIndicator.CUSTOM_MTU -> FeatureIndicator.CUSTOM_MTU + ManagementInterface.FeatureIndicator.DAITA -> FeatureIndicator.DAITA ManagementInterface.FeatureIndicator.LOCKDOWN_MODE, ManagementInterface.FeatureIndicator.SHADOWSOCKS, ManagementInterface.FeatureIndicator.MULTIHOP, ManagementInterface.FeatureIndicator.BRIDGE_MODE, ManagementInterface.FeatureIndicator.CUSTOM_MSS_FIX, - ManagementInterface.FeatureIndicator.DAITA, ManagementInterface.FeatureIndicator.UNRECOGNIZED -> error("Feature not supported") } 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 42cf7455107e..39c7cce3cc04 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 @@ -29,6 +29,7 @@ class RelayNameComparatorTest { providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned ), + daita = false ) val relay10 = RelayItem.Location.Relay( @@ -38,7 +39,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) relay9 assertOrderBothDirection relay10 @@ -54,7 +56,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay9b = RelayItem.Location.Relay( @@ -64,7 +67,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -81,7 +85,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay1 = RelayItem.Location.Relay( @@ -91,7 +96,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay3 = RelayItem.Location.Relay( @@ -101,7 +107,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay100 = RelayItem.Location.Relay( @@ -111,7 +118,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) relay001 assertOrderBothDirection relay1 @@ -130,7 +138,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay9b = RelayItem.Location.Relay( @@ -140,7 +149,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -157,7 +167,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay005 = RelayItem.Location.Relay( @@ -167,7 +178,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) relay001 assertOrderBothDirection relay005 @@ -183,7 +195,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relayAr8 = RelayItem.Location.Relay( @@ -193,7 +206,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relaySe5 = RelayItem.Location.Relay( @@ -203,7 +217,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relaySe10 = RelayItem.Location.Relay( @@ -213,7 +228,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) relayAr2 assertOrderBothDirection relayAr8 @@ -231,7 +247,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay2w = RelayItem.Location.Relay( @@ -241,7 +258,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) relay2c assertOrderBothDirection relay2w @@ -257,7 +275,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) val relay22b = RelayItem.Location.Relay( @@ -267,7 +286,8 @@ class RelayNameComparatorTest { Provider( providerId = ProviderId("Provider"), ownership = Ownership.MullvadOwned - ) + ), + daita = false ) 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 7ad0b3ab6998..d11f40586950 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,12 +9,12 @@ enum class FeatureIndicator { CUSTOM_DNS, SERVER_IP_OVERRIDE, CUSTOM_MTU, + DAITA, // Currently not supported // LOCKDOWN_MODE, // SHADOWSOCKS, // MULTIHOP, // BRIDGE_MODE, // CUSTOM_MSS_FIX, - // DAITA, // UNRECOGNIZED, } 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 17bc563a8ddd..8bfa6c1f64f4 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 @@ -60,6 +60,7 @@ sealed interface RelayItem { override val id: GeoLocationId.Hostname, val provider: Provider, override val active: Boolean, + val daita: Boolean ) : Location { override val name: String = id.code override val hasChildren: Boolean = false diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt new file mode 100644 index 000000000000..f636267c0914 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetDaitaSettingsError { + data class Unknown(val throwable: Throwable) : SetDaitaSettingsError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt index e801397b2773..96dbd77ab5a0 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt @@ -15,5 +15,7 @@ data class Settings( val splitTunnelSettings: SplitTunnelSettings, val apiAccessMethodSettings: List ) { + fun isDaitaEnabled() = tunnelOptions.wireguard.daita + companion object } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt index d715f1676610..8b61ab1fd77d 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt @@ -3,5 +3,6 @@ package net.mullvad.mullvadvpn.lib.model data class TunnelEndpoint( val endpoint: Endpoint, val quantumResistant: Boolean, - val obfuscation: ObfuscationEndpoint? + val obfuscation: ObfuscationEndpoint?, + val daita: Boolean ) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt index 8ed43bd294b2..1751e9f80360 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt @@ -38,4 +38,12 @@ sealed class TunnelState { is Error -> this.errorState.isBlocking } } + + fun isUsingDaita(): Boolean { + return when (this) { + is Connected -> endpoint.daita + is Connecting -> endpoint?.daita ?: false + else -> false + } + } } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt new file mode 100644 index 000000000000..9e328b92e636 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class WireguardRelayEndpointData(val daita: Boolean) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt index 573f08213ef0..0bdb19e70181 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt @@ -1,3 +1,7 @@ package net.mullvad.mullvadvpn.lib.model -data class WireguardTunnelOptions(val mtu: Mtu?, val quantumResistant: QuantumResistantState) +data class WireguardTunnelOptions( + val mtu: Mtu?, + val quantumResistant: QuantumResistantState, + val daita: Boolean +) diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index c43acdb496d9..b25761803581 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -385,4 +385,10 @@ Failed to set to current - Unknown reason %s was removed from \"%s\" \"%s\" was created + %s (%s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size. + Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %s. + %s using %s + Setting: %s + Enable anyway + This feature isn’t available on all servers. You might need to change location after enabling. 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 110e112e99f3..9cf571171a8b 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 @@ -14,4 +14,6 @@
  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16
  • fe80::/10
  • fc00::/7
  • ]]>
    + DAITA + Defence against AI-guided Traffic Analysis diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt deleted file mode 100644 index 71a05e674385..000000000000 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.mullvad.mullvadvpn.service - -import android.content.Context -import java.io.File -import java.io.FileOutputStream - -class FileResourceExtractor(val context: Context) { - fun extract(asset: String, force: Boolean = false) { - val destination = File(context.filesDir, asset) - - if (!destination.exists() || force) { - extractFile(asset, destination) - } - } - - private fun extractFile(asset: String, destination: File) { - val destinationStream = FileOutputStream(destination) - - context.assets.open(asset).copyTo(destinationStream) - - destinationStream.close() - } -} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 992326510711..cc8b7b2365e0 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -31,13 +31,14 @@ import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling import net.mullvad.mullvadvpn.service.notifications.ForegroundNotificationManager import net.mullvad.mullvadvpn.service.notifications.NotificationChannelFactory import net.mullvad.mullvadvpn.service.notifications.NotificationManager +import net.mullvad.mullvadvpn.service.util.extractAndOverwriteIfAssetMoreRecent import net.mullvad.talpid.TalpidVpnService import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules import org.koin.core.qualifier.named -private const val RELAYS_FILE = "relays.json" -private const val MAYBENOT_MACHINES_FILE = "maybenot_machines" +private const val RELAY_LIST_ASSET_NAME = "relays.json" +private const val MAYBENOT_MACHINES_ASSET_NAME = "maybenot_machines" class MullvadVpnService : TalpidVpnService() { @@ -230,27 +231,10 @@ class MullvadVpnService : TalpidVpnService() { } private fun Context.prepareFiles() { - prepareRelayList() - prepareMaybenotMachines() + extractAndOverwriteIfAssetMoreRecent(RELAY_LIST_ASSET_NAME) + extractAndOverwriteIfAssetMoreRecent(MAYBENOT_MACHINES_ASSET_NAME) } - private fun Context.prepareRelayList() { - val shouldOverwriteRelayList = - lastUpdatedTime() > File(filesDir, RELAYS_FILE).lastModified() - FileResourceExtractor(this).apply { extract(RELAYS_FILE, shouldOverwriteRelayList) } - } - - private fun Context.prepareMaybenotMachines() { - val shouldOverwriteMaybenotMachines = - lastUpdatedTime() > File(filesDir, RELAYS_FILE).lastModified() - FileResourceExtractor(this).apply { - extract(MAYBENOT_MACHINES_FILE, shouldOverwriteMaybenotMachines) - } - } - - private fun Context.lastUpdatedTime(): Long = - packageManager.getPackageInfo(packageName, 0).lastUpdateTime - companion object { init { System.loadLibrary("mullvad_jni") diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ContextExtensions.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ContextExtensions.kt new file mode 100644 index 000000000000..51240fa16d62 --- /dev/null +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ContextExtensions.kt @@ -0,0 +1,23 @@ +package net.mullvad.mullvadvpn.service.util + +import android.content.Context +import java.io.File +import java.io.FileOutputStream + +fun Context.extractAndOverwriteIfAssetMoreRecent(assetName: String) { + val forceOverwriteIfMoreRecent = lastUpdatedTime() > File(filesDir, assetName).lastModified() + val destination = File(filesDir, assetName) + + if (!destination.exists() || forceOverwriteIfMoreRecent) { + extractFile(assetName, destination) + } +} + +private fun Context.lastUpdatedTime(): Long = + packageManager.getPackageInfo(packageName, 0).lastUpdateTime + +private fun Context.extractFile(asset: String, destination: File) { + val destinationStream = FileOutputStream(destination) + assets.open(asset).copyTo(destinationStream) + destinationStream.close() +} diff --git a/wireguard-go-rs/libwg/build-android.sh b/wireguard-go-rs/libwg/build-android.sh index cd304e2e3ab7..036fa51cf8a7 100755 --- a/wireguard-go-rs/libwg/build-android.sh +++ b/wireguard-go-rs/libwg/build-android.sh @@ -43,7 +43,8 @@ for arch in ${ARCHITECTURES:-armv7 aarch64 x86_64 i686}; do # Build Wireguard-Go pwd make -f Android.mk clean - export BUILD_DIR=$(realpath -s "../../build/") + export BUILD_DIR + BUILD_DIR=$(realpath -s "../../build/") LDFLAGS="-L$BUILD_DIR/lib/$RUST_TARGET_TRIPLE" make -f Android.mk # Strip and copy the library to `android/build/extraJni/$ANDROID_ABI` to be able to build the APK