diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 2e0dc6a..dcf98e5 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -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) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt index e8a87e0..8befc74 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt @@ -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) -> Unit +typealias TailnetLockStatusResponseHandler = (Result) -> Unit + typealias BugReportIdHandler = (Result) -> Unit typealias PrefsHandler = (Result) -> 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 get( path: String, body: ByteArray? = null, diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt index cbbe7ad..cb3c81a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/IpnState.kt @@ -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? = null, var StateID: ULong? = null, - ) + var TrustedKeys: List? = 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, diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt new file mode 100644 index 0000000..9b327b1 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt @@ -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) + } + } +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt index 3b5ecad..7a2adf3 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt @@ -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) { - 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 { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 699937f..979c0f0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -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()) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt index c601905..cc589c8 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt @@ -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) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt new file mode 100644 index 0000000..1adb737 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -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 +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index 77f85b2..aaf71db 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -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 = listOfNotNull( + Setting( + titleRes = R.string.tailnet_lock, + SettingType.NAV, + onClick = { navigation.onNavigateToTailnetLock() }, + enabled = MutableStateFlow(true) + ), Setting( titleRes = R.string.about, SettingType.NAV, diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/TailnetLockSetupViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/TailnetLockSetupViewModel.kt new file mode 100644 index 0000000..f439efc --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/TailnetLockSetupViewModel.kt @@ -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 create(modelClass: Class): T { + return TailnetLockSetupViewModel() as T + } +} + +data class StatusItem(@StringRes val title: Int, @DrawableRes val icon: Int) + +class TailnetLockSetupViewModel() : IpnViewModel() { + + val statusItems: StateFlow> = MutableStateFlow(emptyList()) + val nodeKey: StateFlow = MutableStateFlow("unknown") + val tailnetLockKey: StateFlow = 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 { + networkLockStatus?.let { status -> + val items = emptyList().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() + } + } +} diff --git a/android/src/main/res/drawable/check_circle.xml b/android/src/main/res/drawable/check_circle.xml new file mode 100644 index 0000000..a426909 --- /dev/null +++ b/android/src/main/res/drawable/check_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/src/main/res/drawable/clipboard.xml b/android/src/main/res/drawable/clipboard.xml new file mode 100644 index 0000000..4fe3235 --- /dev/null +++ b/android/src/main/res/drawable/clipboard.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/src/main/res/drawable/xmark_circle.xml b/android/src/main/res/drawable/xmark_circle.xml new file mode 100644 index 0000000..8d26e4f --- /dev/null +++ b/android/src/main/res/drawable/xmark_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 36ec421..d60590a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ Report a Bug To report a bug,  contact our support team  - and include the ID below. + and include the identifier below. This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information. @@ -63,7 +63,7 @@ Open Support - Waiting… + Loading… -- Please Login Stopped @@ -100,5 +100,20 @@ 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. Enabled Disabled + Tailnet lock + Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. + Tailnet lock is currently enabled. + Tailnet lock is currently not enabled. + This node has been signed by another device. + This node has not been signed by another device. + This node is trusted to change the Tailnet lock configuration. + This node is not trusted to change the Tailnet lock configuration. + Copy to Clipboard + Node Key + Tailnet Lock Key + Used to sign this node from another signing device in your tailnet. + Used to authorize changes to the Tailnet lock configuration. + Bug Report ID + Learn more…