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 8 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.RunExitNodeView
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.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.IpnViewModel
@ -79,6 +80,7 @@ class MainActivity : ComponentActivity() {
SettingsNav(
onNavigateToBugReport = { navController.navigate("bugReport") },
onNavigateToAbout = { navController.navigate("about") },
onNavigateToTailnetLock = { navController.navigate("tailnetLock") },
onNavigateToMDMSettings = { navController.navigate("mdmSettings") },
onNavigateToManagedBy = { navController.navigate("managedBy") },
onNavigateToUserSwitcher = { navController.navigate("userSwitcher") },
@ -116,6 +118,7 @@ class MainActivity : ComponentActivity() {
PeerDetails(nav = backNav, it.arguments?.getString("nodeId") ?: "")
}
composable("bugReport") { BugReportView(nav = backNav) }
composable("tailnetLock") { TailnetLockSetupView(nav = backNav) }
composable("about") { AboutView(nav = backNav) }
composable("mdmSettings") { MDMSettingsDebugView(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.IpnState
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.Dispatchers
import kotlinx.coroutines.launch
@ -21,6 +18,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.serializer
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
private object Endpoint {
const val DEBUG = "debug"
@ -47,6 +47,8 @@ private object Endpoint {
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
typealias TailnetLockStatusResponseHandler = (Result<IpnState.NetworkLockStatus>) -> Unit
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
@ -107,6 +109,10 @@ class Client(private val scope: CoroutineScope) {
return post(Endpoint.LOGOUT, responseHandler = responseHandler)
}
fun tailnetLockStatus(responseHandler: TailnetLockStatusResponseHandler) {
get(Endpoint.TKA_STATUS, responseHandler = responseHandler)
}
private inline fun <reified T> get(
path: String,
body: ByteArray? = null,

@ -81,13 +81,19 @@ class IpnState {
@Serializable
data class NetworkLockStatus(
var Enabled: Boolean,
var PublicKey: String,
var NodeKey: String,
var NodeKeySigned: Boolean,
var Enabled: Boolean? = null,
var PublicKey: String? = null,
var NodeKey: String? = null,
var NodeKeySigned: Boolean? = null,
var FilteredPeers: List<TKAFilteredPeer>? = null,
var StateID: ULong? = null,
)
var TrustedKeys: List<TKAKey>? = null
) {
fun IsPublicKeyTrusted(): Boolean {
return TrustedKeys?.any { it.Key == PublicKey } == true
}
}
@Serializable
data class TKAFilteredPeer(
@ -96,6 +102,8 @@ class IpnState {
var NodeKey: String,
)
@Serializable data class TKAKey(var Key: String)
@Serializable
data class PingResult(
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
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.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.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
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.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
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.defaultPaddingModifier
import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.util.ClipboardValueView
import com.tailscale.ipn.ui.viewModel.BugReportViewModel
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) {
val handler = LocalUriHandler.current
val bugReportID = model.bugReportID.collectAsState().value
Scaffold(topBar = { Header(R.string.bug_report_title, onBack = nav.onBack) }) { innerPadding ->
Column(
@ -55,15 +43,14 @@ fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel())
ClickableText(
text = contactText(),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
onClick = { handler.openUri(Links.SUPPORT_URL) })
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))
Text(
text = stringResource(id = R.string.bug_report_id_desc),
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
fun contactText(): AnnotatedString {
val annotatedString = buildAnnotatedString {

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

@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.MaterialTheme
import androidx.compose.material3.Scaffold
@ -24,11 +22,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
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.viewModel.PeerDetailsViewModel
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory
@ -103,7 +103,10 @@ fun AddressRow(address: String, type: String) {
color = MaterialTheme.colorScheme.secondary)
}
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(
val onNavigateToBugReport: () -> Unit,
val onNavigateToAbout: () -> Unit,
val onNavigateToTailnetLock: () -> Unit,
val onNavigateToMDMSettings: () -> Unit,
val onNavigateToManagedBy: () -> Unit,
val onNavigateToUserSwitcher: () -> Unit,
@ -135,6 +136,12 @@ class SettingsViewModel(val navigation: SettingsNav) : IpnViewModel() {
private fun footerSettings(mdmSettings: MDMSettings): List<Setting> =
listOfNotNull(
Setting(
titleRes = R.string.tailnet_lock,
SettingType.NAV,
onClick = { navigation.onNavigateToTailnetLock() },
enabled = MutableStateFlow(true)
),
Setting(
titleRes = R.string.about,
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_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_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>
<!-- Strings for the settings screen -->
@ -63,7 +63,7 @@
<string name="open_support">Open Support</string>
<!-- State strings -->
<string name="waiting">Waiting…</string>
<string name="waiting">Loading…</string>
<string name="placeholder">--</string>
<string name="please_login">Please Login</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="enabled">Enabled</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>

Loading…
Cancel
Save