android: add Tailnet lock setup UI (#231)

Updates ENG-2981

Adds a view to see the Tailnet lock settings and copy node key and public key, resembling the iOS and macOS ones.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
pull/233/head
Andrea Gottardo 3 months ago committed by GitHub
parent 19adff3077
commit f96e9b923f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -39,6 +39,7 @@ import com.tailscale.ipn.ui.view.MullvadExitNodePicker
import com.tailscale.ipn.ui.view.PeerDetails import com.tailscale.ipn.ui.view.PeerDetails
import com.tailscale.ipn.ui.view.RunExitNodeView import com.tailscale.ipn.ui.view.RunExitNodeView
import com.tailscale.ipn.ui.view.Settings import com.tailscale.ipn.ui.view.Settings
import com.tailscale.ipn.ui.view.TailnetLockSetupView
import com.tailscale.ipn.ui.view.UserSwitcherView import com.tailscale.ipn.ui.view.UserSwitcherView
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.IpnViewModel import com.tailscale.ipn.ui.viewModel.IpnViewModel
@ -79,6 +80,7 @@ class MainActivity : ComponentActivity() {
SettingsNav( SettingsNav(
onNavigateToBugReport = { navController.navigate("bugReport") }, onNavigateToBugReport = { navController.navigate("bugReport") },
onNavigateToAbout = { navController.navigate("about") }, onNavigateToAbout = { navController.navigate("about") },
onNavigateToTailnetLock = { navController.navigate("tailnetLock") },
onNavigateToMDMSettings = { navController.navigate("mdmSettings") }, onNavigateToMDMSettings = { navController.navigate("mdmSettings") },
onNavigateToManagedBy = { navController.navigate("managedBy") }, onNavigateToManagedBy = { navController.navigate("managedBy") },
onNavigateToUserSwitcher = { navController.navigate("userSwitcher") }, onNavigateToUserSwitcher = { navController.navigate("userSwitcher") },
@ -116,6 +118,7 @@ class MainActivity : ComponentActivity() {
PeerDetails(nav = backNav, it.arguments?.getString("nodeId") ?: "") PeerDetails(nav = backNav, it.arguments?.getString("nodeId") ?: "")
} }
composable("bugReport") { BugReportView(nav = backNav) } composable("bugReport") { BugReportView(nav = backNav) }
composable("tailnetLock") { TailnetLockSetupView(nav = backNav) }
composable("about") { AboutView(nav = backNav) } composable("about") { AboutView(nav = backNav) }
composable("mdmSettings") { MDMSettingsDebugView(nav = backNav) } composable("mdmSettings") { MDMSettingsDebugView(nav = backNav) }
composable("managedBy") { ManagedByView(nav = backNav) } composable("managedBy") { ManagedByView(nav = backNav) }

@ -10,9 +10,6 @@ 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.IpnState import com.tailscale.ipn.ui.model.IpnState
import com.tailscale.ipn.ui.util.InputStreamAdapter import com.tailscale.ipn.ui.util.InputStreamAdapter
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -21,6 +18,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
private object Endpoint { private object Endpoint {
const val DEBUG = "debug" const val DEBUG = "debug"
@ -47,6 +47,8 @@ private object Endpoint {
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
typealias TailnetLockStatusResponseHandler = (Result<IpnState.NetworkLockStatus>) -> Unit
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
@ -107,6 +109,10 @@ class Client(private val scope: CoroutineScope) {
return post(Endpoint.LOGOUT, responseHandler = responseHandler) return post(Endpoint.LOGOUT, responseHandler = responseHandler)
} }
fun tailnetLockStatus(responseHandler: TailnetLockStatusResponseHandler) {
get(Endpoint.TKA_STATUS, responseHandler = responseHandler)
}
private inline fun <reified T> get( private inline fun <reified T> get(
path: String, path: String,
body: ByteArray? = null, body: ByteArray? = null,

@ -81,13 +81,19 @@ class IpnState {
@Serializable @Serializable
data class NetworkLockStatus( data class NetworkLockStatus(
var Enabled: Boolean, var Enabled: Boolean? = null,
var PublicKey: String, var PublicKey: String? = null,
var NodeKey: String, var NodeKey: String? = null,
var NodeKeySigned: Boolean, var NodeKeySigned: Boolean? = null,
var FilteredPeers: List<TKAFilteredPeer>? = null, var FilteredPeers: List<TKAFilteredPeer>? = null,
var StateID: ULong? = null, var StateID: ULong? = null,
) var TrustedKeys: List<TKAKey>? = null
) {
fun IsPublicKeyTrusted(): Boolean {
return TrustedKeys?.any { it.Key == PublicKey } == true
}
}
@Serializable @Serializable
data class TKAFilteredPeer( data class TKAFilteredPeer(
@ -96,6 +102,8 @@ class IpnState {
var NodeKey: String, var NodeKey: String,
) )
@Serializable data class TKAKey(var Key: String)
@Serializable @Serializable
data class PingResult( data class PingResult(
var IP: Addr, var IP: Addr,

@ -0,0 +1,82 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.util
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.ts_color_light_blue
@Composable
fun ClipboardValueView(
value: String,
title: String? = null,
subtitle: String? = null,
fontFamily: FontFamily = FontFamily.Monospace
) {
val localClipboardManager = LocalClipboardManager.current
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.clip(shape = RoundedCornerShape(8.dp))) {
Row(
horizontalArrangement =
Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
modifier =
Modifier.fillMaxWidth()
.padding(8.dp)
.clickable(onClick = { localClipboardManager.setText(AnnotatedString(value)) }),
verticalAlignment = Alignment.CenterVertically) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth().weight(10f)) {
title?.let { title ->
Text(
title,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold)
}
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
fontFamily = fontFamily,
maxLines = 2,
overflow = TextOverflow.Ellipsis)
subtitle?.let { subtitle ->
Text(
subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary)
}
}
Icon(
painterResource(R.drawable.clipboard),
stringResource(R.string.copy_to_clipboard),
modifier = Modifier.width(24.dp).height(24.dp),
tint = ts_color_light_blue)
}
}
}

@ -3,51 +3,39 @@
package com.tailscale.ipn.ui.view package com.tailscale.ipn.ui.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text 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.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp 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.ui.Links import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.theme.ts_color_light_blue import com.tailscale.ipn.ui.theme.ts_color_light_blue
import com.tailscale.ipn.ui.util.defaultPaddingModifier import com.tailscale.ipn.ui.util.ClipboardValueView
import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.viewModel.BugReportViewModel import com.tailscale.ipn.ui.viewModel.BugReportViewModel
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) { fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
val bugReportID = model.bugReportID.collectAsState().value
Scaffold(topBar = { Header(R.string.bug_report_title, onBack = nav.onBack) }) { innerPadding -> Scaffold(topBar = { Header(R.string.bug_report_title, onBack = nav.onBack) }) { innerPadding ->
Column( Column(
@ -55,15 +43,14 @@ fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel())
ClickableText( ClickableText(
text = contactText(), text = contactText(),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
onClick = { handler.openUri(Links.SUPPORT_URL) }) onClick = { handler.openUri(Links.SUPPORT_URL) })
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
ReportIdRow(bugReportIdFlow = model.bugReportID) ClipboardValueView(bugReportID, title = stringResource(R.string.bug_report_id))
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = R.string.bug_report_id_desc), text = stringResource(id = R.string.bug_report_id_desc),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -74,33 +61,6 @@ fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel())
} }
} }
@Composable
fun ReportIdRow(bugReportIdFlow: StateFlow<String>) {
val localClipboardManager = LocalClipboardManager.current
val bugReportId = bugReportIdFlow.collectAsState()
Row(
modifier =
settingsRowModifier()
.fillMaxWidth()
.clickable(
onClick = { localClipboardManager.setText(AnnotatedString(bugReportId.value)) }),
verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.weight(10f)) {
Text(
text = bugReportId.value,
style = MaterialTheme.typography.titleMedium,
fontFamily = FontFamily.Monospace,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = defaultPaddingModifier())
}
Box(Modifier.weight(1f)) {
Icon(Icons.Outlined.Share, null, modifier = Modifier.width(24.dp).height(24.dp))
}
}
}
@Composable @Composable
fun contactText(): AnnotatedString { fun contactText(): AnnotatedString {
val annotatedString = buildAnnotatedString { val annotatedString = buildAnnotatedString {

@ -80,7 +80,7 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
modifier = modifier =
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer) .background(MaterialTheme.colorScheme.secondaryContainer)
.padding(horizontal = 8.dp) .padding(horizontal = 16.dp)
.padding(top = 10.dp), .padding(top = 10.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false) val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
@ -146,7 +146,7 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
Box( Box(
modifier = modifier =
Modifier.clickable { navAction() } Modifier.clickable { navAction() }
.padding(horizontal = 8.dp) .padding(horizontal = 16.dp)
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.fillMaxWidth()) { .fillMaxWidth()) {

@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -24,11 +22,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp 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.ui.theme.ts_color_light_blue
import com.tailscale.ipn.ui.util.settingsRowModifier import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory
@ -103,7 +103,10 @@ fun AddressRow(address: String, type: String) {
color = MaterialTheme.colorScheme.secondary) color = MaterialTheme.colorScheme.secondary)
} }
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Icon(Icons.Outlined.Share, null, tint = MaterialTheme.colorScheme.secondary) Icon(
painter = painterResource(id = R.drawable.clipboard),
null,
tint = ts_color_light_blue)
} }
} }
} }

