diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-12-02 10:46:36 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-12-02 11:05:06 +0100 |
| commit | e0ef5463c2087f073fbf86a347903644aa4543ed (patch) | |
| tree | 0308fdc27a7800608898ca00b96c959b2b87f81f /android/app | |
| parent | 64a5704fd0dc57cc73669251da43d4285fa42e92 (diff) | |
| download | mullvadvpn-e0ef5463c2087f073fbf86a347903644aa4543ed.tar.xz mullvadvpn-e0ef5463c2087f073fbf86a347903644aa4543ed.zip | |
Implement support for daita with multihop
Diffstat (limited to 'android/app')
31 files changed, 1229 insertions, 150 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> |
