diff options
38 files changed, 1276 insertions, 167 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/ContentType.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/ContentType.kt index 04b29268a5..47b6ba7923 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/ContentType.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/ContentType.kt @@ -9,4 +9,5 @@ object ContentType { const val SPACER = 5 const val PROGRESS = 6 const val EMPTY_TEXT = 7 + const val BUTTON = 8 } 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/DaitaDirectOnlyConfirmationDialog.kt index b79814746c..a1b6e7bba0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaConfirmationDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaDirectOnlyConfirmationDialog.kt @@ -1,8 +1,6 @@ package net.mullvad.mullvadvpn.compose.dialog -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,34 +16,24 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialog import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialogTitleType import net.mullvad.mullvadvpn.lib.theme.AppTheme -import net.mullvad.mullvadvpn.lib.theme.Dimens @Preview @Composable -private fun PreviewDaitaConfirmationDialog() { - AppTheme { DaitaConfirmation(EmptyResultBackNavigator()) } +private fun PreviewDaitaDirectOnlyConfirmationDialog() { + AppTheme { DaitaDirectOnlyConfirmation(EmptyResultBackNavigator()) } } @Destination<RootGraph>(style = DestinationStyle.Dialog::class) @Composable -fun DaitaConfirmation(navigator: ResultBackNavigator<Boolean>) { +fun DaitaDirectOnlyConfirmation(navigator: ResultBackNavigator<Boolean>) { InfoConfirmationDialog( navigator = navigator, titleType = InfoConfirmationDialogTitleType.IconOnly, - confirmButtonTitle = stringResource(R.string.enable_anyway), - cancelButtonTitle = stringResource(R.string.back), + confirmButtonTitle = stringResource(R.string.enable_direct_only), + cancelButtonTitle = stringResource(R.string.cancel), ) { 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)), + text = stringResource(id = R.string.direct_only_description), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/info/DaitaInfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/info/DaitaDirectOnlyInfoDialog.kt index 4cfbbb087e..64e5cd46de 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/info/DaitaInfoDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/info/DaitaDirectOnlyInfoDialog.kt @@ -14,22 +14,15 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme @Preview @Composable -private fun PreviewDaitaInfoDialog() { - AppTheme { DaitaInfo(EmptyDestinationsNavigator) } +private fun PreviewDaitaDirectOnlyInfoDialog() { + AppTheme { DaitaDirectOnlyInfo(EmptyDestinationsNavigator) } } @Destination<RootGraph>(style = DestinationStyle.Dialog::class) @Composable -fun DaitaInfo(navigator: DestinationsNavigator) { +fun DaitaDirectOnlyInfo(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)), + message = stringResource(id = R.string.daita_info), onDismiss = dropUnlessResumed { navigator.navigateUp() }, ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt index b0415b1c7e..6893397f9f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt @@ -12,12 +12,13 @@ class SelectLocationsUiStatePreviewParameterProvider : PreviewParameterProvider<SelectLocationUiState> { override val values = sequenceOf( - SelectLocationUiState( + SelectLocationUiState.Loading, + SelectLocationUiState.Data( filterChips = emptyList(), multihopEnabled = false, relayListType = RelayListType.EXIT, ), - SelectLocationUiState( + SelectLocationUiState.Data( filterChips = listOf( FilterChip.Ownership(ownership = ModelOwnership.Rented), @@ -26,12 +27,12 @@ class SelectLocationsUiStatePreviewParameterProvider : multihopEnabled = false, relayListType = RelayListType.EXIT, ), - SelectLocationUiState( + SelectLocationUiState.Data( filterChips = emptyList(), multihopEnabled = true, relayListType = RelayListType.ENTRY, ), - SelectLocationUiState( + SelectLocationUiState.Data( filterChips = listOf( FilterChip.Ownership(ownership = ModelOwnership.MullvadOwned), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt index 18f422a988..5a7a6b276a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt @@ -10,6 +10,7 @@ class SettingsUiStatePreviewParameterProvider : PreviewParameterProvider<Setting appVersion = "2222.22", isLoggedIn = true, isSupportedVersion = true, + isDaitaEnabled = true, isPlayBuild = true, multihopEnabled = false, ), @@ -17,6 +18,7 @@ class SettingsUiStatePreviewParameterProvider : PreviewParameterProvider<Setting appVersion = "9000.1", isLoggedIn = false, isSupportedVersion = false, + isDaitaEnabled = false, isPlayBuild = false, multihopEnabled = false, ), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt index c041f5516c..796965d856 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt @@ -20,7 +20,6 @@ class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider<VpnS VpnSettingsUiState.createDefault( mtu = Mtu(MTU), isLocalNetworkSharingEnabled = true, - isDaitaEnabled = true, isCustomDnsEnabled = true, customDnsItems = listOf(CustomDnsItem("0.0.0.0", false)), contentBlockersOptions = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt new file mode 100644 index 0000000000..c7c3b61752 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt @@ -0,0 +1,204 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.DaitaDirectOnlyConfirmationDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaDirectOnlyInfoDestination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.result.ResultRecipient +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell +import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell +import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.compose.state.DaitaUiState +import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition +import net.mullvad.mullvadvpn.compose.util.OnNavResultValue +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.viewmodel.DaitaViewModel +import org.koin.androidx.compose.koinViewModel + +@Preview +@Composable +private fun PreviewDaitaScreen() { + AppTheme { DaitaScreen(state = DaitaUiState(daitaEnabled = false, directOnly = false)) } +} + +@Destination<RootGraph>(style = SlideInFromRightTransition::class) +@Composable +fun Daita( + navigator: DestinationsNavigator, + daitaConfirmationDialogResult: ResultRecipient<DaitaDirectOnlyConfirmationDestination, Boolean>, +) { + val viewModel = koinViewModel<DaitaViewModel>() + val state by viewModel.uiState.collectAsStateWithLifecycle() + + daitaConfirmationDialogResult.OnNavResultValue { + if (it) { + viewModel.setDirectOnly(true) + } + } + + DaitaScreen( + state = state, + onDaitaEnabled = viewModel::setDaita, + onDirectOnlyClick = { enable -> + if (enable) { + navigator.navigate(DaitaDirectOnlyConfirmationDestination) + } else { + viewModel.setDirectOnly(false) + } + }, + onDirectOnlyInfoClick = + dropUnlessResumed { navigator.navigate(DaitaDirectOnlyInfoDestination) }, + onBackClick = dropUnlessResumed { navigator.navigateUp() }, + ) +} + +@Composable +fun DaitaScreen( + state: DaitaUiState, + onDaitaEnabled: (enable: Boolean) -> Unit = {}, + onDirectOnlyClick: (enable: Boolean) -> Unit = {}, + onDirectOnlyInfoClick: () -> Unit = {}, + onBackClick: () -> Unit = {}, +) { + ScaffoldWithMediumTopBar( + appBarTitle = stringResource(id = R.string.daita), + navigationIcon = { NavigateBackIconButton { onBackClick() } }, + ) { modifier -> + Column(modifier = modifier) { + val pagerState = rememberPagerState(pageCount = { DaitaPages.entries.size }) + DescriptionPager(pagerState = pagerState) + PageIndicator(pagerState = pagerState) + HeaderSwitchComposeCell( + title = stringResource(R.string.enable), + isToggled = state.daitaEnabled, + onCellClicked = onDaitaEnabled, + ) + HorizontalDivider() + HeaderSwitchComposeCell( + title = stringResource(R.string.direct_only), + isToggled = state.directOnly, + isEnabled = state.daitaEnabled, + onCellClicked = onDirectOnlyClick, + onInfoClicked = onDirectOnlyInfoClick, + ) + } + } +} + +@Composable +private fun DescriptionPager(pagerState: PagerState) { + HorizontalPager( + state = pagerState, + verticalAlignment = Alignment.Top, + beyondViewportPageCount = DaitaPages.entries.size, + ) { page -> + Column(modifier = Modifier.fillMaxWidth()) { + val page = DaitaPages.entries[page] + // Scale image to fit width up to certain width + Image( + contentScale = ContentScale.FillWidth, + modifier = + Modifier.widthIn(max = Dimens.settingsDetailsImageMaxWidth) + .fillMaxWidth() + .padding(horizontal = Dimens.mediumPadding) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = page.image), + contentDescription = stringResource(R.string.daita), + ) + DescriptionText( + firstParagraph = page.textFirstParagraph, + secondParagraph = page.textSecondParagraph, + thirdParagraph = page.textThirdParagraph, + ) + } + } +} + +@Composable +private fun DescriptionText(firstParagraph: Int, secondParagraph: Int, thirdParagraph: Int) { + SwitchComposeSubtitleCell( + modifier = Modifier.padding(vertical = Dimens.smallPadding), + text = + buildString { + appendLine(stringResource(firstParagraph)) + appendLine() + appendLine(stringResource(secondParagraph)) + appendLine() + append(stringResource(thirdParagraph)) + }, + ) +} + +@Composable +private fun PageIndicator(pagerState: PagerState) { + Row( + Modifier.wrapContentHeight().fillMaxWidth().padding(bottom = Dimens.mediumPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom, + ) { + repeat(pagerState.pageCount) { iteration -> + val color = + if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.primary + Box( + modifier = + Modifier.padding(Dimens.indicatorPadding) + .clip(CircleShape) + .background(color) + .size(Dimens.indicatorSize) + ) + } + } +} + +private enum class DaitaPages( + val image: Int, + val textFirstParagraph: Int, + val textSecondParagraph: Int, + val textThirdParagraph: Int, +) { + FIRST( + image = R.drawable.daita_illustration_1, + textFirstParagraph = R.string.daita_description_slide_1_first_paragraph, + textSecondParagraph = R.string.daita_description_slide_1_second_paragraph, + textThirdParagraph = R.string.daita_description_slide_1_third_paragraph, + ), + SECOND( + image = R.drawable.daita_illustration_2, + textFirstParagraph = R.string.daita_description_slide_2_first_paragraph, + textSecondParagraph = R.string.daita_description_slide_2_second_paragraph, + textThirdParagraph = R.string.daita_description_slide_2_third_paragraph, + ), +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt index b8c418cd06..75ba5abdd8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt @@ -26,6 +26,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.ApiAccessListDestination import com.ramcosta.composedestinations.generated.destinations.AppInfoDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaDestination import com.ramcosta.composedestinations.generated.destinations.MultihopDestination import com.ramcosta.composedestinations.generated.destinations.ReportProblemDestination import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination @@ -74,6 +75,7 @@ fun Settings(navigator: DestinationsNavigator) { onReportProblemCellClick = dropUnlessResumed { navigator.navigate(ReportProblemDestination) }, onMultihopClick = dropUnlessResumed { navigator.navigate(MultihopDestination) }, + onDaitaClick = dropUnlessResumed { navigator.navigate(DaitaDestination) }, onBackClick = dropUnlessResumed { navigator.navigateUp() }, ) } @@ -88,6 +90,7 @@ fun SettingsScreen( onReportProblemCellClick: () -> Unit = {}, onApiAccessClick: () -> Unit = {}, onMultihopClick: () -> Unit = {}, + onDaitaClick: () -> Unit = {}, onBackClick: () -> Unit = {}, ) { ScaffoldWithMediumTopBar( @@ -100,6 +103,9 @@ fun SettingsScreen( ) { if (state.isLoggedIn) { itemWithDivider { + DaitaCell(isDaitaEnabled = state.isDaitaEnabled, onDaitaClick = onDaitaClick) + } + itemWithDivider { MultihopCell( isMultihopEnabled = state.multihopEnabled, onMultihopClick = onMultihopClick, @@ -221,6 +227,23 @@ private fun PrivacyPolicy(state: SettingsUiState) { } @Composable +private fun DaitaCell(isDaitaEnabled: Boolean, onDaitaClick: () -> Unit) { + val title = stringResource(id = R.string.daita) + TwoRowCell( + titleText = title, + subtitleText = + stringResource( + if (isDaitaEnabled) { + R.string.on + } else { + R.string.off + } + ), + onCellClicked = onDaitaClick, + ) +} + +@Composable private fun MultihopCell(isMultihopEnabled: Boolean, onMultihopClick: () -> Unit) { val title = stringResource(id = R.string.multihop) TwoRowCell( 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 24b19cfae6..f763272438 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 @@ -37,8 +37,6 @@ 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 @@ -141,7 +139,6 @@ 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() @@ -171,12 +168,6 @@ fun VpnSettings( } } - daitaConfirmationDialogResult.OnNavResultValue { doEnableDaita -> - if (doEnableDaita) { - vm.onToggleDaita(true) - } - } - val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current CollectSideEffectWithLifecycle(vm.uiSideEffect) { @@ -223,16 +214,12 @@ 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, onToggleBlockAds = vm::onToggleBlockAds, onToggleBlockMalware = vm::onToggleBlockMalware, onToggleLocalNetworkSharing = vm::onToggleLocalNetworkSharing, - onDisableDaita = { vm.onToggleDaita(false) }, onToggleBlockAdultContent = vm::onToggleBlockAdultContent, onToggleBlockGambling = vm::onToggleBlockGambling, onToggleBlockSocialMedia = vm::onToggleBlockSocialMedia, @@ -280,15 +267,12 @@ fun VpnSettingsScreen( navigateToQuantumResistanceInfo: () -> Unit = {}, navigateToWireguardPortInfo: (availablePortRanges: List<PortRange>) -> Unit = {}, navigateToLocalNetworkSharingInfo: () -> Unit = {}, - navigateToDaitaInfo: () -> Unit = {}, - navigateToDaitaConfirmation: () -> Unit = {}, navigateToWireguardPortDialog: () -> Unit = {}, navigateToServerIpOverrides: () -> Unit = {}, onToggleBlockTrackers: (Boolean) -> Unit = {}, onToggleBlockAds: (Boolean) -> Unit = {}, onToggleBlockMalware: (Boolean) -> Unit = {}, onToggleLocalNetworkSharing: (Boolean) -> Unit = {}, - onDisableDaita: () -> Unit = {}, onToggleBlockAdultContent: (Boolean) -> Unit = {}, onToggleBlockGambling: (Boolean) -> Unit = {}, onToggleBlockSocialMedia: (Boolean) -> Unit = {}, @@ -502,23 +486,6 @@ fun VpnSettingsScreen( ) } - 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), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt index 8f07ab180e..3538aacff1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt @@ -1,19 +1,28 @@ package net.mullvad.mullvadvpn.compose.screen.location import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.compose.collectAsStateWithLifecycle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.compose.constant.ContentType @@ -23,6 +32,7 @@ import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange import net.mullvad.mullvadvpn.lib.model.RelayItem +import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel import org.koin.androidx.compose.koinViewModel @@ -33,6 +43,7 @@ fun SelectLocationList( backgroundColor: Color, relayListType: RelayListType, onSelectRelay: (RelayItem) -> Unit, + openDaitaSettings: () -> Unit, onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit, ) { val viewModel = @@ -58,11 +69,20 @@ fun SelectLocationList( ), state = lazyListState, horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = + if (state is SelectLocationListUiState.EntryBlocked) { + Arrangement.Center + } else { + Arrangement.Top + }, ) { when (stateActual) { SelectLocationListUiState.Loading -> { loading() } + SelectLocationListUiState.EntryBlocked -> { + entryBlocked(openDaitaSettings = openDaitaSettings) + } is SelectLocationListUiState.Content -> { relayListContent( backgroundColor = backgroundColor, @@ -83,6 +103,28 @@ private fun LazyListScope.loading() { } } +private fun LazyListScope.entryBlocked(openDaitaSettings: () -> Unit) { + item(contentType = ContentType.DESCRIPTION) { + Text( + text = stringResource(R.string.multihop_entry_disabled_description), + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = Dimens.mediumPadding), + ) + } + item(contentType = ContentType.SPACER) { + Spacer(modifier = Modifier.height(Dimens.mediumPadding)) + } + item(contentType = ContentType.BUTTON) { + PrimaryButton( + text = stringResource(R.string.open_daita_settings), + onClick = openDaitaSettings, + modifier = Modifier.padding(horizontal = Dimens.mediumPadding), + ) + } +} + private fun SelectLocationListUiState.indexOfSelectedRelayItem(): Int? = if (this is SelectLocationListUiState.Content) { val index = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt index 3e40d57090..d6d4721f20 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt @@ -3,7 +3,9 @@ package net.mullvad.mullvadvpn.compose.screen.location import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -39,6 +42,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.CreateCustomListDestination import com.ramcosta.composedestinations.generated.destinations.CustomListLocationsDestination import com.ramcosta.composedestinations.generated.destinations.CustomListsDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaDestination import com.ramcosta.composedestinations.generated.destinations.DeleteCustomListDestination import com.ramcosta.composedestinations.generated.destinations.EditCustomListNameDestination import com.ramcosta.composedestinations.generated.destinations.FilterDestination @@ -53,6 +57,7 @@ import net.mullvad.mullvadvpn.compose.button.MullvadSegmentedEndButton import net.mullvad.mullvadvpn.compose.button.MullvadSegmentedStartButton import net.mullvad.mullvadvpn.compose.cell.FilterRow import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.ScaffoldWithSmallTopBar import net.mullvad.mullvadvpn.compose.extensions.dropUnlessResumed import net.mullvad.mullvadvpn.compose.preview.SelectLocationsUiStatePreviewParameterProvider @@ -69,7 +74,7 @@ import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationSideEffect import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationViewModel import org.koin.androidx.compose.koinViewModel -@Preview("Default|Filters|Multihop|Multihop and Filters") +@Preview("Loading|Default|Filters|Multihop|Multihop and Filters") @Composable private fun PreviewSelectLocationScreen( @PreviewParameter(SelectLocationsUiStatePreviewParameterProvider::class) @@ -190,6 +195,7 @@ fun SelectLocation( ) }, onSelectRelayList = vm::selectRelayList, + openDaitaSettings = dropUnlessResumed { navigator.navigate(DaitaDestination) }, ) } @@ -216,6 +222,7 @@ fun SelectLocationScreen( onEditLocationsCustomList: (RelayItem.CustomList) -> Unit = {}, onDeleteCustomList: (RelayItem.CustomList) -> Unit = {}, onSelectRelayList: (RelayListType) -> Unit = {}, + openDaitaSettings: () -> Unit = {}, ) { val backgroundColor = MaterialTheme.colorScheme.surface @@ -232,14 +239,19 @@ fun SelectLocationScreen( }, snackbarHostState = snackbarHostState, actions = { - IconButton(onClick = { onSearchClick(state.relayListType) }) { + IconButton( + enabled = state is SelectLocationUiState.Data, + onClick = { + if (state is SelectLocationUiState.Data) onSearchClick(state.relayListType) + }, + ) { Icon( imageVector = Icons.Default.Search, - contentDescription = stringResource(id = R.string.filter), + contentDescription = stringResource(id = R.string.search), tint = MaterialTheme.colorScheme.onSurface, ) } - IconButton(onClick = onFilterClick) { + IconButton(enabled = state is SelectLocationUiState.Data, onClick = onFilterClick) { Icon( imageVector = Icons.Default.FilterList, contentDescription = stringResource(id = R.string.filter), @@ -261,32 +273,51 @@ fun SelectLocationScreen( onHideBottomSheet = { locationBottomSheetState = null }, ) - Column(modifier = modifier.background(backgroundColor).fillMaxSize()) { - AnimatedContent(targetState = state.filterChips, label = "Select location top bar") { - filterChips -> - if (filterChips.isNotEmpty()) { - FilterRow( - filters = filterChips, - onRemoveOwnershipFilter = removeOwnershipFilter, - onRemoveProviderFilter = removeProviderFilter, - ) + Column( + modifier = modifier.background(backgroundColor).fillMaxSize(), + verticalArrangement = + when (state) { + SelectLocationUiState.Loading -> Arrangement.Center + is SelectLocationUiState.Data -> Arrangement.Top + }, + ) { + when (state) { + SelectLocationUiState.Loading -> { + Loading() } - } + is SelectLocationUiState.Data -> { + AnimatedContent( + targetState = state.filterChips, + label = "Select location top bar", + ) { filterChips -> + if (filterChips.isNotEmpty()) { + FilterRow( + filters = filterChips, + onRemoveOwnershipFilter = removeOwnershipFilter, + onRemoveProviderFilter = removeProviderFilter, + ) + } + } - if (state.multihopEnabled) { - MultihopBar(state.relayListType, onSelectRelayList) - } + if (state.multihopEnabled) { + MultihopBar(state.relayListType, onSelectRelayList) + } - if (state.filterChips.isNotEmpty() || state.multihopEnabled) { - Spacer(modifier = Modifier.height(height = Dimens.verticalSpace)) - } + if (state.filterChips.isNotEmpty() || state.multihopEnabled) { + Spacer(modifier = Modifier.height(height = Dimens.verticalSpace)) + } - RelayLists( - state = state, - backgroundColor = backgroundColor, - onSelectRelay = onSelectRelay, - onUpdateBottomSheetState = { newState -> locationBottomSheetState = newState }, - ) + RelayLists( + state = state, + backgroundColor = backgroundColor, + onSelectRelay = onSelectRelay, + openDaitaSettings = openDaitaSettings, + onUpdateBottomSheetState = { newState -> + locationBottomSheetState = newState + }, + ) + } + } } } } @@ -312,22 +343,15 @@ private fun MultihopBar(relayListType: RelayListType, onSelectRelayList: (RelayL @Composable private fun RelayLists( - state: SelectLocationUiState, + state: SelectLocationUiState.Data, backgroundColor: Color, onSelectRelay: (RelayItem) -> Unit, + openDaitaSettings: () -> Unit, onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit, ) { - // For multihop we want to start on the entry list. - // If multihop is not enabled we want to start on the exit list. - // The exit endpoint is what is selected when multihop is disabled. val pagerState = rememberPagerState( - initialPage = - if (state.multihopEnabled) { - RelayListType.ENTRY.ordinal - } else { - RelayListType.EXIT.ordinal - }, + initialPage = state.relayListType.ordinal, pageCount = { RelayListType.entries.size }, ) LaunchedEffect(state.relayListType) { @@ -349,7 +373,13 @@ private fun RelayLists( backgroundColor = backgroundColor, relayListType = RelayListType.entries[pageIndex], onSelectRelay = onSelectRelay, + openDaitaSettings = openDaitaSettings, onUpdateBottomSheetState = onUpdateBottomSheetState, ) } } + +@Composable +private fun ColumnScope.Loading() { + MullvadCircularProgressIndicatorLarge(modifier = Modifier.align(Alignment.CenterHorizontally)) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DaitaUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DaitaUiState.kt new file mode 100644 index 0000000000..59f00d0347 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DaitaUiState.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.compose.state + +data class DaitaUiState(val daitaEnabled: Boolean, val directOnly: Boolean) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt index bb320de81d..d470187bcf 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt @@ -6,6 +6,8 @@ sealed interface SelectLocationListUiState { data object Loading : SelectLocationListUiState + data object EntryBlocked : SelectLocationListUiState + data class Content( val relayListItems: List<RelayListItem>, val customLists: List<RelayItem.CustomList>, 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 bb61bd4e7d..fd2abab8c4 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 @@ -2,8 +2,12 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.usecase.FilterChip -data class SelectLocationUiState( - val filterChips: List<FilterChip>, - val multihopEnabled: Boolean, - val relayListType: RelayListType, -) +sealed interface SelectLocationUiState { + data object Loading : SelectLocationUiState + + data class Data( + val filterChips: List<FilterChip>, + val multihopEnabled: Boolean, + val relayListType: RelayListType, + ) : SelectLocationUiState +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SettingsUiState.kt index 4ebbf9ad23..ad8dbd0e22 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SettingsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SettingsUiState.kt @@ -4,6 +4,7 @@ data class SettingsUiState( val appVersion: String, val isLoggedIn: Boolean, val isSupportedVersion: Boolean, + val isDaitaEnabled: Boolean, val isPlayBuild: Boolean, val multihopEnabled: Boolean, ) 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 eede76ff7c..49d0ebd4aa 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 @@ -12,7 +12,6 @@ import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem data class VpnSettingsUiState( val mtu: Mtu?, val isLocalNetworkSharingEnabled: Boolean, - val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsItems: List<CustomDnsItem>, val contentBlockersOptions: DefaultDnsOptions, @@ -34,7 +33,6 @@ data class VpnSettingsUiState( fun createDefault( mtu: Mtu? = null, isLocalNetworkSharingEnabled: Boolean = false, - isDaitaEnabled: Boolean = false, isCustomDnsEnabled: Boolean = false, customDnsItems: List<CustomDnsItem> = emptyList(), contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(), @@ -51,7 +49,6 @@ data class VpnSettingsUiState( VpnSettingsUiState( mtu, 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 f43f1caf8f..df35e54006 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 @@ -63,6 +63,7 @@ import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogViewModel import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsViewModel import net.mullvad.mullvadvpn.viewmodel.CustomListsViewModel +import net.mullvad.mullvadvpn.viewmodel.DaitaViewModel import net.mullvad.mullvadvpn.viewmodel.DeleteApiAccessMethodConfirmationViewModel import net.mullvad.mullvadvpn.viewmodel.DeleteCustomListConfirmationViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel @@ -217,8 +218,8 @@ val uiModule = module { viewModel { WireguardCustomPortDialogViewModel(get()) } viewModel { LoginViewModel(get(), get(), get()) } viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) } - viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { SettingsViewModel(get(), get(), get(), IS_PLAY_BUILD) } + viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { SettingsViewModel(get(), get(), get(), get(), IS_PLAY_BUILD) } viewModel { SplashViewModel(get(), get(), get(), get()) } viewModel { VoucherDialogViewModel(get()) } viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get()) } @@ -262,8 +263,9 @@ val uiModule = module { ) } viewModel { (relayListType: RelayListType) -> - SelectLocationListViewModel(relayListType, get(), get(), get(), get(), get(), get()) + SelectLocationListViewModel(relayListType, get(), get(), get(), get(), get(), get(), get()) } + viewModel { DaitaViewModel(get()) } // This view model must be single so we correctly attach lifecycle and share it with activity single { NoDaemonViewModel(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 f21adee735..5584d8e991 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 @@ -95,8 +95,8 @@ private fun RelayItem.Location.City.filter( } } -private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(isDaitaEnabled: Boolean): Boolean { - return if (isDaitaEnabled) daita else true +private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(filterDaita: Boolean): Boolean { + return if (filterDaita) daita else true } private fun RelayItem.Location.Relay.filter( 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 8be8d2ae7e..e6b03ee599 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 @@ -72,4 +72,6 @@ class SettingsRepository( managementService.setAllowLan(isEnabled) suspend fun setDaitaEnabled(enabled: Boolean) = managementService.setDaitaEnabled(enabled) + + suspend fun setDaitaDirectOnly(enabled: Boolean) = managementService.setDaitaDirectOnly(enabled) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt index 366a7321f6..37e5f71ecc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt @@ -8,6 +8,7 @@ import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.Providers +import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository @@ -38,7 +39,7 @@ class FilterChipUseCase( selectedOwnership = selectedOwnership, selectedConstraintProviders = selectedConstraintProviders, allProviders = allProviders, - isDaitaEnabled = settings?.isDaitaEnabled() == true, + daitaDirectOnly = settings?.daitaAndDirectOnly() == true, isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true, relayListType = relayListType, ) @@ -48,7 +49,7 @@ class FilterChipUseCase( selectedOwnership: Constraint<Ownership>, selectedConstraintProviders: Constraint<Providers>, allProviders: List<Provider>, - isDaitaEnabled: Boolean, + daitaDirectOnly: Boolean, isMultihopEnabled: Boolean, relayListType: RelayListType, ): List<FilterChip> { @@ -72,7 +73,7 @@ class FilterChipUseCase( } if ( shouldFilterByDaita( - isDaitaEnabled = isDaitaEnabled, + daitaDirectOnly = daitaDirectOnly, relayListType = relayListType, isMultihopEnabled = isMultihopEnabled, ) @@ -88,6 +89,10 @@ class FilterChipUseCase( ): List<Provider> = if (selectedOwnership == null) selectedProviders else selectedProviders.filter { it.ownership == selectedOwnership } + + private fun Settings.daitaAndDirectOnly() = + tunnelOptions.wireguard.daitaSettings.enabled && + tunnelOptions.wireguard.daitaSettings.directOnly } sealed interface FilterChip { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt index 6712d9275f..15eee7e4ed 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 @@ -6,6 +6,7 @@ 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.lib.model.Settings import net.mullvad.mullvadvpn.relaylist.filter import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository @@ -32,7 +33,7 @@ class FilteredRelayListUseCase( providers = selectedProviders, shouldFilterByDaita = shouldFilterByDaita( - isDaitaEnabled = settings?.isDaitaEnabled() == true, + daitaDirectOnly = settings?.daitaAndDirectOnly() == true, isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true, relayListType = relayListType, ), @@ -44,4 +45,8 @@ class FilteredRelayListUseCase( providers: Constraint<Providers>, shouldFilterByDaita: Boolean, ) = mapNotNull { it.filter(ownership, providers, shouldFilterByDaita) } + + private fun Settings.daitaAndDirectOnly() = + tunnelOptions.wireguard.daitaSettings.enabled && + tunnelOptions.wireguard.daitaSettings.directOnly } 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 c326b176a5..42250ec1dc 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 @@ -7,6 +7,7 @@ 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.lib.model.Settings import net.mullvad.mullvadvpn.relaylist.filter import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.SettingsRepository @@ -33,7 +34,7 @@ class FilterCustomListsRelayItemUseCase( providers = selectedProviders, daita = shouldFilterByDaita( - isDaitaEnabled = settings?.isDaitaEnabled() == true, + daitaDirectOnly = settings?.daitaAndDirectOnly() == true, isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true, relayListType = relayListType, ), @@ -45,4 +46,8 @@ class FilterCustomListsRelayItemUseCase( providers: Constraint<Providers>, daita: Boolean, ) = mapNotNull { it.filter(ownership, providers, daita = daita) } + + private fun Settings.daitaAndDirectOnly() = + tunnelOptions.wireguard.daitaSettings.enabled && + tunnelOptions.wireguard.daitaSettings.directOnly } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt index 717d007f92..8049bc2d28 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt @@ -3,10 +3,10 @@ package net.mullvad.mullvadvpn.util import net.mullvad.mullvadvpn.compose.state.RelayListType fun shouldFilterByDaita( - isDaitaEnabled: Boolean, + daitaDirectOnly: Boolean, isMultihopEnabled: Boolean, relayListType: RelayListType, ) = - isDaitaEnabled && + daitaDirectOnly && (relayListType == RelayListType.ENTRY || !isMultihopEnabled && relayListType == RelayListType.EXIT) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt new file mode 100644 index 0000000000..3243239fed --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt @@ -0,0 +1,38 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.DaitaUiState +import net.mullvad.mullvadvpn.lib.model.Settings +import net.mullvad.mullvadvpn.repository.SettingsRepository + +class DaitaViewModel(private val settingsRepository: SettingsRepository) : ViewModel() { + + val uiState = + settingsRepository.settingsUpdates + .map { settings -> + DaitaUiState( + daitaEnabled = settings?.daitaSettings()?.enabled == true, + directOnly = settings?.daitaSettings()?.directOnly == true, + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = DaitaUiState(daitaEnabled = false, directOnly = false), + ) + + fun setDaita(enable: Boolean) { + viewModelScope.launch { settingsRepository.setDaitaEnabled(enable) } + } + + fun setDirectOnly(enable: Boolean) { + viewModelScope.launch { settingsRepository.setDaitaDirectOnly(enable) } + } + + private fun Settings.daitaSettings() = tunnelOptions.wireguard.daitaSettings +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt index 22309fecfd..5cc6f1562b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.compose.state.SettingsUiState import net.mullvad.mullvadvpn.lib.model.DeviceState import net.mullvad.mullvadvpn.lib.shared.DeviceRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository @@ -16,6 +17,7 @@ class SettingsViewModel( deviceRepository: DeviceRepository, appVersionInfoRepository: AppVersionInfoRepository, wireguardConstraintsRepository: WireguardConstraintsRepository, + settingsRepository: SettingsRepository, isPlayBuild: Boolean, ) : ViewModel() { @@ -24,13 +26,16 @@ class SettingsViewModel( deviceRepository.deviceState, appVersionInfoRepository.versionInfo, wireguardConstraintsRepository.wireguardConstraints, - ) { deviceState, versionInfo, wireguardConstraints -> + settingsRepository.settingsUpdates, + ) { deviceState, versionInfo, wireguardConstraints, settings -> SettingsUiState( isLoggedIn = deviceState is DeviceState.LoggedIn, appVersion = versionInfo.currentVersion, isSupportedVersion = versionInfo.isSupported, + multihopEnabled = wireguardConstraints?.isMultihopEnabled == true, + isDaitaEnabled = + settings?.tunnelOptions?.wireguard?.daitaSettings?.enabled == true, isPlayBuild = isPlayBuild, - multihopEnabled = wireguardConstraints?.isMultihopEnabled ?: false, ) } .stateIn( @@ -40,6 +45,7 @@ class SettingsViewModel( appVersion = "", isLoggedIn = false, isSupportedVersion = true, + isDaitaEnabled = false, isPlayBuild = isPlayBuild, multihopEnabled = false, ), 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 e160776ee0..90f98fceaa 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 @@ -67,9 +67,8 @@ class VpnSettingsViewModel( ) { settings, portRanges, customWgPort, autoStartAndConnectOnBoot -> VpnSettingsViewModelState( mtuValue = settings?.tunnelOptions?.wireguard?.mtu, - isLocalNetworkSharingEnabled = settings?.allowLan ?: false, - isDaitaEnabled = settings?.isDaitaEnabled() ?: false, - isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false, + isLocalNetworkSharingEnabled = settings?.allowLan == true, + isCustomDnsEnabled = settings?.isCustomDnsEnabled() == true, customDnsList = settings?.addresses()?.asStringAddressList() ?: listOf(), contentBlockersOptions = settings?.contentBlockersSettings() ?: DefaultDnsOptions(), 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 624716198d..e8ccf8f4a0 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 @@ -12,7 +12,6 @@ import net.mullvad.mullvadvpn.lib.model.QuantumResistantState data class VpnSettingsViewModelState( val mtuValue: Mtu?, val isLocalNetworkSharingEnabled: Boolean, - val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsList: List<CustomDnsItem>, val contentBlockersOptions: DefaultDnsOptions, @@ -34,7 +33,6 @@ data class VpnSettingsViewModelState( VpnSettingsUiState( mtuValue, isLocalNetworkSharingEnabled, - isDaitaEnabled, isCustomDnsEnabled, customDnsList, contentBlockersOptions, @@ -54,7 +52,6 @@ data class VpnSettingsViewModelState( VpnSettingsViewModelState( mtuValue = null, isLocalNetworkSharingEnabled = false, - isDaitaEnabled = false, isCustomDnsEnabled = false, customDnsList = listOf(), contentBlockersOptions = DefaultDnsOptions(), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt index d5063f0f44..46d8ac519d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt @@ -12,7 +12,9 @@ import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.RelayItemId +import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase @@ -27,16 +29,25 @@ class SelectLocationListViewModel( private val wireguardConstraintsRepository: WireguardConstraintsRepository, private val relayListRepository: RelayListRepository, customListsRelayItemUseCase: CustomListsRelayItemUseCase, + settingsRepository: SettingsRepository, ) : ViewModel() { private val _expandedItems: MutableStateFlow<Set<String>> = MutableStateFlow(initialExpand(initialSelection())) val uiState: StateFlow<SelectLocationListUiState> = - combine(relayListItems(), customListsRelayItemUseCase()) { relayListItems, customLists -> - SelectLocationListUiState.Content( - relayListItems = relayListItems, - customLists = customLists, - ) + combine( + relayListItems(), + customListsRelayItemUseCase(), + settingsRepository.settingsUpdates, + ) { relayListItems, customLists, settings -> + if (relayListType == RelayListType.ENTRY && settings?.entryBlocked() == true) { + SelectLocationListUiState.EntryBlocked + } else { + SelectLocationListUiState.Content( + relayListItems = relayListItems, + customLists = customLists, + ) + } } .stateIn(viewModelScope, SharingStarted.Lazily, SelectLocationListUiState.Loading) @@ -86,4 +97,11 @@ class SelectLocationListViewModel( wireguardConstraintsRepository.wireguardConstraints.value?.entryLocation RelayListType.EXIT -> relayListRepository.selectedLocation.value }?.getOrNull() + + // If Daita is enabled without direct only, it is not possible to manually select the entry + // location. + private fun Settings.entryBlocked() = + tunnelOptions.wireguard.daitaSettings.enabled && + !tunnelOptions.wireguard.daitaSettings.directOnly && + relaySettings.relayConstraints.wireguardConstraints.isMultihopEnabled } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt index dd6736a45d..f78b59c3fb 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt @@ -18,9 +18,11 @@ import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.RelayItem +import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.usecase.FilterChipUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase @@ -34,6 +36,7 @@ class SelectLocationViewModel( private val relayListRepository: RelayListRepository, private val wireguardConstraintsRepository: WireguardConstraintsRepository, private val filterChipUseCase: FilterChipUseCase, + private val settingsRepository: SettingsRepository, ) : ViewModel() { private val _relayListType: MutableStateFlow<RelayListType> = MutableStateFlow(initialRelayListSelection()) @@ -44,30 +47,24 @@ class SelectLocationViewModel( wireguardConstraintsRepository.wireguardConstraints, _relayListType, ) { filterChips, wireguardConstraints, relayListSelection -> - SelectLocationUiState( + SelectLocationUiState.Data( filterChips = filterChips, multihopEnabled = wireguardConstraints?.isMultihopEnabled == true, relayListType = relayListSelection, ) } - .stateIn( - viewModelScope, - SharingStarted.Lazily, - SelectLocationUiState( - filterChips = emptyList(), - multihopEnabled = false, - relayListType = RelayListType.EXIT, - ), - ) + .stateIn(viewModelScope, SharingStarted.Lazily, SelectLocationUiState.Loading) private val _uiSideEffect = Channel<SelectLocationSideEffect>() val uiSideEffect = _uiSideEffect.receiveAsFlow() private fun initialRelayListSelection() = - if (wireguardConstraintsRepository.wireguardConstraints.value?.isMultihopEnabled == true) { - RelayListType.ENTRY - } else { - RelayListType.EXIT + when { + settingsRepository.settingsUpdates.value?.daitaWithoutDirectOnly() == true -> + RelayListType.EXIT + wireguardConstraintsRepository.wireguardConstraints.value?.isMultihopEnabled == true -> + RelayListType.ENTRY + else -> RelayListType.EXIT } private fun filterChips() = _relayListType.flatMapLatest { filterChipUseCase(it) } @@ -133,6 +130,10 @@ class SelectLocationViewModel( fun removeProviderFilter() { viewModelScope.launch { relayListFilterRepository.updateSelectedProviders(Constraint.Any) } } + + private fun Settings.daitaWithoutDirectOnly() = + tunnelOptions.wireguard.daitaSettings.enabled && + !tunnelOptions.wireguard.daitaSettings.directOnly } sealed interface SelectLocationSideEffect { diff --git a/android/app/src/main/res/drawable/daita_illustration_1.xml b/android/app/src/main/res/drawable/daita_illustration_1.xml new file mode 100644 index 0000000000..918f0c9e6e --- /dev/null +++ b/android/app/src/main/res/drawable/daita_illustration_1.xml @@ -0,0 +1,342 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:width="328dp" + android:height="103dp" + android:viewportWidth="328" + android:viewportHeight="103" + tools:ignore="VectorRaster"> + <path + android:pathData="M0,12C0,5.37 5.37,0 12,0H316C322.63,0 328,5.37 328,12V90.5C328,97.13 322.63,102.5 316,102.5H12C5.37,102.5 0,97.13 0,90.5V12Z" + android:fillColor="#152637"/> + <path + android:pathData="M1,12C1,5.92 5.92,1 12,1H316C322.08,1 327,5.92 327,12V90.5C327,96.58 322.08,101.5 316,101.5H12C5.92,101.5 1,96.58 1,90.5V12Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#304358"/> + <path + android:pathData="M75.17,52.39C74.54,52.39 74.03,51.88 74.03,51.25L74.03,50.11C74.03,49.48 74.54,48.97 75.17,48.97C75.8,48.97 76.31,49.48 76.31,50.11L76.31,51.25C76.31,51.88 75.8,52.39 75.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M102.5,52.39C101.87,52.39 101.36,51.88 101.36,51.25L101.36,50.11C101.36,49.48 101.87,48.97 102.5,48.97C103.13,48.97 103.64,49.48 103.64,50.11L103.64,51.25C103.64,51.88 103.13,52.39 102.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M107.06,52.39C106.43,52.39 105.92,51.88 105.92,51.25L105.92,50.11C105.92,49.48 106.43,48.97 107.06,48.97C107.68,48.97 108.19,49.48 108.19,50.11L108.19,51.25C108.19,51.88 107.68,52.39 107.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M111.61,52.39C110.98,52.39 110.47,51.88 110.47,51.25L110.47,50.11C110.47,49.48 110.98,48.97 111.61,48.97C112.24,48.97 112.75,49.48 112.75,50.11L112.75,51.25C112.75,51.88 112.24,52.39 111.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M116.17,52.39C115.54,52.39 115.03,51.88 115.03,51.25L115.03,50.11C115.03,49.48 115.54,48.97 116.17,48.97C116.8,48.97 117.31,49.48 117.31,50.11L117.31,51.25C117.31,51.88 116.8,52.39 116.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M120.72,52.39C120.09,52.39 119.58,51.88 119.58,51.25L119.58,50.11C119.58,49.48 120.09,48.97 120.72,48.97C121.35,48.97 121.86,49.48 121.86,50.11L121.86,51.25C121.86,51.88 121.35,52.39 120.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M125.28,52.39C124.65,52.39 124.14,51.88 124.14,51.25L124.14,50.11C124.14,49.48 124.65,48.97 125.28,48.97C125.91,48.97 126.42,49.48 126.42,50.11L126.42,51.25C126.42,51.88 125.91,52.39 125.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M129.83,52.39C129.2,52.39 128.69,51.88 128.69,51.25L128.69,50.11C128.69,49.48 129.2,48.97 129.83,48.97C130.46,48.97 130.97,49.48 130.97,50.11L130.97,51.25C130.97,51.88 130.46,52.39 129.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M134.39,52.39C133.76,52.39 133.25,51.88 133.25,51.25L133.25,50.11C133.25,49.48 133.76,48.97 134.39,48.97C135.02,48.97 135.53,49.48 135.53,50.11L135.53,51.25C135.53,51.88 135.02,52.39 134.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M138.94,52.39C138.32,52.39 137.81,51.88 137.81,51.25L137.81,50.11C137.81,49.48 138.32,48.97 138.94,48.97C139.57,48.97 140.08,49.48 140.08,50.11L140.08,51.25C140.08,51.88 139.57,52.39 138.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M143.5,52.39C142.87,52.39 142.36,51.88 142.36,51.25L142.36,50.11C142.36,49.48 142.87,48.97 143.5,48.97C144.13,48.97 144.64,49.48 144.64,50.11L144.64,51.25C144.64,51.88 144.13,52.39 143.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M184.5,52.39C183.87,52.39 183.36,51.88 183.36,51.25L183.36,50.11C183.36,49.48 183.87,48.97 184.5,48.97C185.13,48.97 185.64,49.48 185.64,50.11L185.64,51.25C185.64,51.88 185.13,52.39 184.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M189.06,52.39C188.43,52.39 187.92,51.88 187.92,51.25L187.92,50.11C187.92,49.48 188.43,48.97 189.06,48.97C189.68,48.97 190.19,49.48 190.19,50.11L190.19,51.25C190.19,51.88 189.68,52.39 189.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M193.61,52.39C192.98,52.39 192.47,51.88 192.47,51.25L192.47,50.11C192.47,49.48 192.98,48.97 193.61,48.97C194.24,48.97 194.75,49.48 194.75,50.11L194.75,51.25C194.75,51.88 194.24,52.39 193.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M198.17,52.39C197.54,52.39 197.03,51.88 197.03,51.25L197.03,50.11C197.03,49.48 197.54,48.97 198.17,48.97C198.8,48.97 199.31,49.48 199.31,50.11L199.31,51.25C199.31,51.88 198.8,52.39 198.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M202.72,52.39C202.09,52.39 201.58,51.88 201.58,51.25L201.58,50.11C201.58,49.48 202.09,48.97 202.72,48.97C203.35,48.97 203.86,49.48 203.86,50.11L203.86,51.25C203.86,51.88 203.35,52.39 202.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M207.28,52.39C206.65,52.39 206.14,51.88 206.14,51.25L206.14,50.11C206.14,49.48 206.65,48.97 207.28,48.97C207.91,48.97 208.42,49.48 208.42,50.11L208.42,51.25C208.42,51.88 207.91,52.39 207.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M211.83,52.39C211.2,52.39 210.69,51.88 210.69,51.25L210.69,50.11C210.69,49.48 211.2,48.97 211.83,48.97C212.46,48.97 212.97,49.48 212.97,50.11L212.97,51.25C212.97,51.88 212.46,52.39 211.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M216.39,52.39C215.76,52.39 215.25,51.88 215.25,51.25L215.25,50.11C215.25,49.48 215.76,48.97 216.39,48.97C217.02,48.97 217.53,49.48 217.53,50.11L217.53,51.25C217.53,51.88 217.02,52.39 216.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M220.94,52.39C220.32,52.39 219.81,51.88 219.81,51.25L219.81,50.11C219.81,49.48 220.32,48.97 220.94,48.97C221.57,48.97 222.08,49.48 222.08,50.11L222.08,51.25C222.08,51.88 221.57,52.39 220.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M225.5,52.39C224.87,52.39 224.36,51.88 224.36,51.25L224.36,50.11C224.36,49.48 224.87,48.97 225.5,48.97C226.13,48.97 226.64,49.48 226.64,50.11L226.64,51.25C226.64,51.88 226.13,52.39 225.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M230.06,52.39C229.43,52.39 228.92,51.88 228.92,51.25L228.92,50.11C228.92,49.48 229.43,48.97 230.06,48.97C230.68,48.97 231.19,49.48 231.19,50.11L231.19,51.25C231.19,51.88 230.68,52.39 230.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M234.61,52.39C233.98,52.39 233.47,51.88 233.47,51.25L233.47,50.11C233.47,49.48 233.98,48.97 234.61,48.97C235.24,48.97 235.75,49.48 235.75,50.11L235.75,51.25C235.75,51.88 235.24,52.39 234.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M239.17,52.39C238.54,52.39 238.03,51.88 238.03,51.25L238.03,50.11C238.03,49.48 238.54,48.97 239.17,48.97C239.8,48.97 240.31,49.48 240.31,50.11L240.31,51.25C240.31,51.88 239.8,52.39 239.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M243.72,52.39C243.09,52.39 242.58,51.88 242.58,51.25L242.58,50.11C242.58,49.48 243.09,48.97 243.72,48.97C244.35,48.97 244.86,49.48 244.86,50.11L244.86,51.25C244.86,51.88 244.35,52.39 243.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M248.28,52.39C247.65,52.39 247.14,51.88 247.14,51.25L247.14,50.11C247.14,49.48 247.65,48.97 248.28,48.97C248.91,48.97 249.42,49.48 249.42,50.11L249.42,51.25C249.42,51.88 248.91,52.39 248.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M252.83,52.39C252.2,52.39 251.69,51.88 251.69,51.25L251.69,50.11C251.69,49.48 252.2,48.97 252.83,48.97C253.46,48.97 253.97,49.48 253.97,50.11L253.97,51.25C253.97,51.88 253.46,52.39 252.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M257.39,52.39C256.76,52.39 256.25,51.88 256.25,51.25L256.25,50.11C256.25,49.48 256.76,48.97 257.39,48.97C258.02,48.97 258.53,49.48 258.53,50.11L258.53,51.25C258.53,51.88 258.02,52.39 257.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M261.94,52.39C261.32,52.39 260.81,51.88 260.81,51.25L260.81,50.11C260.81,49.48 261.32,48.97 261.94,48.97C262.57,48.97 263.08,49.48 263.08,50.11L263.08,51.25C263.08,51.88 262.57,52.39 261.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M266.5,52.39C265.87,52.39 265.36,51.88 265.36,51.25L265.36,50.11C265.36,49.48 265.87,48.97 266.5,48.97C267.13,48.97 267.64,49.48 267.64,50.11L267.64,51.25C267.64,51.88 267.13,52.39 266.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M271.06,52.39C270.43,52.39 269.92,51.88 269.92,51.25L269.92,50.11C269.92,49.48 270.43,48.97 271.06,48.97C271.68,48.97 272.19,49.48 272.19,50.11L272.19,51.25C272.19,51.88 271.68,52.39 271.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M275.61,52.39C274.98,52.39 274.47,51.88 274.47,51.25L274.47,50.11C274.47,49.48 274.98,48.97 275.61,48.97C276.24,48.97 276.75,49.48 276.75,50.11L276.75,51.25C276.75,51.88 276.24,52.39 275.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M70.61,52.39C69.98,52.39 69.47,51.88 69.47,51.25L69.47,50.11C69.47,49.48 69.98,48.97 70.61,48.97C71.24,48.97 71.75,49.48 71.75,50.11L71.75,51.25C71.75,51.88 71.24,52.39 70.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M97.94,52.39C97.32,52.39 96.81,51.88 96.81,51.25L96.81,50.11C96.81,49.48 97.32,48.97 97.94,48.97C98.57,48.97 99.08,49.48 99.08,50.11L99.08,51.25C99.08,51.88 98.57,52.39 97.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M93.39,52.39C92.76,52.39 92.25,51.88 92.25,51.25L92.25,50.11C92.25,49.48 92.76,48.97 93.39,48.97C94.02,48.97 94.53,49.48 94.53,50.11L94.53,51.25C94.53,51.88 94.02,52.39 93.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M52.39,52.39C51.76,52.39 51.25,51.88 51.25,51.25L51.25,50.11C51.25,49.48 51.76,48.97 52.39,48.97C53.02,48.97 53.53,49.48 53.53,50.11L53.53,51.25C53.53,51.88 53.02,52.39 52.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M56.94,52.39C56.32,52.39 55.81,51.88 55.81,51.25L55.81,50.11C55.81,49.48 56.32,48.97 56.94,48.97C57.57,48.97 58.08,49.48 58.08,50.11L58.08,51.25C58.08,51.88 57.57,52.39 56.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M61.5,52.39C60.87,52.39 60.36,51.88 60.36,51.25L60.36,50.11C60.36,49.48 60.87,48.97 61.5,48.97C62.13,48.97 62.64,49.48 62.64,50.11L62.64,51.25C62.64,51.88 62.13,52.39 61.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M66.06,52.39C65.43,52.39 64.92,51.88 64.92,51.25L64.92,50.11C64.92,49.48 65.43,48.97 66.06,48.97C66.68,48.97 67.19,49.48 67.19,50.11L67.19,51.25C67.19,51.88 66.68,52.39 66.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M79.72,52.39C79.09,52.39 78.58,51.88 78.58,51.25L78.58,50.11C78.58,49.48 79.09,48.97 79.72,48.97C80.35,48.97 80.86,49.48 80.86,50.11L80.86,51.25C80.86,51.88 80.35,52.39 79.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M84.28,52.39C83.65,52.39 83.14,51.88 83.14,51.25L83.14,50.11C83.14,49.48 83.65,48.97 84.28,48.97C84.91,48.97 85.42,49.48 85.42,50.11L85.42,51.25C85.42,51.88 84.91,52.39 84.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M88.83,52.39C88.2,52.39 87.69,51.88 87.69,51.25L87.69,50.11C87.69,49.48 88.2,48.97 88.83,48.97C89.46,48.97 89.97,49.48 89.97,50.11L89.97,51.25C89.97,51.88 89.46,52.39 88.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M52.39,60.36L52.39,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M61.5,55.81L61.5,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M56.94,74.03L56.94,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M79.72,60.36L79.72,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M88.83,55.81L88.83,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M84.28,74.03L84.28,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M107.06,60.36L107.06,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M116.17,55.81L116.17,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M111.61,74.03L111.61,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M134.39,60.36L134.39,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M143.5,55.81L143.5,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M138.94,74.03L138.94,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M184.5,60.36L184.5,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M193.61,55.81L193.61,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M189.06,74.03L189.06,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M211.83,60.36L211.83,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M220.94,55.81L220.94,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M216.39,74.03L216.39,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M239.17,60.36L239.17,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M248.28,55.81L248.28,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M243.72,74.03L243.72,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M266.5,60.36L266.5,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M275.61,55.81L275.61,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M271.06,74.03L271.06,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M301.08,52.39C301.17,51.64 301.24,50.89 301.24,50.11C301.24,49.34 301.17,48.58 301.08,47.83H304.93C305.11,48.56 305.22,49.33 305.22,50.11C305.22,50.9 305.11,51.66 304.93,52.39M299.06,58.72C299.74,57.46 300.27,56.09 300.63,54.67H303.99C302.89,56.57 301.14,58.01 299.06,58.72ZM298.78,52.39H293.45C293.33,51.64 293.26,50.89 293.26,50.11C293.26,49.34 293.33,48.57 293.45,47.83H298.78C298.88,48.57 298.96,49.34 298.96,50.11C298.96,50.89 298.88,51.64 298.78,52.39ZM296.11,59.18C295.17,57.81 294.4,56.3 293.94,54.67H298.29C297.82,56.3 297.06,57.81 296.11,59.18ZM291.56,45.56H288.23C289.32,43.65 291.07,42.21 293.15,41.5C292.47,42.77 291.95,44.13 291.56,45.56ZM288.23,54.67H291.56C291.95,56.09 292.47,57.46 293.15,58.72C291.08,58.01 289.33,56.57 288.23,54.67ZM287.3,52.39C287.11,51.66 287,50.9 287,50.11C287,49.33 287.11,48.56 287.3,47.83H291.15C291.05,48.58 290.99,49.34 290.99,50.11C290.99,50.89 291.05,51.64 291.15,52.39M296.11,41.03C297.06,42.4 297.82,43.93 298.29,45.56H293.94C294.4,43.93 295.17,42.4 296.11,41.03ZM303.99,45.56H300.63C300.28,44.15 299.75,42.78 299.06,41.5C301.16,42.22 302.9,43.67 303.99,45.56ZM296.11,38.72C289.81,38.72 284.72,43.85 284.72,50.11C284.72,53.13 285.92,56.03 288.06,58.16C289.12,59.22 290.37,60.06 291.75,60.63C293.14,61.21 294.62,61.5 296.11,61.5C299.13,61.5 302.03,60.3 304.16,58.16C306.3,56.03 307.5,53.13 307.5,50.11C307.5,48.62 307.2,47.13 306.63,45.75C306.06,44.37 305.22,43.12 304.16,42.06C303.11,41 301.85,40.16 300.47,39.59C299.09,39.02 297.61,38.72 296.11,38.72Z" + android:fillColor="#ffffff" + tools:ignore="VectorPath" /> + <group> + <clip-path + android:pathData="M18.22,37.58h27.33v27.33h-27.33z"/> + <path + android:pathData="M26.19,63.78C25.57,63.78 25.03,63.55 24.59,63.11C24.14,62.66 23.92,62.13 23.92,61.5V41C23.92,40.37 24.14,39.84 24.59,39.39C25.03,38.95 25.57,38.72 26.19,38.72H37.58C38.21,38.72 38.75,38.95 39.19,39.39C39.64,39.84 39.86,40.37 39.86,41V61.5C39.86,62.13 39.64,62.66 39.19,63.11C38.75,63.55 38.21,63.78 37.58,63.78H26.19ZM26.19,60.36V61.5H37.58V60.36H26.19ZM26.19,58.08H37.58V44.42H26.19V58.08ZM26.19,42.14H37.58V41H26.19V42.14Z" + android:fillColor="#ffffff"/> + </group> + <path + android:pathData="M152.61,42.82V48.29C152.61,49.31 153.18,50.11 153.98,50.11H174.14C174.82,50.11 175.5,49.31 175.5,48.29V42.82C175.39,41.8 174.82,41 174.02,41H153.98C153.18,41 152.61,41.8 152.61,42.82ZM161.72,46.69V44.42H160.58V46.69H161.72ZM156.03,46.69H158.31V44.42H156.03V46.69ZM173.11,47.83H154.89V43.28H173.11V47.83ZM152.61,54.21V59.68C152.61,60.7 153.18,61.5 153.98,61.5H174.14C174.82,61.5 175.5,60.7 175.5,59.68V54.21C175.5,53.19 174.93,52.39 174.14,52.39H153.98C153.18,52.39 152.61,53.19 152.61,54.21ZM161.72,58.08V55.81H160.58V58.08H161.72ZM156.03,58.08H158.31V55.81H156.03V58.08ZM173.11,59.22H154.89V54.67H173.11V59.22Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/android/app/src/main/res/drawable/daita_illustration_2.xml b/android/app/src/main/res/drawable/daita_illustration_2.xml new file mode 100644 index 0000000000..b8de37fadf --- /dev/null +++ b/android/app/src/main/res/drawable/daita_illustration_2.xml @@ -0,0 +1,402 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:width="328dp" + android:height="103dp" + android:viewportWidth="328" + android:viewportHeight="103" + tools:ignore="VectorRaster"> + <path + android:pathData="M0,12C0,5.37 5.37,0 12,0H316C322.63,0 328,5.37 328,12V90.5C328,97.13 322.63,102.5 316,102.5H12C5.37,102.5 0,97.13 0,90.5V12Z" + android:fillColor="#152637"/> + <path + android:pathData="M1,12C1,5.92 5.92,1 12,1H316C322.08,1 327,5.92 327,12V90.5C327,96.58 322.08,101.5 316,101.5H12C5.92,101.5 1,96.58 1,90.5V12Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#304358"/> + <path + android:pathData="M75.17,52.39C74.54,52.39 74.03,51.88 74.03,51.25L74.03,50.11C74.03,49.48 74.54,48.97 75.17,48.97C75.8,48.97 76.31,49.48 76.31,50.11L76.31,51.25C76.31,51.88 75.8,52.39 75.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M102.5,52.39C101.87,52.39 101.36,51.88 101.36,51.25L101.36,50.11C101.36,49.48 101.87,48.97 102.5,48.97C103.13,48.97 103.64,49.48 103.64,50.11L103.64,51.25C103.64,51.88 103.13,52.39 102.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M107.06,52.39C106.43,52.39 105.92,51.88 105.92,51.25L105.92,50.11C105.92,49.48 106.43,48.97 107.06,48.97C107.68,48.97 108.19,49.48 108.19,50.11L108.19,51.25C108.19,51.88 107.68,52.39 107.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M111.61,52.39C110.98,52.39 110.47,51.88 110.47,51.25L110.47,50.11C110.47,49.48 110.98,48.97 111.61,48.97C112.24,48.97 112.75,49.48 112.75,50.11L112.75,51.25C112.75,51.88 112.24,52.39 111.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M116.17,52.39C115.54,52.39 115.03,51.88 115.03,51.25L115.03,50.11C115.03,49.48 115.54,48.97 116.17,48.97C116.8,48.97 117.31,49.48 117.31,50.11L117.31,51.25C117.31,51.88 116.8,52.39 116.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M120.72,52.39C120.09,52.39 119.58,51.88 119.58,51.25L119.58,50.11C119.58,49.48 120.09,48.97 120.72,48.97C121.35,48.97 121.86,49.48 121.86,50.11L121.86,51.25C121.86,51.88 121.35,52.39 120.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M125.28,52.39C124.65,52.39 124.14,51.88 124.14,51.25L124.14,50.11C124.14,49.48 124.65,48.97 125.28,48.97C125.91,48.97 126.42,49.48 126.42,50.11L126.42,51.25C126.42,51.88 125.91,52.39 125.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M129.83,52.39C129.2,52.39 128.69,51.88 128.69,51.25L128.69,50.11C128.69,49.48 129.2,48.97 129.83,48.97C130.46,48.97 130.97,49.48 130.97,50.11L130.97,51.25C130.97,51.88 130.46,52.39 129.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M134.39,52.39C133.76,52.39 133.25,51.88 133.25,51.25L133.25,50.11C133.25,49.48 133.76,48.97 134.39,48.97C135.02,48.97 135.53,49.48 135.53,50.11L135.53,51.25C135.53,51.88 135.02,52.39 134.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M138.94,52.39C138.32,52.39 137.81,51.88 137.81,51.25L137.81,50.11C137.81,49.48 138.32,48.97 138.94,48.97C139.57,48.97 140.08,49.48 140.08,50.11L140.08,51.25C140.08,51.88 139.57,52.39 138.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M143.5,52.39C142.87,52.39 142.36,51.88 142.36,51.25L142.36,50.11C142.36,49.48 142.87,48.97 143.5,48.97C144.13,48.97 144.64,49.48 144.64,50.11L144.64,51.25C144.64,51.88 144.13,52.39 143.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M184.5,52.39C183.87,52.39 183.36,51.88 183.36,51.25L183.36,50.11C183.36,49.48 183.87,48.97 184.5,48.97C185.13,48.97 185.64,49.48 185.64,50.11L185.64,51.25C185.64,51.88 185.13,52.39 184.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M189.06,52.39C188.43,52.39 187.92,51.88 187.92,51.25L187.92,50.11C187.92,49.48 188.43,48.97 189.06,48.97C189.68,48.97 190.19,49.48 190.19,50.11L190.19,51.25C190.19,51.88 189.68,52.39 189.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M193.61,52.39C192.98,52.39 192.47,51.88 192.47,51.25L192.47,50.11C192.47,49.48 192.98,48.97 193.61,48.97C194.24,48.97 194.75,49.48 194.75,50.11L194.75,51.25C194.75,51.88 194.24,52.39 193.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M198.17,52.39C197.54,52.39 197.03,51.88 197.03,51.25L197.03,50.11C197.03,49.48 197.54,48.97 198.17,48.97C198.8,48.97 199.31,49.48 199.31,50.11L199.31,51.25C199.31,51.88 198.8,52.39 198.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M202.72,52.39C202.09,52.39 201.58,51.88 201.58,51.25L201.58,50.11C201.58,49.48 202.09,48.97 202.72,48.97C203.35,48.97 203.86,49.48 203.86,50.11L203.86,51.25C203.86,51.88 203.35,52.39 202.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M207.28,52.39C206.65,52.39 206.14,51.88 206.14,51.25L206.14,50.11C206.14,49.48 206.65,48.97 207.28,48.97C207.91,48.97 208.42,49.48 208.42,50.11L208.42,51.25C208.42,51.88 207.91,52.39 207.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M211.83,52.39C211.2,52.39 210.69,51.88 210.69,51.25L210.69,50.11C210.69,49.48 211.2,48.97 211.83,48.97C212.46,48.97 212.97,49.48 212.97,50.11L212.97,51.25C212.97,51.88 212.46,52.39 211.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M216.39,52.39C215.76,52.39 215.25,51.88 215.25,51.25L215.25,50.11C215.25,49.48 215.76,48.97 216.39,48.97C217.02,48.97 217.53,49.48 217.53,50.11L217.53,51.25C217.53,51.88 217.02,52.39 216.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M220.94,52.39C220.32,52.39 219.81,51.88 219.81,51.25L219.81,50.11C219.81,49.48 220.32,48.97 220.94,48.97C221.57,48.97 222.08,49.48 222.08,50.11L222.08,51.25C222.08,51.88 221.57,52.39 220.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M225.5,52.39C224.87,52.39 224.36,51.88 224.36,51.25L224.36,50.11C224.36,49.48 224.87,48.97 225.5,48.97C226.13,48.97 226.64,49.48 226.64,50.11L226.64,51.25C226.64,51.88 226.13,52.39 225.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M230.06,52.39C229.43,52.39 228.92,51.88 228.92,51.25L228.92,50.11C228.92,49.48 229.43,48.97 230.06,48.97C230.68,48.97 231.19,49.48 231.19,50.11L231.19,51.25C231.19,51.88 230.68,52.39 230.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M234.61,52.39C233.98,52.39 233.47,51.88 233.47,51.25L233.47,50.11C233.47,49.48 233.98,48.97 234.61,48.97C235.24,48.97 235.75,49.48 235.75,50.11L235.75,51.25C235.75,51.88 235.24,52.39 234.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M239.17,52.39C238.54,52.39 238.03,51.88 238.03,51.25L238.03,50.11C238.03,49.48 238.54,48.97 239.17,48.97C239.8,48.97 240.31,49.48 240.31,50.11L240.31,51.25C240.31,51.88 239.8,52.39 239.17,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M243.72,52.39C243.09,52.39 242.58,51.88 242.58,51.25L242.58,50.11C242.58,49.48 243.09,48.97 243.72,48.97C244.35,48.97 244.86,49.48 244.86,50.11L244.86,51.25C244.86,51.88 244.35,52.39 243.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M248.28,52.39C247.65,52.39 247.14,51.88 247.14,51.25L247.14,50.11C247.14,49.48 247.65,48.97 248.28,48.97C248.91,48.97 249.42,49.48 249.42,50.11L249.42,51.25C249.42,51.88 248.91,52.39 248.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M252.83,52.39C252.2,52.39 251.69,51.88 251.69,51.25L251.69,50.11C251.69,49.48 252.2,48.97 252.83,48.97C253.46,48.97 253.97,49.48 253.97,50.11L253.97,51.25C253.97,51.88 253.46,52.39 252.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M257.39,52.39C256.76,52.39 256.25,51.88 256.25,51.25L256.25,50.11C256.25,49.48 256.76,48.97 257.39,48.97C258.02,48.97 258.53,49.48 258.53,50.11L258.53,51.25C258.53,51.88 258.02,52.39 257.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M261.94,52.39C261.32,52.39 260.81,51.88 260.81,51.25L260.81,50.11C260.81,49.48 261.32,48.97 261.94,48.97C262.57,48.97 263.08,49.48 263.08,50.11L263.08,51.25C263.08,51.88 262.57,52.39 261.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M266.5,52.39C265.87,52.39 265.36,51.88 265.36,51.25L265.36,50.11C265.36,49.48 265.87,48.97 266.5,48.97C267.13,48.97 267.64,49.48 267.64,50.11L267.64,51.25C267.64,51.88 267.13,52.39 266.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M271.06,52.39C270.43,52.39 269.92,51.88 269.92,51.25L269.92,50.11C269.92,49.48 270.43,48.97 271.06,48.97C271.68,48.97 272.19,49.48 272.19,50.11L272.19,51.25C272.19,51.88 271.68,52.39 271.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M275.61,52.39C274.98,52.39 274.47,51.88 274.47,51.25L274.47,50.11C274.47,49.48 274.98,48.97 275.61,48.97C276.24,48.97 276.75,49.48 276.75,50.11L276.75,51.25C276.75,51.88 276.24,52.39 275.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M70.61,52.39C69.98,52.39 69.47,51.88 69.47,51.25L69.47,50.11C69.47,49.48 69.98,48.97 70.61,48.97C71.24,48.97 71.75,49.48 71.75,50.11L71.75,51.25C71.75,51.88 71.24,52.39 70.61,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M97.94,52.39C97.32,52.39 96.81,51.88 96.81,51.25L96.81,50.11C96.81,49.48 97.32,48.97 97.94,48.97C98.57,48.97 99.08,49.48 99.08,50.11L99.08,51.25C99.08,51.88 98.57,52.39 97.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M93.39,52.39C92.76,52.39 92.25,51.88 92.25,51.25L92.25,50.11C92.25,49.48 92.76,48.97 93.39,48.97C94.02,48.97 94.53,49.48 94.53,50.11L94.53,51.25C94.53,51.88 94.02,52.39 93.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M52.39,52.39C51.76,52.39 51.25,51.88 51.25,51.25L51.25,50.11C51.25,49.48 51.76,48.97 52.39,48.97C53.02,48.97 53.53,49.48 53.53,50.11L53.53,51.25C53.53,51.88 53.02,52.39 52.39,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M56.94,52.39C56.32,52.39 55.81,51.88 55.81,51.25L55.81,50.11C55.81,49.48 56.32,48.97 56.94,48.97C57.57,48.97 58.08,49.48 58.08,50.11L58.08,51.25C58.08,51.88 57.57,52.39 56.94,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M61.5,52.39C60.87,52.39 60.36,51.88 60.36,51.25L60.36,50.11C60.36,49.48 60.87,48.97 61.5,48.97C62.13,48.97 62.64,49.48 62.64,50.11L62.64,51.25C62.64,51.88 62.13,52.39 61.5,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M66.06,52.39C65.43,52.39 64.92,51.88 64.92,51.25L64.92,50.11C64.92,49.48 65.43,48.97 66.06,48.97C66.68,48.97 67.19,49.48 67.19,50.11L67.19,51.25C67.19,51.88 66.68,52.39 66.06,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M79.72,52.39C79.09,52.39 78.58,51.88 78.58,51.25L78.58,50.11C78.58,49.48 79.09,48.97 79.72,48.97C80.35,48.97 80.86,49.48 80.86,50.11L80.86,51.25C80.86,51.88 80.35,52.39 79.72,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M84.28,52.39C83.65,52.39 83.14,51.88 83.14,51.25L83.14,50.11C83.14,49.48 83.65,48.97 84.28,48.97C84.91,48.97 85.42,49.48 85.42,50.11L85.42,51.25C85.42,51.88 84.91,52.39 84.28,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M88.83,52.39C88.2,52.39 87.69,51.88 87.69,51.25L87.69,50.11C87.69,49.48 88.2,48.97 88.83,48.97C89.46,48.97 89.97,49.48 89.97,50.11L89.97,51.25C89.97,51.88 89.46,52.39 88.83,52.39Z" + android:fillColor="#3E5F81" + android:fillType="evenOdd"/> + <path + android:pathData="M52.39,75.17C51.76,75.17 51.25,74.66 51.25,74.03L51.25,28.47C51.25,27.84 51.76,27.33 52.39,27.33C53.02,27.33 53.53,27.84 53.53,28.47L53.53,74.03C53.53,74.66 53.02,75.17 52.39,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M61.5,75.17C60.87,75.17 60.36,74.66 60.36,74.03L60.36,28.47C60.36,27.84 60.87,27.33 61.5,27.33C62.13,27.33 62.64,27.84 62.64,28.47L62.64,74.03C62.64,74.66 62.13,75.17 61.5,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M79.72,75.17C79.09,75.17 78.58,74.66 78.58,74.03L78.58,28.47C78.58,27.84 79.09,27.33 79.72,27.33C80.35,27.33 80.86,27.84 80.86,28.47L80.86,74.03C80.86,74.66 80.35,75.17 79.72,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M88.83,75.17C88.2,75.17 87.69,74.66 87.69,74.03L87.69,28.47C87.69,27.84 88.2,27.33 88.83,27.33C89.46,27.33 89.97,27.84 89.97,28.47L89.97,74.03C89.97,74.66 89.46,75.17 88.83,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M107.06,75.17C106.43,75.17 105.92,74.66 105.92,74.03L105.92,28.47C105.92,27.84 106.43,27.33 107.06,27.33C107.68,27.33 108.19,27.84 108.19,28.47L108.19,74.03C108.19,74.66 107.68,75.17 107.06,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M116.17,75.17C115.54,75.17 115.03,74.66 115.03,74.03L115.03,28.47C115.03,27.84 115.54,27.33 116.17,27.33C116.8,27.33 117.31,27.84 117.31,28.47L117.31,74.03C117.31,74.66 116.8,75.17 116.17,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M134.39,75.17C133.76,75.17 133.25,74.66 133.25,74.03L133.25,28.47C133.25,27.84 133.76,27.33 134.39,27.33C135.02,27.33 135.53,27.84 135.53,28.47L135.53,74.03C135.53,74.66 135.02,75.17 134.39,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M143.5,75.17C142.87,75.17 142.36,74.66 142.36,74.03L142.36,28.47C142.36,27.84 142.87,27.33 143.5,27.33C144.13,27.33 144.64,27.84 144.64,28.47L144.64,74.03C144.64,74.66 144.13,75.17 143.5,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M97.94,75.17C97.32,75.17 96.81,74.66 96.81,74.03L96.81,28.47C96.81,27.84 97.32,27.33 97.94,27.33C98.57,27.33 99.08,27.84 99.08,28.47L99.08,74.03C99.08,74.66 98.57,75.17 97.94,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M66.06,75.17C65.43,75.17 64.92,74.66 64.92,74.03L64.92,28.47C64.92,27.84 65.43,27.33 66.06,27.33C66.68,27.33 67.19,27.84 67.19,28.47L67.19,74.03C67.19,74.66 66.68,75.17 66.06,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M129.83,75.17C129.2,75.17 128.69,74.66 128.69,74.03L128.69,28.47C128.69,27.84 129.2,27.33 129.83,27.33C130.46,27.33 130.97,27.84 130.97,28.47L130.97,74.03C130.97,74.66 130.46,75.17 129.83,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M56.94,75.17C56.32,75.17 55.81,74.66 55.81,74.03L55.81,28.47C55.81,27.84 56.32,27.33 56.94,27.33C57.57,27.33 58.08,27.84 58.08,28.47L58.08,74.03C58.08,74.66 57.57,75.17 56.94,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M84.28,75.17C83.65,75.17 83.14,74.66 83.14,74.03L83.14,28.47C83.14,27.84 83.65,27.33 84.28,27.33C84.91,27.33 85.42,27.84 85.42,28.47L85.42,74.03C85.42,74.66 84.91,75.17 84.28,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M111.61,75.17C110.98,75.17 110.47,74.66 110.47,74.03L110.47,28.47C110.47,27.84 110.98,27.33 111.61,27.33C112.24,27.33 112.75,27.84 112.75,28.47L112.75,74.03C112.75,74.66 112.24,75.17 111.61,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M138.94,75.17C138.32,75.17 137.81,74.66 137.81,74.03L137.81,28.47C137.81,27.84 138.32,27.33 138.94,27.33C139.57,27.33 140.08,27.84 140.08,28.47L140.08,74.03C140.08,74.66 139.57,75.17 138.94,75.17Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:pathData="M52.39,60.36L52.39,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M61.5,55.81L61.5,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M56.94,74.03L56.94,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M79.72,60.36L79.72,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M88.83,55.81L88.83,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M84.28,74.03L84.28,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M107.06,60.36L107.06,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M116.17,55.81L116.17,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M111.61,74.03L111.61,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M134.39,60.36L134.39,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M143.5,55.81L143.5,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M138.94,74.03L138.94,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M184.5,60.36L184.5,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M193.61,55.81L193.61,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M189.06,74.03L189.06,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M211.83,60.36L211.83,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M220.94,55.81L220.94,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M216.39,74.03L216.39,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M239.17,60.36L239.17,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M248.28,55.81L248.28,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M243.72,74.03L243.72,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M266.5,60.36L266.5,42.14" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M275.61,55.81L275.61,46.69" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M271.06,74.03L271.06,28.47" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> + <path + android:pathData="M301.08,52.39C301.17,51.64 301.24,50.89 301.24,50.11C301.24,49.34 301.17,48.58 301.08,47.83H304.93C305.11,48.56 305.22,49.33 305.22,50.11C305.22,50.9 305.11,51.66 304.93,52.39M299.06,58.72C299.74,57.46 300.27,56.09 300.63,54.67H303.99C302.89,56.57 301.14,58.01 299.06,58.72ZM298.78,52.39H293.45C293.33,51.64 293.26,50.89 293.26,50.11C293.26,49.34 293.33,48.57 293.45,47.83H298.78C298.88,48.57 298.96,49.34 298.96,50.11C298.96,50.89 298.88,51.64 298.78,52.39ZM296.11,59.18C295.17,57.81 294.4,56.3 293.94,54.67H298.29C297.82,56.3 297.06,57.81 296.11,59.18ZM291.56,45.56H288.23C289.32,43.65 291.07,42.21 293.15,41.5C292.47,42.77 291.95,44.13 291.56,45.56ZM288.23,54.67H291.56C291.95,56.09 292.47,57.46 293.15,58.72C291.08,58.01 289.33,56.57 288.23,54.67ZM287.3,52.39C287.11,51.66 287,50.9 287,50.11C287,49.33 287.11,48.56 287.3,47.83H291.15C291.05,48.58 290.99,49.34 290.99,50.11C290.99,50.89 291.05,51.64 291.15,52.39M296.11,41.03C297.06,42.4 297.82,43.93 298.29,45.56H293.94C294.4,43.93 295.17,42.4 296.11,41.03ZM303.99,45.56H300.63C300.28,44.15 299.75,42.78 299.06,41.5C301.16,42.22 302.9,43.67 303.99,45.56ZM296.11,38.72C289.81,38.72 284.72,43.85 284.72,50.11C284.72,53.13 285.92,56.03 288.06,58.16C289.12,59.22 290.37,60.06 291.75,60.63C293.14,61.21 294.62,61.5 296.11,61.5C299.13,61.5 302.03,60.3 304.16,58.16C306.3,56.03 307.5,53.13 307.5,50.11C307.5,48.62 307.2,47.13 306.63,45.75C306.06,44.37 305.22,43.12 304.16,42.06C303.11,41 301.85,40.16 300.47,39.59C299.09,39.02 297.61,38.72 296.11,38.72Z" + android:fillColor="#ffffff" + tools:ignore="VectorPath" /> + <group> + <clip-path + android:pathData="M18.22,37.58h27.33v27.33h-27.33z"/> + <path + android:pathData="M26.19,63.78C25.57,63.78 25.03,63.55 24.59,63.11C24.14,62.66 23.92,62.13 23.92,61.5V41C23.92,40.37 24.14,39.84 24.59,39.39C25.03,38.95 25.57,38.72 26.19,38.72H37.58C38.21,38.72 38.75,38.95 39.19,39.39C39.64,39.84 39.86,40.37 39.86,41V61.5C39.86,62.13 39.64,62.66 39.19,63.11C38.75,63.55 38.21,63.78 37.58,63.78H26.19ZM26.19,60.36V61.5H37.58V60.36H26.19ZM26.19,58.08H37.58V44.42H26.19V58.08ZM26.19,42.14H37.58V41H26.19V42.14Z" + android:fillColor="#ffffff"/> + </group> + <path + android:pathData="M152.61,42.82V48.29C152.61,49.31 153.18,50.11 153.98,50.11H174.14C174.82,50.11 175.5,49.31 175.5,48.29V42.82C175.39,41.8 174.82,41 174.02,41H153.98C153.18,41 152.61,41.8 152.61,42.82ZM161.72,46.69V44.42H160.58V46.69H161.72ZM156.03,46.69H158.31V44.42H156.03V46.69ZM173.11,47.83H154.89V43.28H173.11V47.83ZM152.61,54.21V59.68C152.61,60.7 153.18,61.5 153.98,61.5H174.14C174.82,61.5 175.5,60.7 175.5,59.68V54.21C175.5,53.19 174.93,52.39 174.14,52.39H153.98C153.18,52.39 152.61,53.19 152.61,54.21ZM161.72,58.08V55.81H160.58V58.08H161.72ZM156.03,58.08H158.31V55.81H156.03V58.08ZM173.11,59.22H154.89V54.67H173.11V59.22Z" + android:fillColor="#ffffff"/> +</vector> 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 bd27574cbe..d4cedd1e61 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 @@ -123,6 +123,7 @@ import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData as ModelWireguardEndpointData import net.mullvad.mullvadvpn.lib.model.addresses import net.mullvad.mullvadvpn.lib.model.customOptions +import net.mullvad.mullvadvpn.lib.model.enabled import net.mullvad.mullvadvpn.lib.model.entryLocation import net.mullvad.mullvadvpn.lib.model.isMultihopEnabled import net.mullvad.mullvadvpn.lib.model.location @@ -507,17 +508,12 @@ class ManagementService( .mapEmpty() suspend fun setDaitaEnabled(enabled: Boolean): Either<SetDaitaSettingsError, Unit> = - Either.catch { - val daitaSettings = - ManagementInterface.DaitaSettings.newBuilder() - .setEnabled(enabled) - // Before Multihop is supported on Android, calling `setDirectOnly` with - // false will cause undefined behaviour. Will be fixed by as part of - // DROID-1412. - .setDirectOnly(true) - .build() - grpc.setDaitaSettings(daitaSettings) - } + Either.catch { grpc.setEnableDaita(BoolValue.of(enabled)) } + .mapLeft(SetDaitaSettingsError::Unknown) + .mapEmpty() + + suspend fun setDaitaDirectOnly(enabled: Boolean): Either<SetDaitaSettingsError, Unit> = + Either.catch { grpc.setDaitaDirectOnly(BoolValue.of(enabled)) } .mapLeft(SetDaitaSettingsError::Unknown) .mapEmpty() diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt index b3fe88bdc8..f62124a171 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt @@ -8,6 +8,7 @@ import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.DnsOptions import net.mullvad.mullvadvpn.lib.model.DnsState @@ -253,3 +254,9 @@ internal fun ShadowsocksSettings.fromDomain(): ManagementInterface.ShadowsocksSe is Constraint.Only -> ManagementInterface.ShadowsocksSettings.newBuilder().setPort(port.value.value).build() } + +internal fun DaitaSettings.fromDomain(): ManagementInterface.DaitaSettings = + ManagementInterface.DaitaSettings.newBuilder() + .setEnabled(enabled) + .setDirectOnly(directOnly) + .build() 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 0412871f43..c7f47b0c29 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 @@ -26,6 +26,7 @@ import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.Device import net.mullvad.mullvadvpn.lib.model.DeviceId @@ -436,9 +437,12 @@ internal fun ManagementInterface.TunnelOptions.WireguardOptions.toDomain(): Wire WireguardTunnelOptions( mtu = if (hasMtu()) Mtu(mtu) else null, quantumResistant = quantumResistant.toDomain(), - daita = daita.enabled, + daitaSettings = daita.toDomain(), ) +internal fun ManagementInterface.DaitaSettings.toDomain(): DaitaSettings = + DaitaSettings(enabled = enabled, directOnly = directOnly) + internal fun ManagementInterface.QuantumResistantState.toDomain(): QuantumResistantState = when (state) { ManagementInterface.QuantumResistantState.State.AUTO -> QuantumResistantState.Auto diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt new file mode 100644 index 0000000000..791970cf70 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class DaitaSettings(val enabled: Boolean, val directOnly: Boolean) { + companion object +} 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 b3f1a2e8a0..99e8a2b8dc 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 @@ -14,7 +14,5 @@ data class Settings( val splitTunnelSettings: SplitTunnelSettings, val apiAccessMethodSettings: List<ApiAccessMethodSetting>, ) { - fun isDaitaEnabled() = tunnelOptions.wireguard.daita - companion object } 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 70b1599c55..f6a489df12 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,7 +1,12 @@ package net.mullvad.mullvadvpn.lib.model +import arrow.optics.optics + +@optics data class WireguardTunnelOptions( val mtu: Mtu?, val quantumResistant: QuantumResistantState, - val daita: Boolean, -) + val daitaSettings: DaitaSettings, +) { + companion object +} diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 9877098d7c..cd71db65b3 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -359,7 +359,7 @@ <string name="failed_to_set_current_unknown_error">Failed to set to current - Unknown reason</string> <string name="location_was_removed_from_list">%s was removed from \"%s\"</string> <string name="create_custom_list_message">\"%s\" was created</string> - <string name="daita_info">%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.</string> + <string name="daita_info">By enabling \"Direct Only\" you will have to manually select a server that is DAITA-enabled. This can cause you to end up in a blocked state until you have selected a compatible server in the \"Select location\" view.</string> <string name="daita_warning">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 and battery usage.</string> <string name="setting_chip">Setting: %s</string> <string name="enable_anyway">Enable anyway</string> @@ -410,4 +410,16 @@ <string name="search_results">Search results</string> <string name="filters">Filters:</string> <string name="search_query_empty">Type at least 2 characters to start searching.</string> + <string name="daita_description_slide_1_first_paragraph">DAITA (Defense against AI-guided Traffic Analysis) hides patterns in your encrypted VPN traffic.</string> + <string name="daita_description_slide_1_second_paragraph">By using sophisticated AI it’s possible to analyze the traffic of data packets going in and out of your device (even if the traffic is encrypted).</string> + <string name="daita_description_slide_1_third_paragraph">If an observer monitors these data packets, DAITA makes it significantly harder for them to identify which websites you are visiting or with whom you are communicating.</string> + <string name="daita_description_slide_2_first_paragraph">DAITA does this by carefully adding network noise and making all network packets the same size.</string> + <string name="daita_description_slide_2_second_paragraph">Not all our servers are DAITA-enabled. Therefore, we use multihop automatically to enable DAITA with any server.</string> + <string name="daita_description_slide_2_third_paragraph">Attention: Be cautious if you have a limited data plan as this feature will increase your network traffic.</string> + <string name="direct_only">Direct only</string> + <string name="enable_direct_only">Enable \"Direct only\"</string> + <string name="direct_only_description">Not all our servers are DAITA-enabled. In order to use the internet, you might have to select a new location after enabling.</string> + <string name="multihop_entry_disabled_description">The entry server for multihop is currently overridden by DAITA. To select an entry server, please first enable “Direct only” or disable “DAITA” in the settings.</string> + <string name="open_daita_settings">Open DAITA settings</string> + <string name="search">Search</string> </resources> |
