diff options
| author | MaryamShaghaghi <122574719+MaryamShaghaghi@users.noreply.github.com> | 2023-12-13 13:52:31 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-02-19 11:19:12 +0100 |
| commit | 6634e8dfcb71ea0eda116b1fca3edc2031f55e52 (patch) | |
| tree | 0f94270d01c75db81028563a79fee430d9ddeab8 /android | |
| parent | 0fa52c9746b446318c0baa58036ef7a9e90226c1 (diff) | |
| download | mullvadvpn-6634e8dfcb71ea0eda116b1fca3edc2031f55e52.tar.xz mullvadvpn-6634e8dfcb71ea0eda116b1fca3edc2031f55e52.zip | |
Create auto connect and lockdown mode screen
Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com>
Diffstat (limited to 'android')
5 files changed, 384 insertions, 4 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt index 9a35df1ad3..b53b02670f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt @@ -1,15 +1,18 @@ package net.mullvad.mullvadvpn.compose.component +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar @@ -23,6 +26,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.PrimaryButton +import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar @@ -183,3 +190,63 @@ fun ScaffoldWithMediumTopBar( } ) } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScaffoldWithLargeTopBarAndButton( + appBarTitle: String, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + onButtonClick: () -> Unit = {}, // Add button + buttonTitle: String, + scrollbarColor: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaScrollbar), + content: @Composable (modifier: Modifier) -> Unit +) { + val appBarState = rememberTopAppBarState() + val scrollState = rememberScrollState() + val canScroll = scrollState.canScrollForward || scrollState.canScrollBackward + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState, canScroll = { canScroll }) + Scaffold( + modifier = + modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .systemBarsPadding() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + MullvadLargeTopBar( + title = appBarTitle, + navigationIcon = navigationIcon, + actions, + scrollBehavior = scrollBehavior + ) + }, + bottomBar = { + PrimaryButton( + text = buttonTitle, + onClick = onButtonClick, + modifier = + Modifier.padding( + horizontal = Dimens.sideMargin, + vertical = Dimens.screenVerticalMargin + ), + icon = { + Icon( + painter = painterResource(id = R.drawable.icon_extlink), + contentDescription = null + ) + }, + ) + }, + content = { + content( + Modifier.fillMaxSize() + .padding(it) + .drawVerticalScrollbar(state = scrollState, color = scrollbarColor) + .verticalScroll(scrollState) + ) + } + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt index d7b4541116..52e6e03efb 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Surface @@ -198,6 +199,16 @@ private fun PreviewMediumTopBar() { } } +@Preview +@Composable +private fun PreviewLargeTopBar() { + AppTheme { + MullvadLargeTopBar( + title = "Title", + ) + } +} + @Preview(widthDp = 260) @Composable private fun PreviewSlimMediumTopBar() { @@ -237,6 +248,28 @@ fun MullvadMediumTopBar( ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MullvadLargeTopBar( + title: String, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + LargeTopAppBar( + title = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }, + navigationIcon = navigationIcon, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.mediumTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + scrolledContainerColor = MaterialTheme.colorScheme.background, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary.copy(AlphaTopBar), + ), + actions = actions + ) +} + @Preview @Composable private fun PreviewMullvadTopBarWithLongDeviceName() { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt new file mode 100644 index 0000000000..f4d417e6f3 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt @@ -0,0 +1,274 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.ExperimentalFoundationApi +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.constraintlayout.compose.ConstrainedLayoutReference +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope +import androidx.core.text.HtmlCompat +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndButton +import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString +import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition +import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription +import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible +import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible + +@Preview +@Composable +private fun PreviewAutoConnectAndLockdownModeScreen() { + AppTheme { AutoConnectAndLockdownModeScreen() } +} + +@Destination(style = SlideInFromRightTransition::class) +@Composable +fun AutoConnectAndLockdownMode(navigator: DestinationsNavigator) { + AutoConnectAndLockdownModeScreen(onBackClick = navigator::navigateUp) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AutoConnectAndLockdownModeScreen(onBackClick: () -> Unit = {}) { + val context = LocalContext.current + ScaffoldWithLargeTopBarAndButton( + appBarTitle = stringResource(id = R.string.auto_connect_and_lockdown_mode_two_lines), + navigationIcon = { NavigateBackIconButton(onBackClick) }, + buttonTitle = stringResource(id = R.string.go_to_vpn_settings), + onButtonClick = { context.openVpnSettings() }, + content = { modifier -> + Column(modifier = modifier, verticalArrangement = Arrangement.Center) { + val pagerState = rememberPagerState(pageCount = { PAGES.entries.size }) + val scope = rememberCoroutineScope() + ConstraintLayout( + modifier = Modifier.fillMaxSize(), + ) { + val (pager, backButtonRef, nextButtonRef, pageIndicatorRef) = createRefs() + + AutoConnectCarousel( + pagerState = pagerState, + backButtonRef = backButtonRef, + nextButtonRef = nextButtonRef, + pager = pager + ) + + // Go to previous page + CarouselNavigationButton( + modifier = + Modifier.constrainAs(backButtonRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + }, + onClick = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + isEnabled = { pagerState.currentPage != 0 }, + rotation = 180f + ) + + // Go to next page + CarouselNavigationButton( + modifier = + Modifier.constrainAs(nextButtonRef) { + top.linkTo(parent.top) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + }, + onClick = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + isEnabled = { pagerState.currentPage != pagerState.pageCount - 1 }, + rotation = 0f + ) + + PageIndicator( + pagerState = pagerState, + pageIndicatorRef = pageIndicatorRef, + pager = pager + ) + } + } + } + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ConstraintLayoutScope.AutoConnectCarousel( + pagerState: PagerState, + backButtonRef: ConstrainedLayoutReference, + nextButtonRef: ConstrainedLayoutReference, + pager: ConstrainedLayoutReference +) { + HorizontalPager( + state = pagerState, + beyondBoundsPageCount = 2, + modifier = + Modifier.constrainAs(pager) { + top.linkTo(parent.top) + start.linkTo(backButtonRef.end) + end.linkTo(nextButtonRef.start) + bottom.linkTo(parent.bottom) + } + ) { page -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.padding(horizontal = Dimens.largePadding), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondary, + text = + HtmlCompat.fromHtml( + stringResource(id = PAGES.entries[page].topText), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + .toAnnotatedString( + boldSpanStyle = + SpanStyle( + fontWeight = FontWeight.ExtraBold, + color = MaterialTheme.colorScheme.onPrimary + ) + ) + ) + Image( + modifier = Modifier.padding(top = Dimens.topPadding, bottom = Dimens.bottomPadding), + painter = painterResource(id = PAGES.entries[page].image), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(horizontal = Dimens.largePadding), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondary, + text = + HtmlCompat.fromHtml( + stringResource(id = PAGES.entries[page].bottomText), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + .toAnnotatedString( + boldSpanStyle = + SpanStyle( + fontWeight = FontWeight.ExtraBold, + color = MaterialTheme.colorScheme.onPrimary + ) + ) + ) + } + } +} + +@Composable +private fun CarouselNavigationButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + isEnabled: () -> Boolean, + rotation: Float, +) { + IconButton( + modifier = modifier.alpha(if (isEnabled.invoke()) AlphaVisible else AlphaInvisible), + onClick = onClick, + enabled = isEnabled.invoke() + ) { + Icon( + painter = painterResource(id = R.drawable.icon_chevron), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.rotate(rotation).alpha(AlphaDescription) + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ConstraintLayoutScope.PageIndicator( + pagerState: PagerState, + pageIndicatorRef: ConstrainedLayoutReference, + pager: ConstrainedLayoutReference +) { + Row( + Modifier.wrapContentHeight().fillMaxWidth().padding(top = Dimens.topPadding).constrainAs( + pageIndicatorRef + ) { + top.linkTo(pager.bottom) + end.linkTo(parent.end) + start.linkTo(parent.start) + }, + 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 PAGES(val topText: Int, val image: Int, val bottomText: Int) { + FIRST( + R.string.auto_connect_carousel_first_slide_top_text, + R.drawable.carousel_slide_1_cogwheel, + R.string.auto_connect_carousel_first_slide_bottom_text + ), + SECOND( + R.string.auto_connect_carousel_second_slide_top_text, + R.drawable.carousel_slide_2_always_on, + R.string.auto_connect_carousel_second_slide_bottom_text + ), + THIRD( + R.string.auto_connect_carousel_third_slide_top_text, + R.drawable.carousel_slide_3_block_connections, + R.string.auto_connect_carousel_third_slide_bottom_text + ) +} 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 2fae02ba1e..7a58771167 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 @@ -53,7 +53,7 @@ import net.mullvad.mullvadvpn.compose.cell.SelectableCell 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.destinations.AutoConnectAndLockdownModeScreenDestination +import net.mullvad.mullvadvpn.compose.destinations.AutoConnectAndLockdownModeDestination import net.mullvad.mullvadvpn.compose.destinations.ContentBlockersInfoDialogDestination import net.mullvad.mullvadvpn.compose.destinations.CustomDnsInfoDialogDestination import net.mullvad.mullvadvpn.compose.destinations.DnsDialogDestination @@ -193,9 +193,7 @@ fun VpnSettings( navigator.navigate(ContentBlockersInfoDialogDestination) { launchSingleTop = true } }, navigateToAutoConnectScreen = { - navigator.navigate(AutoConnectAndLockdownModeScreenDestination) { - launchSingleTop = true - } + navigator.navigate(AutoConnectAndLockdownModeDestination) { launchSingleTop = true } }, navigateToCustomDnsInfo = { navigator.navigate(CustomDnsInfoDialogDestination) { launchSingleTop = true } diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt index b983e3538d..836a583284 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt @@ -41,3 +41,11 @@ fun Context.openLink(uri: Uri) { val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) } + +fun Context.openVpnSettings() { + val intent = Intent("android.settings.VPN_SETTINGS") + startActivity(intent) +} + +fun Context.vpnSettingsAvailable(): Boolean = + Intent("android.net.vpn.SETTINGS").resolveActivity(packageManager) != null |
