ui: add Mullvad info view (#450)

Updates tailscale/tailscale#9421

Adds a view to highlight Mullvad support when it is not enabled.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
angott/dot-915
Andrea Gottardo 4 months ago committed by GitHub
parent b9917c8647
commit 1465b2a67f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -59,6 +59,7 @@ import com.tailscale.ipn.ui.view.MainViewNavigation
import com.tailscale.ipn.ui.view.ManagedByView import com.tailscale.ipn.ui.view.ManagedByView
import com.tailscale.ipn.ui.view.MullvadExitNodePicker import com.tailscale.ipn.ui.view.MullvadExitNodePicker
import com.tailscale.ipn.ui.view.MullvadExitNodePickerList import com.tailscale.ipn.ui.view.MullvadExitNodePickerList
import com.tailscale.ipn.ui.view.MullvadInfoView
import com.tailscale.ipn.ui.view.PeerDetails import com.tailscale.ipn.ui.view.PeerDetails
import com.tailscale.ipn.ui.view.PermissionsView import com.tailscale.ipn.ui.view.PermissionsView
import com.tailscale.ipn.ui.view.RunExitNodeView import com.tailscale.ipn.ui.view.RunExitNodeView
@ -179,6 +180,7 @@ class MainActivity : ComponentActivity() {
}, },
onNavigateBackToExitNodes = backTo("exitNodes"), onNavigateBackToExitNodes = backTo("exitNodes"),
onNavigateToMullvad = { navController.navigate("mullvad") }, onNavigateToMullvad = { navController.navigate("mullvad") },
onNavigateToMullvadInfo = { navController.navigate("mullvad_info") },
onNavigateBackToMullvad = backTo("mullvad"), onNavigateBackToMullvad = backTo("mullvad"),
onNavigateToMullvadCountry = { navController.navigate("mullvad/$it") }, onNavigateToMullvadCountry = { navController.navigate("mullvad/$it") },
onNavigateToRunAsExitNode = { navController.navigate("runExitNode") }) onNavigateToRunAsExitNode = { navController.navigate("runExitNode") })
@ -198,6 +200,7 @@ class MainActivity : ComponentActivity() {
composable("settings") { SettingsView(settingsNav) } composable("settings") { SettingsView(settingsNav) }
composable("exitNodes") { ExitNodePicker(exitNodePickerNav) } composable("exitNodes") { ExitNodePicker(exitNodePickerNav) }
composable("mullvad") { MullvadExitNodePickerList(exitNodePickerNav) } composable("mullvad") { MullvadExitNodePickerList(exitNodePickerNav) }
composable("mullvad_info") { MullvadInfoView(exitNodePickerNav) }
composable( composable(
"mullvad/{countryCode}", "mullvad/{countryCode}",
arguments = arguments =

@ -49,6 +49,7 @@ fun ExitNodePicker(
val mullvadExitNodesByCountryCode by model.mullvadExitNodesByCountryCode.collectAsState() val mullvadExitNodesByCountryCode by model.mullvadExitNodesByCountryCode.collectAsState()
val mullvadExitNodeCount by model.mullvadExitNodeCount.collectAsState() val mullvadExitNodeCount by model.mullvadExitNodeCount.collectAsState()
val anyActive by model.anyActive.collectAsState() val anyActive by model.anyActive.collectAsState()
val shouldShowMullvadInfo by model.shouldShowMullvadInfo.collectAsState()
val allowLANAccess = Notifier.prefs.collectAsState().value?.ExitNodeAllowLANAccess == true val allowLANAccess = Notifier.prefs.collectAsState().value?.ExitNodeAllowLANAccess == true
val showRunAsExitNode by MDMSettings.runExitNode.flow.collectAsState() val showRunAsExitNode by MDMSettings.runExitNode.flow.collectAsState()
val allowLanAccessMDMDisposition by MDMSettings.exitNodeAllowLANAccess.flow.collectAsState() val allowLanAccessMDMDisposition by MDMSettings.exitNodeAllowLANAccess.flow.collectAsState()
@ -91,6 +92,11 @@ fun ExitNodePicker(
MullvadItem( MullvadItem(
nav, mullvadExitNodesByCountryCode.size, mullvadExitNodesByCountryCode.selected) nav, mullvadExitNodesByCountryCode.size, mullvadExitNodesByCountryCode.selected)
} }
} else if (shouldShowMullvadInfo) {
item(key = "mullvad_info") {
Lists.SectionDivider()
MullvadInfoItem(nav)
}
} }
if (!allowLanAccessMDMDisposition.hiddenFromUser) { if (!allowLanAccessMDMDisposition.hiddenFromUser) {
@ -167,6 +173,24 @@ fun MullvadItem(nav: ExitNodePickerNav, count: Int, selected: Boolean) {
} }
} }
@Composable
fun MullvadInfoItem(nav: ExitNodePickerNav) {
Box {
ListItem(
modifier = Modifier.clickable { nav.onNavigateToMullvadInfo() },
headlineContent = {
Text(
stringResource(R.string.mullvad_exit_nodes),
style = MaterialTheme.typography.bodyMedium)
},
supportingContent = {
Text(
stringResource(R.string.enable_in_the_admin_console),
style = MaterialTheme.typography.bodyMedium)
})
}
}
@Composable @Composable
fun RunAsExitNodeItem( fun RunAsExitNodeItem(
nav: ExitNodePickerNav, nav: ExitNodePickerNav,

@ -0,0 +1,56 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
@Composable
fun MullvadInfoView(nav: ExitNodePickerNav) {
Scaffold(
topBar = {
Header(R.string.choose_mullvad_exit_node, onBack = nav.onNavigateBackToExitNodes)
}) { innerPadding ->
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 48.dp),
modifier = Modifier.padding(innerPadding)) {
item {
Image(
painter = painterResource(id = R.drawable.mullvad_logo),
contentDescription = stringResource(R.string.the_mullvad_vpn_logo))
}
item {
Text(
stringResource(R.string.mullvad_info_title),
fontFamily = MaterialTheme.typography.titleLarge.fontFamily,
fontSize = MaterialTheme.typography.titleLarge.fontSize,
fontWeight = FontWeight.SemiBold)
}
item {
Text(
stringResource(R.string.mullvad_info_explainer),
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center)
}
}
}
}