@ -0,0 +1,123 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.view
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.theme.ts_color_light_blue
import com.tailscale.ipn.ui.util.ClipboardValueView
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.viewModel.TailnetLockSetupViewModel
import com.tailscale.ipn.ui.viewModel.TailnetLockSetupViewModelFactory
@Composable
fun TailnetLockSetupView(
nav: BackNavigation,
model: TailnetLockSetupViewModel = viewModel(factory = TailnetLockSetupViewModelFactory())
) {
val statusItems = model.statusItems.collectAsState().value
val nodeKey = model.nodeKey.collectAsState().value
val tailnetLockKey = model.tailnetLockKey.collectAsState().value
Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = nav.onBack) }) { innerPadding ->
LoadingIndicator.Wrap {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(innerPadding)) {
item(key = "header") {
ExplainerView()
Spacer(Modifier.size(4.dp))
}
items(items = statusItems) { statusItem ->
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(horizontal = 16.dp)) {
Icon(
painter = painterResource(id = statusItem.icon),
contentDescription = null,
tint = ts_color_light_blue)
Text(stringResource(statusItem.title))
}
}
item(key = "nodeKey") {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)) {
Spacer(Modifier.size(4.dp))
ClipboardValueView(
value = nodeKey,
title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer))
}
}
item(key = "tailnetLockKey") {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)) {
ClipboardValueView(
value = tailnetLockKey,
title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer))
}
}
}
}
}
}
@Composable
private fun ExplainerView() {
val handler = LocalUriHandler.current
ClickableText(
explainerText(),
modifier = Modifier.padding(16.dp),
onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) })
}
@Composable
fun explainerText(): AnnotatedString {
val annotatedString = buildAnnotatedString {
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.tailnet_lock_explainer))
}
pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL)
withStyle(
style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) {
append(stringResource(id = R.string.learn_more))
}
pop()
}
return annotatedString
}

