summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorMaryamShaghaghi <122574719+MaryamShaghaghi@users.noreply.github.com>2023-12-13 13:52:31 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-02-19 11:19:12 +0100
commit6634e8dfcb71ea0eda116b1fca3edc2031f55e52 (patch)
tree0f94270d01c75db81028563a79fee430d9ddeab8 /android
parent0fa52c9746b446318c0baa58036ef7a9e90226c1 (diff)
downloadmullvadvpn-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')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt67
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt33
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt274
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt6
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt8
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