diff options
| author | Albin <albin@mullvad.net> | 2024-08-26 15:48:37 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2024-09-06 14:59:55 +0200 |
| commit | 3847c1eb82a324a4b5fff2a5e076750ebf4449c4 (patch) | |
| tree | 55c45a4a75e56503e675fdad5418cbeafb850854 /android/app/src | |
| parent | d464325f98bc488f091ef18b4ba04e0d7dbe2605 (diff) | |
| download | mullvadvpn-3847c1eb82a324a4b5fff2a5e076750ebf4449c4.tar.xz mullvadvpn-3847c1eb82a324a4b5fff2a5e076750ebf4449c4.zip | |
Add daita grpc and ui
Diffstat (limited to 'android/app/src')
23 files changed, 350 insertions, 47 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 3cea5e5c68..a073bc60ff 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,6 +22,7 @@ private val DUMMY_RELAY_1 = active = true, provider = Provider(providerId = ProviderId("PROVIDER RENTED"), ownership = Ownership.Rented), + daita = false, ) private val DUMMY_RELAY_2 = RelayItem.Location.Relay( @@ -33,6 +34,7 @@ private val DUMMY_RELAY_2 = active = true, provider = 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 a94846dacf..d3e233c67b 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 @@ -57,6 +57,7 @@ fun FilterRow( is FilterChip.Ownership -> OwnershipFilterChip(it.ownership, onRemoveOwnershipFilter) is FilterChip.Provider -> ProviderFilterChip(it.count, onRemoveProviderFilter) + is FilterChip.Daita -> DaitaFilterChip() } } } @@ -67,6 +68,7 @@ fun ProviderFilterChip(providers: Int, onRemoveClick: () -> Unit) { MullvadFilterChip( text = stringResource(id = R.string.number_of_providers, providers), onRemoveClick = onRemoveClick, + enabled = true, ) } @@ -75,6 +77,16 @@ fun OwnershipFilterChip(ownership: Ownership, onRemoveClick: () -> Unit) { MullvadFilterChip( text = stringResource(ownership.stringResources()), onRemoveClick = onRemoveClick, + enabled = true, + ) +} + +@Composable +fun DaitaFilterChip() { + MullvadFilterChip( + text = stringResource(id = R.string.setting_chip, stringResource(id = R.string.daita)), + onRemoveClick = {}, + enabled = 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 4761c15c9d..94e1c7853a 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 @@ -19,11 +19,24 @@ import net.mullvad.mullvadvpn.lib.theme.shape.chipShape @Preview @Composable -private fun PreviewMullvadFilterChip() { +private fun PreviewEnabledMullvadFilterChip() { AppTheme { MullvadFilterChip( text = stringResource(id = R.string.number_of_providers), onRemoveClick = {}, + enabled = true, + ) + } +} + +@Preview +@Composable +private fun PreviewDisabledMullvadFilterChip() { + AppTheme { + MullvadFilterChip( + text = stringResource(id = R.string.number_of_providers), + onRemoveClick = {}, + enabled = false, ) } } @@ -36,30 +49,38 @@ fun MullvadFilterChip( iconColor: Color = MaterialTheme.colorScheme.onPrimary, text: String, onRemoveClick: () -> Unit, + enabled: Boolean, ) { InputChip( + enabled = enabled, shape = MaterialTheme.shapes.chipShape, colors = FilterChipDefaults.filterChipColors( containerColor = containerColor, + disabledContainerColor = containerColor, labelColor = labelColor, + disabledLabelColor = labelColor, iconColor = iconColor, ), border = FilterChipDefaults.filterChipBorder( borderColor = borderColor, + disabledBorderColor = borderColor, enabled = true, selected = false, ), selected = false, 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), - ) - }, + trailingIcon = + if (enabled) { + { + Icon( + painter = painterResource(id = R.drawable.icon_close), + contentDescription = null, + modifier = Modifier.size(Dimens.smallIconSize), + ) + } + } else null, ) } 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 f9c0f26862..3fbd40c537 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<String, Int, TransportProtocol>?, 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,36 @@ 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 0000000000..a5237a0ca5 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaConfirmationDialog.kt @@ -0,0 +1,101 @@ +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<RootGraph>(style = DestinationStyle.Dialog::class) +@Composable +fun DaitaConfirmation(navigator: ResultBackNavigator<Boolean>) { + AlertDialog( + onDismissRequest = dropUnlessResumed { navigator.navigateBack(false) }, + icon = { + Icon( + modifier = Modifier.fillMaxWidth().height(Dimens.dialogIconHeight), + painter = painterResource(id = R.drawable.icon_alert), + contentDescription = null, + 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 0000000000..a7e2b0b78e --- /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<RootGraph>(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 70d707c10c..0af4199f40 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,11 +51,13 @@ private fun generateRelayItemRelay( cityCode: GeoLocationId.City, hostName: String, active: Boolean = true, + daita: Boolean = true, ) = RelayItem.Location.Relay( id = GeoLocationId.Hostname(city = cityCode, code = hostName), 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 a8981e612e..b6ca18c91a 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, @@ -48,6 +48,7 @@ private fun generateTunnelEndpoint(quantumResistant: Boolean): TunnelEndpoint = 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 c5b2be56ed..c9ee962eb1 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 @@ -356,6 +356,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 1bccbaa713..c130d6d61c 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<DnsDestination, DnsDialogResult>, customWgPortResult: ResultRecipient<WireguardCustomPortDestination, Port?>, mtuDialogResult: ResultRecipient<MtuDestination, Boolean>, + daitaConfirmationDialogResult: ResultRecipient<DaitaConfirmationDestination, Boolean>, ) { val vm = koinViewModel<VpnSettingsViewModel>() 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 CollectSideEffectWithLifecycle(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<PortRange>) -> 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 = { enable -> + if (enable) { + 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 4fa2fbb1eb..d8245792a3 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 + + data 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 0eaf1695a7..b57de21bc5 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<CustomDnsItem>, 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<CustomDnsItem> = 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 1f1a489684..6b909d394d 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 c87a9548c5..5d6e48a3f7 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<Provi true } -fun RelayItem.CustomList.filterOnOwnershipAndProvider( +fun RelayItem.CustomList.filter( ownership: Constraint<Ownership>, providers: Constraint<Providers>, + 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<Ownership>, providers: Constraint<Providers>, + 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<Ownership>, providers: Constraint<Providers>, + 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<Ownership>, providers: Constraint<Providers>, + 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 19e52b9790..d66d4a5c00 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 @@ -68,4 +68,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 9c1994e075..60de94946f 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,25 +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 settingsRepository: SettingsRepository, ) { operator fun invoke() = combine( relayListRepository.relayList, relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, - ) { relayList, selectedOwnership, selectedProviders -> - relayList.filterOnOwnershipAndProvider(selectedOwnership, selectedProviders) + settingsRepository.settingsUpdates, + ) { relayList, selectedOwnership, selectedProviders, settings -> + relayList.filter( + selectedOwnership, + selectedProviders, + isDaitaEnabled = settings?.isDaitaEnabled() ?: false, + ) } - private fun List<RelayItem.Location.Country>.filterOnOwnershipAndProvider( + private fun List<RelayItem.Location.Country>.filter( ownership: Constraint<Ownership>, providers: Constraint<Providers>, - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + 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 49c8ec89f6..17ead75d2a 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 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<RelayItem.CustomList>.filterOnOwnershipAndProvider( ownership: Constraint<Ownership>, providers: Constraint<Providers>, - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + 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 04476ea560..c34b182aa6 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 @@ -51,6 +52,7 @@ class SelectLocationViewModel( private val customListActionUseCase: CustomListActionUseCase, private val filteredRelayListUseCase: FilteredRelayListUseCase, private val relayListRepository: RelayListRepository, + private val settingsRepository: SettingsRepository, ) : ViewModel() { private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM) @@ -110,7 +112,8 @@ class SelectLocationViewModel( relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, availableProvidersUseCase(), - ) { selectedOwnership, selectedConstraintProviders, allProviders -> + settingsRepository.settingsUpdates, + ) { selectedOwnership, selectedConstraintProviders, allProviders, settings -> val ownershipFilter = selectedOwnership.getOrNull() val providerCountFilter = when (selectedConstraintProviders) { @@ -122,7 +125,6 @@ class SelectLocationViewModel( ) .size } - buildList { if (ownershipFilter != null) { add(FilterChip.Ownership(ownershipFilter)) @@ -130,6 +132,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 abaad265fe..af2ea72e4e 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 @@ -63,6 +63,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 = @@ -123,6 +124,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 534263b44b..676a10cd70 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<CustomDnsItem>, 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 e4012abd9e..1e0de9c96e 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 @@ -322,6 +322,7 @@ class CustomListLocationsViewModelTest { 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 5a44f6db06..bee888d279 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<Settings>(relaxed = true)) + private lateinit var viewModel: SelectLocationViewModel private val allProviders = MutableStateFlow<List<Provider>>(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 c2f9ca34a6..89456c1d02 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 @@ -146,6 +150,7 @@ class VpnSettingsViewModelTest { WireguardTunnelOptions( mtu = null, quantumResistant = QuantumResistantState.Off, + daita = false, ), dnsOptions = mockk(relaxed = true), ) |