@ -23,6 +23,7 @@ data class ExitNodePickerNav(
val onNavigateBackHome: () -> Unit, val onNavigateBackHome: () -> Unit,
val onNavigateBackToExitNodes: () -> Unit, val onNavigateBackToExitNodes: () -> Unit,
val onNavigateToMullvad: () -> Unit, val onNavigateToMullvad: () -> Unit,
val onNavigateToMullvadInfo: () -> Unit,
val onNavigateBackToMullvad: () -> Unit, val onNavigateBackToMullvad: () -> Unit,
val onNavigateToMullvadCountry: (String) -> Unit, val onNavigateToMullvadCountry: (String) -> Unit,
val onNavigateToRunAsExitNode: () -> Unit, val onNavigateToRunAsExitNode: () -> Unit,
@ -55,6 +56,7 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
val mullvadBestAvailableByCountry: StateFlow<Map<String, ExitNode>> = MutableStateFlow(TreeMap()) val mullvadBestAvailableByCountry: StateFlow<Map<String, ExitNode>> = MutableStateFlow(TreeMap())
val mullvadExitNodeCount: StateFlow<Int> = MutableStateFlow(0) val mullvadExitNodeCount: StateFlow<Int> = MutableStateFlow(0)
val anyActive: StateFlow<Boolean> = MutableStateFlow(false) val anyActive: StateFlow<Boolean> = MutableStateFlow(false)
val shouldShowMullvadInfo: StateFlow<Boolean> = MutableStateFlow(false)
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -128,6 +130,13 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
mullvadBestAvailableByCountry.set(bestAvailableByCountry) mullvadBestAvailableByCountry.set(bestAvailableByCountry)
anyActive.set(allNodes.any { it.selected }) anyActive.set(allNodes.any { it.selected })
prefs?.let { prefs ->
// Only show the Mullvad info view if the user is an admin and is using a Tailscale
// control server, as it wouldn't be actionable information otherwise.
shouldShowMullvadInfo.set(
netmap.SelfNode.isAdmin && prefs.ControlURL.endsWith(".tailscale.com"))
}
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -283,4 +283,10 @@
<string name="specifies_a_list_of_apps_that_will_always_use_tailscale_routes_and_dns_when_tailscale_is_running_all_other_apps_won_t_use_tailscale_if_this_value_is_non_empty">Specifies a list of apps that will always use Tailscale routes and DNS when Tailscale is running. All other apps won\'t use Tailscale if this value is non-empty.</string> <string name="specifies_a_list_of_apps_that_will_always_use_tailscale_routes_and_dns_when_tailscale_is_running_all_other_apps_won_t_use_tailscale_if_this_value_is_non_empty">Specifies a list of apps that will always use Tailscale routes and DNS when Tailscale is running. All other apps won\'t use Tailscale if this value is non-empty.</string>
<string name="included_packages">Included packages</string> <string name="included_packages">Included packages</string>
<string name="excluded_packages">Excluded packages</string> <string name="excluded_packages">Excluded packages</string>
<!-- Strings for the Mullvad info view -->
<string name="enable_in_the_admin_console">Enable in the admin console</string>
<string name="mullvad_info_explainer">Once you enable Mullvad VPN on the admin console, you\'ll be able to encrypt and route your traffic using Mullvads global network of servers as exit nodes.</string>
<string name="mullvad_info_title">Mullvad VPN is not configured</string>
<string name="the_mullvad_vpn_logo">The Mullvad VPN logo</string>
</resources> </resources>

Loading…
Cancel
Save