android: add MDM info to exit node picker and banner (#414)

android: add MDM information in exit node banner and picker

Updates tailscale/corp#19122

Signed-off-by: kari-ts <kari@tailscale.com>
pull/416/head
kari-ts 4 months ago committed by GitHub
parent 8f62f0da79
commit 15da8f3797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -162,11 +162,20 @@ val ColorScheme.customError: Color
@Composable @Composable
get() = get() =
if (isSystemInDarkTheme()) { if (isSystemInDarkTheme()) {
Color(0xFF940821) // red-600 Color(0xFF940821) // red-600
} else { } else {
Color(0xFFB22D30) // red-500 Color(0xFFB22D30) // red-500
} }
val ColorScheme.customErrorContainer: Color
@Composable
get() =
if (isSystemInDarkTheme()) {
Color(0xFF760012) // red-700
} else {
Color(0xFF940821) // red-600
}
/** /**
* Main color scheme for list items, uses onPrimaryContainer color for leading and trailing icons. * Main color scheme for list items, uses onPrimaryContainer color for leading and trailing icons.
*/ */

@ -18,8 +18,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.mdm.MDMSettings
@ -27,6 +29,7 @@ import com.tailscale.ipn.mdm.ShowHide
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.disabledListItem import com.tailscale.ipn.ui.theme.disabledListItem
import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.theme.off
import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.util.itemsWithDividers
@ -51,17 +54,27 @@ fun ExitNodePicker(
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()
val managedByOrganization by model.managedByOrganization.collectAsState()
LazyColumn(modifier = Modifier.padding(innerPadding)) { LazyColumn(modifier = Modifier.padding(innerPadding)) {
item(key = "header") { item(key = "header") {
ExitNodeItem( if (MDMSettings.exitNodeID.flow.value != null){
model, Text(
ExitNodePickerViewModel.ExitNode( text =
label = stringResource(R.string.none), managedByOrganization?.let {
online = MutableStateFlow(true), stringResource(R.string.exit_node_mdm_orgname, it)
selected = !anyActive, } ?: stringResource(R.string.exit_node_mdm) ,
)) style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 4.dp))
ExitNodeItem(
model,
ExitNodePickerViewModel.ExitNode(
label = stringResource(R.string.none),
online = MutableStateFlow(true),
selected = !anyActive,
))
}
if (showRunAsExitNode == ShowHide.Show) { if (showRunAsExitNode == ShowHide.Show) {
Lists.ItemDivider() Lists.ItemDivider()
RunAsExitNodeItem(nav = nav, viewModel = model, anyActive) RunAsExitNodeItem(nav = nav, viewModel = model, anyActive)
@ -105,7 +118,7 @@ fun ExitNodeItem(
Box { Box {
var modifier: Modifier = Modifier var modifier: Modifier = Modifier
if (online && !isRunningExitNode) { if (online && !isRunningExitNode && MDMSettings.exitNodeID.flow.value != null) {
modifier = modifier.clickable { viewModel.setExitNode(node) } modifier = modifier.clickable { viewModel.setExitNode(node) }
} }
ListItem( ListItem(

@ -52,6 +52,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -71,6 +72,7 @@ import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.Netmap import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.Permissions import com.tailscale.ipn.ui.model.Permissions
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.theme.customErrorContainer
import com.tailscale.ipn.ui.theme.disabled import com.tailscale.ipn.ui.theme.disabled
import com.tailscale.ipn.ui.theme.errorButton import com.tailscale.ipn.ui.theme.errorButton
import com.tailscale.ipn.ui.theme.errorListItem import com.tailscale.ipn.ui.theme.errorListItem
@ -88,6 +90,7 @@ import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.IpnViewModel.NodeState
import com.tailscale.ipn.ui.viewModel.MainViewModel import com.tailscale.ipn.ui.viewModel.MainViewModel
// Navigation actions for the MainView // Navigation actions for the MainView
@ -109,7 +112,8 @@ fun MainView(
Column( Column(
modifier = Modifier.fillMaxWidth().padding(paddingInsets), modifier = Modifier.fillMaxWidth().padding(paddingInsets),
verticalArrangement = Arrangement.Center) { verticalArrangement = Arrangement.Center) {
// Assume VPN has been prepared for optimistic UI. Whether or not it has been prepared cannot be known // Assume VPN has been prepared for optimistic UI. Whether or not it has been prepared
// cannot be known
// until permission has been granted to prepare the VPN. // until permission has been granted to prepare the VPN.
val isPrepared by viewModel.vpnPrepared.collectAsState(initial = true) val isPrepared by viewModel.vpnPrepared.collectAsState(initial = true)
val isOn by viewModel.vpnToggleState.collectAsState(initial = false) val isOn by viewModel.vpnToggleState.collectAsState(initial = false)
@ -205,28 +209,11 @@ fun MainView(
} }
} }
enum class NodeState {
NONE,
ACTIVE_AND_RUNNING,
// Last selected exit node is active but is not being used.
ACTIVE_NOT_RUNNING,
// Last selected exit node is currently offline.
OFFLINE_ENABLED,
// Last selected exit node has been de-selected and is currently offline.
OFFLINE_DISABLED,
// Exit node selection is managed by an administrator, and last selected exit node is currently
// offline
OFFLINE_MDM,
RUNNING_AS_EXIT_NODE
}
@Composable @Composable
fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
val nodeState by viewModel.nodeState.collectAsState()
val maybePrefs by viewModel.prefs.collectAsState() val maybePrefs by viewModel.prefs.collectAsState()
val netmap by viewModel.netmap.collectAsState() val netmap by viewModel.netmap.collectAsState()
val isRunningExitNode by viewModel.isRunningExitNode.collectAsState()
var nodeState by remember { mutableStateOf(NodeState.NONE) }
// There's nothing to render if we haven't loaded the prefs yet // There's nothing to render if we haven't loaded the prefs yet
val prefs = maybePrefs ?: return val prefs = maybePrefs ?: return
@ -238,121 +225,117 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
val exitNodePeer = chosenExitNodeId?.let { id -> netmap?.Peers?.find { it.StableID == id } } val exitNodePeer = chosenExitNodeId?.let { id -> netmap?.Peers?.find { it.StableID == id } }
val name = exitNodePeer?.ComputedName val name = exitNodePeer?.ComputedName
val online = exitNodePeer?.Online val managedByOrganization by viewModel.managedByOrganization.collectAsState()
LaunchedEffect(prefs.ExitNodeID, exitNodePeer?.Online, isRunningExitNode) { Box(
when { modifier =
exitNodePeer?.Online == false -> { Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surfaceContainer)) {
if (MDMSettings.exitNodeID.flow.value != null) { if (nodeState == NodeState.OFFLINE_MDM) {
nodeState = NodeState.OFFLINE_MDM Box(
} else if (prefs.activeExitNodeID != null) { modifier =
nodeState = NodeState.OFFLINE_ENABLED Modifier.padding(start = 16.dp, end = 16.dp, top = 56.dp, bottom = 16.dp)
} else { .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
nodeState = NodeState.OFFLINE_DISABLED .background(MaterialTheme.colorScheme.customErrorContainer)
} .fillMaxWidth()
} .align(Alignment.TopCenter)) {
exitNodePeer != null -> { Column(
if (!prefs.activeExitNodeID.isNullOrEmpty()) { modifier =
nodeState = NodeState.ACTIVE_AND_RUNNING Modifier.padding(start = 16.dp, end = 16.dp, top = 36.dp, bottom = 16.dp)) {
} else { Text(
nodeState = NodeState.ACTIVE_NOT_RUNNING text =
managedByOrganization?.let {
stringResource(R.string.exit_node_offline_mdm_orgname, it)
} ?: stringResource(R.string.exit_node_offline_mdm),
style = MaterialTheme.typography.bodyMedium,
color = Color.White)
}
}
} }
}
isRunningExitNode -> {
nodeState = NodeState.RUNNING_AS_EXIT_NODE
}
else -> {
nodeState = NodeState.NONE
}
}
}
// (jonathan) TODO: We will block the "enable/disable" button for an exit node for which we cannot Box(
// find a peer on purpose and render the "No Exit Node" state, however, that should modifier =
// eventually show up in the UI as an error case so the user knows to pick an available node. Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 16.dp)
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer)) { .fillMaxWidth()) {
Box( ListItem(
modifier = modifier = Modifier.clickable { navAction() },
Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 16.dp) colors =
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) when (nodeState) {
.fillMaxWidth()) { NodeState.ACTIVE_AND_RUNNING -> MaterialTheme.colorScheme.primaryListItem
ListItem( NodeState.ACTIVE_NOT_RUNNING -> MaterialTheme.colorScheme.primaryListItem
modifier = Modifier.clickable { navAction() }, NodeState.RUNNING_AS_EXIT_NODE -> MaterialTheme.colorScheme.warningListItem
colors = NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorListItem
when (nodeState) { NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorListItem
NodeState.ACTIVE_AND_RUNNING -> MaterialTheme.colorScheme.primaryListItem NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorListItem
NodeState.ACTIVE_NOT_RUNNING -> MaterialTheme.colorScheme.primaryListItem else ->
NodeState.RUNNING_AS_EXIT_NODE -> MaterialTheme.colorScheme.warningListItem ListItemDefaults.colors(
NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorListItem containerColor = MaterialTheme.colorScheme.surface)
NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorListItem },
NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorListItem overlineContent = {
else -> Text(
ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surface) text =
if (nodeState == NodeState.OFFLINE_ENABLED ||
nodeState == NodeState.OFFLINE_DISABLED ||
nodeState == NodeState.OFFLINE_MDM)
stringResource(R.string.exit_node_offline)
else stringResource(R.string.exit_node),
style = MaterialTheme.typography.bodySmall,
)
}, },
overlineContent = { headlineContent = {
Text( Row(verticalAlignment = Alignment.CenterVertically) {
text = Text(
if (nodeState == NodeState.OFFLINE_ENABLED || text =
nodeState == NodeState.OFFLINE_DISABLED || when (nodeState) {
nodeState == NodeState.OFFLINE_MDM) NodeState.NONE -> stringResource(id = R.string.none)
stringResource(R.string.exit_node_offline) NodeState.RUNNING_AS_EXIT_NODE ->
else stringResource(R.string.exit_node), stringResource(id = R.string.running_exit_node)
style = MaterialTheme.typography.bodySmall, else -> name ?: ""
) },
}, style = MaterialTheme.typography.bodyMedium,
headlineContent = { maxLines = 1,
Row(verticalAlignment = Alignment.CenterVertically) { overflow = TextOverflow.Ellipsis)
Text( Icon(
text = imageVector = Icons.Outlined.ArrowDropDown,
when (nodeState) { contentDescription = null,
NodeState.NONE -> stringResource(id = R.string.none) tint =
NodeState.RUNNING_AS_EXIT_NODE -> if (nodeState == NodeState.NONE)
stringResource(id = R.string.running_exit_node) MaterialTheme.colorScheme.onSurfaceVariant
else -> name ?: "" else MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f),
}, )
style = MaterialTheme.typography.bodyMedium, }
maxLines = 1, },
overflow = TextOverflow.Ellipsis) trailingContent = {
Icon( if (nodeState != NodeState.NONE) {
imageVector = Icons.Outlined.ArrowDropDown, Button(
contentDescription = null, colors =
tint = when (nodeState) {
if (nodeState == NodeState.NONE) NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorButton
MaterialTheme.colorScheme.onSurfaceVariant NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorButton
else MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f), NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorButton
) NodeState.RUNNING_AS_EXIT_NODE ->
} MaterialTheme.colorScheme.warningButton
}, else -> MaterialTheme.colorScheme.secondaryButton
trailingContent = { },
if (nodeState != NodeState.NONE) { onClick = {
Button( if (nodeState == NodeState.RUNNING_AS_EXIT_NODE)
colors = viewModel.setRunningExitNode(false)
when (nodeState) { else viewModel.toggleExitNode()
NodeState.OFFLINE_ENABLED -> MaterialTheme.colorScheme.errorButton }) {
NodeState.OFFLINE_DISABLED -> MaterialTheme.colorScheme.errorButton Text(
NodeState.OFFLINE_MDM -> MaterialTheme.colorScheme.errorButton when (nodeState) {
NodeState.RUNNING_AS_EXIT_NODE -> NodeState.OFFLINE_DISABLED -> stringResource(id = R.string.enable)
MaterialTheme.colorScheme.warningButton NodeState.ACTIVE_NOT_RUNNING ->
else -> MaterialTheme.colorScheme.secondaryButton stringResource(id = R.string.enable)
}, NodeState.RUNNING_AS_EXIT_NODE ->
onClick = { stringResource(id = R.string.stop)
if (nodeState == NodeState.RUNNING_AS_EXIT_NODE) else -> stringResource(id = R.string.disable)
viewModel.setRunningExitNode(false) })
else viewModel.toggleExitNode() }
}) { }
Text( })
when (nodeState) { }
NodeState.OFFLINE_DISABLED -> stringResource(id = R.string.enable) }
NodeState.ACTIVE_NOT_RUNNING -> stringResource(id = R.string.enable)
NodeState.RUNNING_AS_EXIT_NODE -> stringResource(id = R.string.stop)
else -> stringResource(id = R.string.disable)
})
}
}
})
}
}
} }
@Composable @Composable

@ -15,12 +15,12 @@ import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.UserID import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.notifier.Notifier.prefs
import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper
import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
@ -40,7 +40,28 @@ open class IpnViewModel : ViewModel() {
private var selfNodeUserId: UserID? = null private var selfNodeUserId: UserID? = null
val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false) val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false)
var lastPrefs: Ipn.Prefs? = null private var lastPrefs: Ipn.Prefs? = null
val prefs = Notifier.prefs
val netmap = Notifier.netmap
private val _nodeState = MutableStateFlow(NodeState.NONE)
val nodeState: StateFlow<NodeState> = _nodeState
val managedByOrganization = MDMSettings.managedByOrganizationName.flow
enum class NodeState {
NONE,
ACTIVE_AND_RUNNING,
// Last selected exit node is active but is not being used.
ACTIVE_NOT_RUNNING,
// Last selected exit node is currently offline.
OFFLINE_ENABLED,
// Last selected exit node has been de-selected and is currently offline.
OFFLINE_DISABLED,
// Exit node selection is managed by an administrator, and last selected exit node is currently
// offline
OFFLINE_MDM,
RUNNING_AS_EXIT_NODE
}
init { init {
// Check if the user has granted permission yet. // Check if the user has granted permission yet.
@ -83,6 +104,44 @@ open class IpnViewModel : ViewModel() {
} }
viewModelScope.launch { loadUserProfiles() } viewModelScope.launch { loadUserProfiles() }
viewModelScope.launch {
combine(prefs, netmap, isRunningExitNode) { prefs, netmap, isRunningExitNode ->
// Handle nullability for prefs and netmap
val validPrefs = prefs ?: return@combine NodeState.NONE
val validNetmap = netmap ?: return@combine NodeState.NONE
val chosenExitNodeId = validPrefs.activeExitNodeID ?: validPrefs.selectedExitNodeID
val exitNodePeer =
chosenExitNodeId?.let { id -> validNetmap.Peers?.find { it.StableID == id } }
when {
exitNodePeer?.Online == false -> {
if (MDMSettings.exitNodeID.flow.value != null) {
NodeState.OFFLINE_MDM
} else if (validPrefs.activeExitNodeID != null) {
NodeState.OFFLINE_ENABLED
} else {
NodeState.OFFLINE_DISABLED
}
}
exitNodePeer != null -> {
if (!validPrefs.activeExitNodeID.isNullOrEmpty()) {
NodeState.ACTIVE_AND_RUNNING
} else {
NodeState.ACTIVE_NOT_RUNNING
}
}
isRunningExitNode == true -> {
NodeState.RUNNING_AS_EXIT_NODE
}
else -> {
NodeState.NONE
}
}
}
.collect { nodeState -> _nodeState.value = nodeState }
}
Log.d(TAG, "Created") Log.d(TAG, "Created")
} }

