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 index f4d417e6f37f..5a01e4c94164 100644 --- 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 @@ -1,5 +1,7 @@ package net.mullvad.mullvadvpn.compose.screen +import android.net.Uri +import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -16,6 +18,7 @@ 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.foundation.text.ClickableText import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -31,8 +34,14 @@ 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.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.UrlAnnotation +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout @@ -46,12 +55,15 @@ 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.openLink 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 +import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD +import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild @Preview @Composable @@ -87,7 +99,8 @@ fun AutoConnectAndLockdownModeScreen(onBackClick: () -> Unit = {}) { pagerState = pagerState, backButtonRef = backButtonRef, nextButtonRef = nextButtonRef, - pager = pager + pager = pager, + onOpenUrl = { url -> context.openLink(Uri.parse(url)) } ) // Go to previous page @@ -131,17 +144,18 @@ fun AutoConnectAndLockdownModeScreen(onBackClick: () -> Unit = {}) { ) } } - } + }, ) } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalTextApi::class) @Composable private fun ConstraintLayoutScope.AutoConnectCarousel( pagerState: PagerState, backButtonRef: ConstrainedLayoutReference, nextButtonRef: ConstrainedLayoutReference, - pager: ConstrainedLayoutReference + pager: ConstrainedLayoutReference, + onOpenUrl: (String) -> Unit ) { HorizontalPager( state = pagerState, @@ -152,32 +166,27 @@ private fun ConstraintLayoutScope.AutoConnectCarousel( start.linkTo(backButtonRef.end) end.linkTo(nextButtonRef.start) bottom.linkTo(parent.bottom) - } - ) { page -> + }, + ) { pageIndex -> + val page = PAGES.entries[pageIndex] Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - Text( + val annotatedTopText = page.annotatedTopText() + ClickableText( 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 - ) - ) + text = annotatedTopText, + onClick = { + annotatedTopText.getUrlAnnotations(it, it).let { annotation -> + annotation.firstOrNull()?.item?.url?.let { onOpenUrl(it) } + } + }, ) Image( modifier = Modifier.padding(top = Dimens.topPadding, bottom = Dimens.bottomPadding), - painter = painterResource(id = PAGES.entries[page].image), + painter = painterResource(id = page.image), contentDescription = null, ) Text( @@ -186,7 +195,7 @@ private fun ConstraintLayoutScope.AutoConnectCarousel( color = MaterialTheme.colorScheme.onSecondary, text = HtmlCompat.fromHtml( - stringResource(id = PAGES.entries[page].bottomText), + stringResource(id = page.bottomText), HtmlCompat.FROM_HTML_MODE_COMPACT ) .toAnnotatedString( @@ -255,20 +264,86 @@ private fun ConstraintLayoutScope.PageIndicator( } } -private enum class PAGES(val topText: Int, val image: Int, val bottomText: Int) { +@Composable +private fun buildTopText(@StringRes id: Int) = buildAnnotatedString { + withStyle( + style = SpanStyle(color = MaterialTheme.colorScheme.onSecondary), + ) { + append( + HtmlCompat.fromHtml(stringResource(id = id), HtmlCompat.FROM_HTML_MODE_COMPACT) + .toAnnotatedString( + boldSpanStyle = + SpanStyle( + fontWeight = FontWeight.ExtraBold, + color = MaterialTheme.colorScheme.onPrimary + ) + ) + ) + } +} + +@OptIn(ExperimentalTextApi::class) +@Composable +private fun buildLockdownTopText() = buildAnnotatedString { + append(buildTopText(id = R.string.auto_connect_carousel_third_slide_top_text)) + append(" ") + + withLink( + UrlAnnotation( + stringResource(id = R.string.lockdown_url).appendHideNavOnPlayBuild(IS_PLAY_BUILD) + ) + ) { + withStyle( + style = + SpanStyle( + color = MaterialTheme.colorScheme.onPrimary, + textDecoration = TextDecoration.Underline + ), + ) { + append( + stringResource( + id = R.string.auto_connect_carousel_third_slide_top_text_website_link + ) + ) + append(".") + } + } +} + +// Will be replaced by upstream withLink in Compose UI 1.7.0 +@OptIn(ExperimentalTextApi::class) +inline fun AnnotatedString.Builder.withLink( + annotation: UrlAnnotation, + block: AnnotatedString.Builder.() -> R +): R { + val index = pushUrlAnnotation(annotation) + return try { + block(this) + } finally { + pop(index) + } +} + +private enum class PAGES( + val annotatedTopText: @Composable () -> AnnotatedString, + val image: Int, + val bottomText: Int +) { FIRST( - R.string.auto_connect_carousel_first_slide_top_text, + annotatedTopText = + @Composable { buildTopText(id = R.string.auto_connect_carousel_first_slide_top_text) }, R.drawable.carousel_slide_1_cogwheel, - R.string.auto_connect_carousel_first_slide_bottom_text + R.string.auto_connect_carousel_first_slide_bottom_text, ), SECOND( - R.string.auto_connect_carousel_second_slide_top_text, + annotatedTopText = + @Composable { buildTopText(id = 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, + annotatedTopText = @Composable { buildLockdownTopText() }, R.drawable.carousel_slide_3_block_connections, - R.string.auto_connect_carousel_third_slide_bottom_text + R.string.auto_connect_carousel_third_slide_bottom_text, ) } diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 67d5707631b6..3b60f3a4aa09 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -87,10 +87,11 @@ Always-on VPN.]]> - Warning: This setting blocks split apps and the Local Network Sharing feature.]]> + Block connections without VPN in the Android system settings. It helps minimize leaks, however it has some known limitations which you can read more about it]]> + here - Block connections without VPN.]]> + Block connections without VPN.

Warning: This setting blocks split apps and the Local Network Sharing feature.]]>
Automatically connect to a server when the app launches. WireGuard MTU diff --git a/android/lib/resource/src/main/res/values/strings_non_translatable.xml b/android/lib/resource/src/main/res/values/strings_non_translatable.xml index 23a4d7c23f85..c364ebff42a7 100644 --- a/android/lib/resource/src/main/res/values/strings_non_translatable.xml +++ b/android/lib/resource/src/main/res/values/strings_non_translatable.xml @@ -6,6 +6,8 @@ https://mullvad.net/download/vpn/android https://mullvad.net/help/tag/mullvad-app/ https://mullvad.net/help/privacy-policy/ + + https://mullvad.net/help/using-mullvad-vpn-on-android Split tunneling WireGuard