summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-02-19 11:22:51 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-02-19 11:22:51 +0100
commit8ae2314de15388f25df17d435b01130146e42250 (patch)
treea514d7951b2cc51fa8131c1eaf3b7b67d2f68b2c
parentdf37a0da43149c63e60ae604d4d7fefba2de9b5c (diff)
parentf8f7da0221ecbfebe0b1023243e8305e03378a63 (diff)
downloadmullvadvpn-8ae2314de15388f25df17d435b01130146e42250.tar.xz
mullvadvpn-8ae2314de15388f25df17d435b01130146e42250.zip
Merge branch 'add-guide-for-auto-connect-and-lockdown-mode-droid-548'
-rw-r--r--CHANGELOG.md2
-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.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SystemVpnSettingsUseCase.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt5
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt16
-rw-r--r--android/config/detekt.yml1
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt5
-rw-r--r--android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_1_cogwheel.pngbin0 -> 17099 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_2_always_on.pngbin0 -> 20837 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_3_block_connections.pngbin0 -> 21642 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_1_cogwheel.pngbin0 -> 2287 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_2_always_on.pngbin0 -> 3079 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_3_block_connections.pngbin0 -> 3086 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_1_cogwheel.pngbin0 -> 4502 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_2_always_on.pngbin0 -> 7005 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_3_block_connections.pngbin0 -> 6693 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_1_cogwheel.pngbin0 -> 7048 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_2_always_on.pngbin0 -> 11146 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_3_block_connections.pngbin0 -> 10415 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_1_cogwheel.pngbin0 -> 71176 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_2_always_on.pngbin0 -> 88097 bytes
-rw-r--r--android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_3_block_connections.pngbin0 -> 87933 bytes
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml20
-rw-r--r--android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt4
-rw-r--r--gui/locales/messages.pot33
31 files changed, 514 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2707f71b2f..4ea63cf7fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,8 @@ Line wrap the file at 100 chars. Th
#### Android
- Add support for all screen orientations.
- Add toggle for enabling or disabling split tunneling.
+- Replace auto connect with auto connect and lockdown mode guide on platforms that has
+ system vpn settings.
### Fixed
- Fix connectivity issues that would occur when using quantum-resistant tunnels with an incorrectly
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 765975a446..32beb656e5 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
@@ -46,11 +46,13 @@ import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell
import net.mullvad.mullvadvpn.compose.cell.InformationComposeCell
import net.mullvad.mullvadvpn.compose.cell.MtuComposeCell
import net.mullvad.mullvadvpn.compose.cell.MtuSubtitle
+import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
import net.mullvad.mullvadvpn.compose.cell.NormalSwitchComposeCell
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.AutoConnectAndLockdownModeDestination
import net.mullvad.mullvadvpn.compose.destinations.ContentBlockersInfoDialogDestination
import net.mullvad.mullvadvpn.compose.destinations.CustomDnsInfoDialogDestination
import net.mullvad.mullvadvpn.compose.destinations.DnsDialogDestination
@@ -188,6 +190,9 @@ fun VpnSettings(
navigateToContentBlockersInfo = {
navigator.navigate(ContentBlockersInfoDialogDestination) { launchSingleTop = true }
},
+ navigateToAutoConnectScreen = {
+ navigator.navigate(AutoConnectAndLockdownModeDestination) { launchSingleTop = true }
+ },
navigateToCustomDnsInfo = {
navigator.navigate(CustomDnsInfoDialogDestination) { launchSingleTop = true }
},
@@ -245,12 +250,14 @@ fun VpnSettings(
)
}
+@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun VpnSettingsScreen(
uiState: VpnSettingsUiState,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
navigateToContentBlockersInfo: () -> Unit = {},
+ navigateToAutoConnectScreen: () -> Unit = {},
navigateToCustomDnsInfo: () -> Unit = {},
navigateToMalwareInfo: () -> Unit = {},
navigateToObfuscationInfo: () -> Unit = {},
@@ -288,17 +295,34 @@ fun VpnSettingsScreen(
modifier = modifier.testTag(LAZY_LIST_TEST_TAG).animateContentSize(),
state = lazyListState
) {
- item {
- Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
- HeaderSwitchComposeCell(
- title = stringResource(R.string.auto_connect),
- isToggled = uiState.isAutoConnectEnabled,
- isEnabled = true,
- onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
- )
- }
- item {
- SwitchComposeSubtitleCell(text = stringResource(id = R.string.auto_connect_footer))
+ if (uiState.systemVpnSettingsAvailable) {
+ item {
+ Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
+ NavigationComposeCell(
+ title = stringResource(id = R.string.auto_connect_and_lockdown_mode),
+ onClick = { navigateToAutoConnectScreen() },
+ )
+ }
+ item {
+ SwitchComposeSubtitleCell(
+ text = stringResource(id = R.string.auto_connect_and_lockdown_mode_footer)
+ )
+ }
+ } else {
+ item {
+ Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
+ HeaderSwitchComposeCell(
+ title = stringResource(R.string.auto_connect),
+ isToggled = uiState.isAutoConnectEnabled,
+ isEnabled = true,
+ onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
+ )
+ }
+ item {
+ SwitchComposeSubtitleCell(
+ text = stringResource(id = R.string.auto_connect_footer)
+ )
+ }
}
item {
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
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 5525dee8ce..75abbc7cef 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
@@ -20,6 +20,7 @@ data class VpnSettingsUiState(
val selectedWireguardPort: Constraint<Port>,
val customWireguardPort: Constraint<Port>?,
val availablePortRanges: List<PortRange>,
+ val systemVpnSettingsAvailable: Boolean,
) {
companion object {
@@ -35,6 +36,7 @@ data class VpnSettingsUiState(
selectedWireguardPort: Constraint<Port> = Constraint.Any(),
customWireguardPort: Constraint.Only<Port>? = null,
availablePortRanges: List<PortRange> = emptyList(),
+ systemVpnSettingsAvailable: Boolean = false,
) =
VpnSettingsUiState(
mtu,
@@ -48,6 +50,7 @@ data class VpnSettingsUiState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
+ systemVpnSettingsAvailable
)
}
}
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 c62cf03851..0c96f72b3b 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
@@ -34,6 +34,7 @@ import net.mullvad.mullvadvpn.usecase.PlayPaymentUseCase
import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase
import net.mullvad.mullvadvpn.util.ChangelogDataProvider
@@ -109,6 +110,7 @@ val uiModule = module {
single { RelayListUseCase(get(), get()) }
single { OutOfTimeUseCase(get(), get()) }
single { ConnectivityUseCase(get()) }
+ single { SystemVpnSettingsUseCase(androidContext()) }
single { InAppNotificationController(get(), get(), get(), get(), MainScope()) }
@@ -151,7 +153,7 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) }
viewModel { SplashViewModel(get(), get(), get()) }
viewModel { VoucherDialogViewModel(get(), get()) }
- viewModel { VpnSettingsViewModel(get(), get(), get(), get()) }
+ viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get()) }
viewModel { WelcomeViewModel(get(), get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
viewModel { ReportProblemViewModel(get(), get()) }
viewModel { ViewLogsViewModel(get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SystemVpnSettingsUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SystemVpnSettingsUseCase.kt
new file mode 100644
index 0000000000..a2ca2cdc64
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SystemVpnSettingsUseCase.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.usecase
+
+import android.content.Context
+import android.content.Intent
+
+class SystemVpnSettingsUseCase(val context: Context) {
+ fun systemVpnSettingsAvailable(): Boolean =
+ Intent("android.net.vpn.SETTINGS").resolveActivity(context.packageManager) != null
+}
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 3e88ccc28a..3a5514d3d3 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
@@ -36,6 +36,7 @@ import net.mullvad.mullvadvpn.model.WireguardConstraints
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import net.mullvad.mullvadvpn.util.isCustom
sealed interface VpnSettingsSideEffect {
@@ -49,6 +50,7 @@ class VpnSettingsViewModel(
private val resources: Resources,
portRangeUseCase: PortRangeUseCase,
private val relayListUseCase: RelayListUseCase,
+ private val systemVpnSettingsUseCase: SystemVpnSettingsUseCase,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
@@ -75,7 +77,9 @@ class VpnSettingsViewModel(
quantumResistant = settings?.quantumResistant() ?: QuantumResistantState.Off,
selectedWireguardPort = settings?.getWireguardPort() ?: Constraint.Any(),
customWireguardPort = customWgPort,
- availablePortRanges = portRanges
+ availablePortRanges = portRanges,
+ systemVpnSettingsAvailable =
+ systemVpnSettingsUseCase.systemVpnSettingsAvailable()
)
}
.stateIn(
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 fd236e8405..91866d5cc2 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
@@ -20,6 +20,7 @@ data class VpnSettingsViewModelState(
val selectedWireguardPort: Constraint<Port>,
val customWireguardPort: Constraint<Port>?,
val availablePortRanges: List<PortRange>,
+ val systemVpnSettingsAvailable: Boolean,
) {
fun toUiState(): VpnSettingsUiState =
VpnSettingsUiState(
@@ -34,6 +35,7 @@ data class VpnSettingsViewModelState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
+ systemVpnSettingsAvailable
)
companion object {
@@ -51,7 +53,8 @@ data class VpnSettingsViewModelState(
quantumResistant = QuantumResistantState.Off,
selectedWireguardPort = Constraint.Any(),
customWireguardPort = null,
- availablePortRanges = emptyList()
+ availablePortRanges = emptyList(),
+ systemVpnSettingsAvailable = false
)
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
index 51bbe3057c..5d44dca487 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
@@ -27,6 +27,7 @@ import net.mullvad.mullvadvpn.model.WireguardTunnelOptions
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -39,6 +40,7 @@ class VpnSettingsViewModelTest {
private val mockResources: Resources = mockk()
private val mockPortRangeUseCase: PortRangeUseCase = mockk()
private val mockRelayListUseCase: RelayListUseCase = mockk()
+ private val mockSystemVpnSettingsUseCase: SystemVpnSettingsUseCase = mockk(relaxed = true)
private val mockSettingsUpdate = MutableStateFlow<Settings?>(null)
private val portRangeFlow = MutableStateFlow(emptyList<PortRange>())
@@ -56,6 +58,7 @@ class VpnSettingsViewModelTest {
resources = mockResources,
portRangeUseCase = mockPortRangeUseCase,
relayListUseCase = mockRelayListUseCase,
+ systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase,
dispatcher = UnconfinedTestDispatcher()
)
}
@@ -146,4 +149,17 @@ class VpnSettingsViewModelTest {
mockRelayListUseCase.updateSelectedWireguardConstraints(wireguardConstraints)
}
}
+
+ @Test
+ fun `given useCase systemVpnSettingsAvailable is true then uiState should be systemVpnSettingsAvailable=true`() =
+ runTest {
+ val systemVpnSettingsAvailable = true
+
+ every { mockSystemVpnSettingsUseCase.systemVpnSettingsAvailable() } returns
+ systemVpnSettingsAvailable
+
+ viewModel.uiState.test {
+ assertEquals(systemVpnSettingsAvailable, awaitItem().systemVpnSettingsAvailable)
+ }
+ }
}
diff --git a/android/config/detekt.yml b/android/config/detekt.yml
index d6444acd7f..9f42344321 100644
--- a/android/config/detekt.yml
+++ b/android/config/detekt.yml
@@ -761,6 +761,7 @@ style:
excludeImportStatements: true
excludeCommentStatements: false
excludeRawStrings: true
+ ignoreAnnotated: ['Test']
MayBeConst:
active: true
ModifierOrder:
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..8ef70dad92 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,8 @@ 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)
+}
diff --git a/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_1_cogwheel.png b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_1_cogwheel.png
new file mode 100644
index 0000000000..b1f4ec3d93
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_1_cogwheel.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_2_always_on.png b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_2_always_on.png
new file mode 100644
index 0000000000..3a9a10a160
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_2_always_on.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_3_block_connections.png b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_3_block_connections.png
new file mode 100644
index 0000000000..1a31c0a64b
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-hdpi/carousel_slide_3_block_connections.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_1_cogwheel.png b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_1_cogwheel.png
new file mode 100644
index 0000000000..e82ed3d283
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_1_cogwheel.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_2_always_on.png b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_2_always_on.png
new file mode 100644
index 0000000000..8f4a886dd2
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_2_always_on.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_3_block_connections.png b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_3_block_connections.png
new file mode 100644
index 0000000000..88a1cc405e
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-mdpi/carousel_slide_3_block_connections.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_1_cogwheel.png b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_1_cogwheel.png
new file mode 100644
index 0000000000..8c7c25dc1f
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_1_cogwheel.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_2_always_on.png b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_2_always_on.png
new file mode 100644
index 0000000000..3cad85b537
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_2_always_on.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_3_block_connections.png b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_3_block_connections.png
new file mode 100644
index 0000000000..58b0d740d0
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xhdpi/carousel_slide_3_block_connections.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_1_cogwheel.png b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_1_cogwheel.png
new file mode 100644
index 0000000000..5f04ff4c7f
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_1_cogwheel.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_2_always_on.png b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_2_always_on.png
new file mode 100644
index 0000000000..04e6f371b8
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_2_always_on.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_3_block_connections.png b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_3_block_connections.png
new file mode 100644
index 0000000000..8753492ea1
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxhdpi/carousel_slide_3_block_connections.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_1_cogwheel.png b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_1_cogwheel.png
new file mode 100644
index 0000000000..0a0fac8055
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_1_cogwheel.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_2_always_on.png b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_2_always_on.png
new file mode 100644
index 0000000000..4e80ea46c5
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_2_always_on.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_3_block_connections.png b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_3_block_connections.png
new file mode 100644
index 0000000000..5a47ba9a9b
--- /dev/null
+++ b/android/lib/resource/src/main/res/drawable-xxxhdpi/carousel_slide_3_block_connections.png
Binary files differ
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 1c19dab826..b163c9f612 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -73,6 +73,26 @@
<string name="local_network_sharing">Local network sharing</string>
<string name="allow_lan_footer">Allows access to other devices on the same network for sharing, printing etc.</string>
<string name="auto_connect">Auto-connect</string>
+ <string name="auto_connect_and_lockdown_mode">Auto-connect &amp; Lockdown mode</string>
+ <string name="auto_connect_and_lockdown_mode_two_lines">Auto-connect &amp; \nLockdown mode</string>
+ <string name="auto_connect_and_lockdown_mode_footer">Makes sure the device is always on the VPN tunnel.</string>
+ <string name="go_to_vpn_settings">Go to VPN settings</string>
+ <string name="vpn_settings_not_found">There is no VPN settings on your device</string>
+ <string name="auto_connect_carousel_first_slide_top_text">The Auto-connect and Lockdown mode settings can be found in the Android system settings, follow this guide to enable one or both.</string>
+ <string name="auto_connect_carousel_first_slide_bottom_text">
+ <![CDATA[1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name.]]>
+ </string>
+ <string name="auto_connect_carousel_second_slide_top_text">Auto-connect is called Always-on VPN in the Android system settings and it makes sure you are constantly connected to the VPN tunnel and auto connects after restart.</string>
+ <string name="auto_connect_carousel_second_slide_bottom_text">
+ <![CDATA[2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>.]]>
+ </string>
+ <string name="auto_connect_carousel_third_slide_top_text">
+ <![CDATA[The Lockdown mode blocks all internet access if the VPN tunnel is manually disconnected. <br/><b>Warning: This setting blocks split apps and the Local Network Sharing feature</b>.]]>
+ </string>
+ <string name="auto_connect_carousel_third_slide_bottom_text">
+ <![CDATA[3. To enable Lockdown mode, click on the toggle next to <b>Block connections without VPN</b>.]]>
+ </string>
+
<string name="auto_connect_footer">Automatically connect to a server when the app launches.</string>
<string name="wireguard_mtu">WireGuard MTU</string>
<string name="wireguard_mtu_footer">Set WireGuard MTU value. Valid range: %1$d - %2$d.</string>
diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
index 2e412d864e..ef6b04146e 100644
--- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
+++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
@@ -9,6 +9,7 @@ data class Dimensions(
val addIconSize: Dp = 24.dp,
val backButtonSideMargin: Dp = 30.dp,
val bigIconSize: Dp = 44.dp,
+ val bottomPadding: Dp = 4.dp,
val buttonHeight: Dp = 44.dp,
val buttonSpacing: Dp = 12.dp,
val buttonVerticalPadding: Dp = 8.dp,
@@ -41,6 +42,8 @@ data class Dimensions(
val iconFailSuccessTopMargin: Dp = 30.dp,
val iconHeight: Dp = 44.dp,
val indentedCellStartPadding: Dp = 38.dp,
+ val indicatorPadding: Dp = 2.dp,
+ val indicatorSize: Dp = 6.dp,
val infoButtonVerticalPadding: Dp = 13.dp,
val largePadding: Dp = 32.dp,
val listIconSize: Dp = 24.dp,
@@ -76,6 +79,7 @@ data class Dimensions(
val switchIconSize: Dp = 24.dp,
val titleIconSize: Dp = 48.dp,
val topBarHeight: Dp = 64.dp,
+ val topPadding: Dp = 20.dp,
val verticalDividerPadding: Dp = 12.dp,
val verticalSpace: Dp = 20.dp,
val verticalSpacer: Dp = 1.dp,
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index f92ac6dc44..ccd498d2c3 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -2007,6 +2007,15 @@ msgstr ""
msgid "%s was added to your account."
msgstr ""
+msgid "1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name."
+msgstr ""
+
+msgid "2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>."
+msgstr ""
+
+msgid "3. To enable Lockdown mode, click on the toggle next to <b>Block connections without VPN</b>."
+msgstr ""
+
msgid "30 days was added to your account."
msgstr ""
@@ -2046,6 +2055,15 @@ msgstr ""
msgid "Attention: this setting cannot be used in combination with <b>Use custom DNS server</b>."
msgstr ""
+msgid "Auto-connect & Lockdown mode"
+msgstr ""
+
+msgid "Auto-connect & \\nLockdown mode"
+msgstr ""
+
+msgid "Auto-connect is called Always-on VPN in the Android system settings and it makes sure you are constantly connected to the VPN tunnel and auto connects after restart."
+msgstr ""
+
msgid "Blocking internet (device offline)"
msgstr ""
@@ -2085,6 +2103,9 @@ msgstr ""
msgid "Excluded applications"
msgstr ""
+msgid "Go to VPN settings"
+msgstr ""
+
msgid "Going to login will unblock the internet on this device."
msgstr ""
@@ -2100,6 +2121,9 @@ msgstr ""
msgid "Install Mullvad VPN (%s) to stay up to date"
msgstr ""
+msgid "Makes sure the device is always on the VPN tunnel."
+msgstr ""
+
msgid "Manage account"
msgstr ""
@@ -2154,9 +2178,18 @@ msgstr ""
msgid "Submit"
msgstr ""
+msgid "The Auto-connect and Lockdown mode settings can be found in the Android system settings, follow this guide to enable one or both."
+msgstr ""
+
+msgid "The Lockdown mode blocks all internet access if the VPN tunnel is manually disconnected. <br/><b>Warning: This setting blocks split apps and the Local Network Sharing feature</b>."
+msgstr ""
+
msgid "The local DNS server will not work unless you enable \"Local Network Sharing\" under Preferences."
msgstr ""
+msgid "There is no VPN settings on your device"
+msgstr ""
+
msgid "This address has already been entered."
msgstr ""