@ -76,6 +76,7 @@ data class Setting(
data class SettingsNav( data class SettingsNav(
val onNavigateToBugReport: () -> Unit, val onNavigateToBugReport: () -> Unit,
val onNavigateToAbout: () -> Unit, val onNavigateToAbout: () -> Unit,
val onNavigateToTailnetLock: () -> Unit,
val onNavigateToMDMSettings: () -> Unit, val onNavigateToMDMSettings: () -> Unit,
val onNavigateToManagedBy: () -> Unit, val onNavigateToManagedBy: () -> Unit,
val onNavigateToUserSwitcher: () -> Unit, val onNavigateToUserSwitcher: () -> Unit,
@ -135,6 +136,12 @@ class SettingsViewModel(val navigation: SettingsNav) : IpnViewModel() {
private fun footerSettings(mdmSettings: MDMSettings): List<Setting> = private fun footerSettings(mdmSettings: MDMSettings): List<Setting> =
listOfNotNull( listOfNotNull(
Setting(
titleRes = R.string.tailnet_lock,
SettingType.NAV,
onClick = { navigation.onNavigateToTailnetLock() },
enabled = MutableStateFlow(true)
),
Setting( Setting(
titleRes = R.string.about, titleRes = R.string.about,
SettingType.NAV, SettingType.NAV,

@ -0,0 +1,75 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.viewModel
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.IpnState
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class TailnetLockSetupViewModelFactory() : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TailnetLockSetupViewModel() as T
}
}
data class StatusItem(@StringRes val title: Int, @DrawableRes val icon: Int)
class TailnetLockSetupViewModel() : IpnViewModel() {
val statusItems: StateFlow<List<StatusItem>> = MutableStateFlow(emptyList())
val nodeKey: StateFlow<String> = MutableStateFlow("unknown")
val tailnetLockKey: StateFlow<String> = MutableStateFlow("unknown")
init {
LoadingIndicator.start()
Client(viewModelScope).tailnetLockStatus { result ->
statusItems.set(generateStatusItems(result.getOrNull()))
nodeKey.set(result.getOrNull()?.NodeKey ?: "unknown")
tailnetLockKey.set(result.getOrNull()?.PublicKey ?: "unknown")
LoadingIndicator.stop()
}
}
fun generateStatusItems(networkLockStatus: IpnState.NetworkLockStatus?): List<StatusItem> {
networkLockStatus?.let { status ->
val items = emptyList<StatusItem>().toMutableList()
if (status.Enabled == true) {
items.add(StatusItem(title = R.string.tailnet_lock_enabled, icon = R.drawable.check_circle))
} else {
items.add(
StatusItem(title = R.string.tailnet_lock_disabled, icon = R.drawable.xmark_circle))
}
if (status.NodeKeySigned == true) {
items.add(
StatusItem(title = R.string.this_node_has_been_signed, icon = R.drawable.check_circle))
} else {
items.add(
StatusItem(
title = R.string.this_node_has_not_been_signed, icon = R.drawable.xmark_circle))
}
if (status.IsPublicKeyTrusted()) {
items.add(StatusItem(title = R.string.this_node_is_trusted, icon = R.drawable.check_circle))
} else {
items.add(
StatusItem(title = R.string.this_node_is_not_trusted, icon = R.drawable.xmark_circle))
}
return items
}
?: run {
return emptyList()
}
}
}

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M424,552L338,466Q327,455 310,455Q293,455 282,466Q271,477 271,494Q271,511 282,522L396,636Q408,648 424,648Q440,648 452,636L678,410Q689,399 689,382Q689,365 678,354Q667,343 650,343Q633,343 622,354L424,552ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M360,720Q327,720 303.5,696.5Q280,673 280,640L280,160Q280,127 303.5,103.5Q327,80 360,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,640Q800,673 776.5,696.5Q753,720 720,720L360,720ZM360,640L720,640Q720,640 720,640Q720,640 720,640L720,160Q720,160 720,160Q720,160 720,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,280Q120,263 131.5,251.5Q143,240 160,240Q177,240 188.5,251.5Q200,263 200,280L200,800Q200,800 200,800Q200,800 200,800L600,800Q617,800 628.5,811.5Q640,823 640,840Q640,857 628.5,868.5Q617,880 600,880L200,880ZM360,640Q360,640 360,640Q360,640 360,640L360,160Q360,160 360,160Q360,160 360,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640L360,640Z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,536L596,652Q607,663 624,663Q641,663 652,652Q663,641 663,624Q663,607 652,596L536,480L652,364Q663,353 663,336Q663,319 652,308Q641,297 624,297Q607,297 596,308L480,424L364,308Q353,297 336,297Q319,297 308,308Q297,319 297,336Q297,353 308,364L424,480L308,596Q297,607 297,624Q297,641 308,652Q319,663 336,663Q353,663 364,652L480,536ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

@ -30,7 +30,7 @@
<string name="bug_report_title">Report a Bug</string> <string name="bug_report_title">Report a Bug</string>
<string name="bug_report_instructions_prefix">To report a bug,&#160;</string> <string name="bug_report_instructions_prefix">To report a bug,&#160;</string>
<string name="bug_report_instructions_linktext">contact our support team&#160;</string> <string name="bug_report_instructions_linktext">contact our support team&#160;</string>
<string name="bug_report_instructions_suffix"> and include the ID below.</string> <string name="bug_report_instructions_suffix"> and include the identifier below.</string>
<string name="bug_report_id_desc">This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information.</string> <string name="bug_report_id_desc">This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information.</string>
<!-- Strings for the settings screen --> <!-- Strings for the settings screen -->
@ -63,7 +63,7 @@
<string name="open_support">Open Support</string> <string name="open_support">Open Support</string>
<!-- State strings --> <!-- State strings -->
<string name="waiting">Waiting…</string> <string name="waiting">Loading…</string>
<string name="placeholder">--</string> <string name="placeholder">--</string>
<string name="please_login">Please Login</string> <string name="please_login">Please Login</string>
<string name="stopped">Stopped</string> <string name="stopped">Stopped</string>
@ -100,5 +100,20 @@
<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="tailnet_lock">Tailnet lock</string>
<string name="tailnet_lock_explainer">Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. </string>
<string name="tailnet_lock_enabled">Tailnet lock is currently enabled.</string>
<string name="tailnet_lock_disabled">Tailnet lock is currently not enabled.</string>
<string name="this_node_has_been_signed">This node has been signed by another device.</string>
<string name="this_node_has_not_been_signed">This node has not been signed by another device.</string>
<string name="this_node_is_trusted">This node is trusted to change the Tailnet lock configuration.</string>
<string name="this_node_is_not_trusted">This node is not trusted to change the Tailnet lock configuration.</string>
<string name="copy_to_clipboard">Copy to Clipboard</string>
<string name="node_key">Node Key</string>
<string name="tailnet_lock_key">Tailnet Lock Key</string>
<string name="node_key_explainer">Used to sign this node from another signing device in your tailnet.</string>
<string name="tailnet_lock_key_explainer">Used to authorize changes to the Tailnet lock configuration.</string>
<string name="bug_report_id">Bug Report ID</string>
<string name="learn_more">Learn more…</string>
</resources> </resources>

Loading…
Cancel
Save