@ -5,7 +5,6 @@ package com.tailscale.ipn.ui.viewModel
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.App import com.tailscale.ipn.App
@ -42,9 +41,6 @@ class MainViewModel : IpnViewModel() {
// The current state of the IPN for determining view visibility // The current state of the IPN for determining view visibility
val ipnState = Notifier.state val ipnState = Notifier.state
val prefs = Notifier.prefs
val netmap = Notifier.netmap
// The active search term for filtering peers // The active search term for filtering peers
val searchTerm: StateFlow<String> = MutableStateFlow("") val searchTerm: StateFlow<String> = MutableStateFlow("")
@ -97,10 +93,6 @@ class MainViewModel : IpnViewModel() {
viewModelScope.launch { viewModelScope.launch {
searchTerm.collect { term -> peers.set(peerCategorizer.groupedAndFilteredPeers(term)) } searchTerm.collect { term -> peers.set(peerCategorizer.groupedAndFilteredPeers(term)) }
} }
viewModelScope.launch {
Notifier.prefs.collect { prefs -> Log.d(TAG, "Main VM - prefs = ${prefs}") }
}
} }
fun showVPNPermissionLauncherIfUnauthorized() { fun showVPNPermissionLauncherIfUnauthorized() {

@ -27,7 +27,6 @@ class PeerDetailsViewModelFactory(private val nodeId: StableNodeID, private val
} }
class PeerDetailsViewModel(val nodeId: StableNodeID, val filesDir: File) : IpnViewModel() { class PeerDetailsViewModel(val nodeId: StableNodeID, val filesDir: File) : IpnViewModel() {
val netmap: StateFlow<Netmap.NetworkMap?> = MutableStateFlow(null)
val node: StateFlow<Tailcfg.Node?> = MutableStateFlow(null) val node: StateFlow<Tailcfg.Node?> = MutableStateFlow(null)
init { init {

@ -29,7 +29,6 @@ data class SettingsNav(
class SettingsViewModel : IpnViewModel() { class SettingsViewModel : IpnViewModel() {
// Display name for the logged in user // Display name for the logged in user
val isAdmin: StateFlow<Boolean> = MutableStateFlow(false) val isAdmin: StateFlow<Boolean> = MutableStateFlow(false)
val managedByOrganization = MDMSettings.managedByOrganizationName.flow
// True if tailnet lock is enabled. nil if not yet known. // True if tailnet lock is enabled. nil if not yet known.
val tailNetLockEnabled: StateFlow<Boolean?> = MutableStateFlow(null) val tailNetLockEnabled: StateFlow<Boolean?> = MutableStateFlow(null)
// True if tailscaleDNS is enabled. nil if not yet known. // True if tailscaleDNS is enabled. nil if not yet known.

@ -53,6 +53,8 @@
<string name="exit_node">EXIT NODE</string> <string name="exit_node">EXIT NODE</string>
<string name="exit_node_offline">EXIT NODE OFFLINE</string> <string name="exit_node_offline">EXIT NODE OFFLINE</string>
<string name="running_exit_node">Running on this device</string> <string name="running_exit_node">Running on this device</string>
<string name="exit_node_offline_mdm_orgname">The exit node selected by %1$s is currently offline. Internet traffic is blocked. Contact a network administrator for help.</string>
<string name="exit_node_offline_mdm">The exit node selected by your organization is currently offline. Internet traffic is blocked. Contact a network administrator for help.</string>
<string name="starting">Starting…</string> <string name="starting">Starting…</string>
<string name="connect_to_tailnet_prefix">"Connect again to talk to the other devices in the "</string> <string name="connect_to_tailnet_prefix">"Connect again to talk to the other devices in the "</string>
<string name="connect_to_tailnet_suffix">" tailnet."</string> <string name="connect_to_tailnet_suffix">" tailnet."</string>
@ -142,6 +144,8 @@
<string name="run_exit_node_explainer_running">Other devices in your tailnet can now route their internet traffic through this Android device. Make sure to approve this exit node in the admin console in order for other devices to see it.</string> <string name="run_exit_node_explainer_running">Other devices in your tailnet can now route their internet traffic through this Android device. Make sure to approve this exit node in the admin console in order for other devices to see it.</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="exit_node_mdm_orgname">%1$s requires you to use an exit node.</string>
<string name="exit_node_mdm">Your organization requires you to use an exit node.</string>
<!-- Strings for the tailnet lock screen --> <!-- Strings for the tailnet lock screen -->
<string name="tailnet_lock">Tailnet lock</string> <string name="tailnet_lock">Tailnet lock</string>

Loading…
Cancel
Save