Skip to content

Commit

Permalink
android: implement the bug reporting and about screen and localize
Browse files Browse the repository at this point in the history
updates tailscale/corp#18202
fixes ENG-2876

Adds the bug reporting view.  Functional, but not properly styled.

Moves the various link URLs to a constants file and corrects link-opening in both but reporting and the settings screen.

Adds an AboutView with app icon and same content as the iOS version.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
  • Loading branch information
agottardo authored and barnstar committed Mar 13, 2024
1 parent 0d867ae commit 4579ccd
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 106 deletions.
3 changes: 2 additions & 1 deletion android/src/main/java/com/tailscale/ipn/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.tailscale.ipn.ui.view.MainViewNavigation
import com.tailscale.ipn.ui.view.PeerDetails
import com.tailscale.ipn.ui.view.Settings
import com.tailscale.ipn.ui.view.SettingsNav
import com.tailscale.ipn.ui.viewModel.BugReportViewModel
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel
import com.tailscale.ipn.ui.viewModel.MainViewModel
import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel
Expand Down Expand Up @@ -72,7 +73,7 @@ class MainActivity : ComponentActivity() {
?: ""))
}
composable("bugReport") {
BugReportView()
BugReportView(BugReportViewModel(manager.apiClient))
}
composable("about") {
AboutView()
Expand Down
26 changes: 26 additions & 0 deletions android/src/main/java/com/tailscale/ipn/ui/Links.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package com.tailscale.ipn.ui

object Links {
const val DEFAULT_CONTROL_URL = "https://controlplane.tailscale.com"
const val SERVER_URL = "https://login.tailscale.com"
const val ADMIN_URL = SERVER_URL + "/admin"
const val SIGNIN_URL = "https://tailscale.com/login"
const val PRIVACY_POLICY_URL = "https://tailscale.com/privacy-policy/"
const val TERMS_URL = "https://tailscale.com/terms"
const val DOCS_URL = "https://tailscale.com/kb/"
const val START_GUIDE_URL = "https://tailscale.com/kb/1017/install/"
const val LICENSES_URL = "https://tailscale.com/licenses/android"
const val DELETE_ACCOUNT_URL = "https://login.tailscale.com/login?next_url=%2Fadmin%2Fsettings%2Fgeneral"
const val TAILNET_LOCK_KB_URL = "https://tailscale.com/kb/1226/tailnet-lock/"
const val KEY_EXPIRY_KB_URL = "https://tailscale.com/kb/1028/key-expiry/"
const val INSTALL_TAILSCALE_KB_URL = "https://tailscale.com/kb/installation/"
const val INSTALL_UNSTABLE_KB_URL = "https://tailscale.com/kb/1083/install-unstable"
const val MAGICDNS_KB_URL = "https://tailscale.com/kb/1081/magicdns"
const val TROUBLESHOOTING_KB_URL = "https://tailscale.com/kb/1023/troubleshooting"
const val SUPPORT_URL = "https://tailscale.com/contact/support#support-form"
const val TAILDROP_KB_URL = "https://tailscale.com/kb/1106/taildrop"
const val TAILFS_KB_URL = "https://tailscale.com/kb/1106/taildrop"
}
12 changes: 7 additions & 5 deletions android/src/main/java/com/tailscale/ipn/ui/service/IpnManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob


typealias PrefChangeCallback = (Result<Boolean>) -> Unit

// Abstracts the actions that can be taken by the UI so that the concept of an IPNManager
Expand All @@ -25,7 +24,6 @@ data class IpnActions(
val stopVPN: () -> Unit,
val login: () -> Unit,
val logout: () -> Unit,
val openAdminConsole: () -> Unit,
val updatePrefs: (Ipn.MaskedPrefs, PrefChangeCallback) -> Unit
)

Expand All @@ -42,7 +40,6 @@ class IpnManager {
stopVPN = { stopVPN() },
login = { apiClient.startLoginInteractive() },
logout = { apiClient.logout() },
openAdminConsole = { /* TODO */ },
updatePrefs = { prefs, callback -> updatePrefs(prefs, callback) }
)

Expand All @@ -62,7 +59,12 @@ class IpnManager {
}

fun updatePrefs(prefs: Ipn.MaskedPrefs, callback: PrefChangeCallback) {
// (jonathan) TODO: Implement this in localAPI
//apiClient.updatePrefs(prefs)
apiClient.editPrefs(prefs) { result ->
result.success?.let {
callback(Result.success(true))
} ?: run {
callback(Result.failure(Throwable(result.error)))
}
}
}
}
119 changes: 119 additions & 0 deletions android/src/main/java/com/tailscale/ipn/ui/view/AboutView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package com.tailscale.ipn.ui.view

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.safeContentPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
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.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.BuildConfig
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links

@Composable
fun AboutView() {
Surface(color = MaterialTheme.colorScheme.surface) {
Column(
verticalArrangement = Arrangement.spacedBy(
space = 20.dp, alignment = Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.safeContentPadding()
) {
Image(
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(RoundedCornerShape(50))
.background(Color.Black)
.padding(15.dp),
painter = painterResource(id = R.drawable.ic_tile),
contentDescription = stringResource(R.string.app_icon_content_description)
)
Column(
verticalArrangement = Arrangement.spacedBy(
space = 2.dp, alignment = Alignment.CenterVertically
), horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(R.string.about_view_title),
fontWeight = FontWeight.SemiBold,
fontSize = MaterialTheme.typography.titleLarge.fontSize,
color = MaterialTheme.colorScheme.primary
)
Text(
text = BuildConfig.VERSION_NAME,
fontWeight = MaterialTheme.typography.bodyMedium.fontWeight,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
color = MaterialTheme.colorScheme.secondary
)
}
Column(
verticalArrangement = Arrangement.spacedBy(
space = 4.dp, alignment = Alignment.CenterVertically
), horizontalAlignment = Alignment.CenterHorizontally
) {
OpenURLButton(
stringResource(R.string.acknowledgements), Links.LICENSES_URL
)
OpenURLButton(
stringResource(R.string.privacy_policy), Links.PRIVACY_POLICY_URL
)
OpenURLButton(
stringResource(R.string.terms_of_service), Links.TERMS_URL
)
}

Text(
stringResource(R.string.about_view_footnotes),
fontWeight = FontWeight.Normal,
fontSize = MaterialTheme.typography.labelMedium.fontSize,
color = MaterialTheme.colorScheme.tertiary,
textAlign = TextAlign.Center
)
}
}
}

@Composable
fun OpenURLButton(title: String, url: String) {
val handler = LocalUriHandler.current

Button(
onClick = { handler.openUri(url) },
content = {
Text(title)
},
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.secondary,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
)
}
113 changes: 113 additions & 0 deletions android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

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.fillMaxWidth
import androidx.compose.foundation.layout.height
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.Surface
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.graphics.Color
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.util.defaultPaddingModifier
import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.viewModel.BugReportViewModel
import kotlinx.coroutines.flow.StateFlow


@Composable
fun BugReportView(viewModel: BugReportViewModel) {
val handler = LocalUriHandler.current

Surface(color = MaterialTheme.colorScheme.surface) {
Column(modifier = defaultPaddingModifier().fillMaxWidth()) {
Text(text = stringResource(id = R.string.bug_report_title),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium)

Spacer(modifier = Modifier.height(8.dp))

ClickableText(text = contactText(),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
onClick = {
handler.openUri(Links.SUPPORT_URL)
})

Spacer(modifier = Modifier.height(8.dp))

ReportIdRow(bugReportIdFlow = viewModel.bugReportID)

Spacer(modifier = Modifier.height(8.dp))

Text(text = stringResource(id = R.string.bug_report_id_desc),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left,
style = MaterialTheme.typography.bodySmall)
}
}
}

@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, maxLines = 1, 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 {
append(stringResource(id = R.string.bug_report_instructions_prefix))

pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL)
withStyle(style = SpanStyle(color = Color.Blue)) {
append(stringResource(id = R.string.bug_report_instructions_linktext))
}
pop()

append(stringResource(id = R.string.bug_report_instructions_suffix))
}
return annotatedString
}

21 changes: 11 additions & 10 deletions android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.Tailcfg
Expand Down Expand Up @@ -82,7 +84,7 @@ fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) {

// (jonathan) TODO: Show the selected exit node name here.
if (state.value == Ipn.State.Running) {
ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, "None")
ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, stringResource(id = R.string.none))
}

when (state.value) {
Expand All @@ -105,15 +107,15 @@ fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) {
}

@Composable
fun ExitNodeStatus(navAction: () -> Unit, exitNode: String = "None") {
fun ExitNodeStatus(navAction: () -> Unit, exitNode: String = stringResource(id = R.string.none)) {
Box(modifier = Modifier
.clickable { navAction() }
.padding(horizontal = 8.dp)
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()) {
Column(modifier = Modifier.padding(6.dp)) {
Text(text = "Exit Node", style = MaterialTheme.typography.titleMedium)
Text(text = stringResource(id = R.string.exit_node), style = MaterialTheme.typography.titleMedium)
Row {
Text(text = exitNode, style = MaterialTheme.typography.bodyMedium)
Icon(
Expand Down Expand Up @@ -159,7 +161,7 @@ fun StartingView() {
.background(MaterialTheme.colorScheme.secondaryContainer),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { Text(text = "Starting...", style = MaterialTheme.typography.titleMedium) }
) { Text(text = stringResource(id = R.string.starting), style = MaterialTheme.typography.titleMedium) }
}

@Composable
Expand All @@ -172,16 +174,15 @@ fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAc
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Not Connected", style = MaterialTheme.typography.titleMedium)
Text(text = stringResource(id = R.string.not_connected), style = MaterialTheme.typography.titleMedium)
if (user != null && !user.isEmpty()) {
val tailnetName = user.NetworkProfile?.DomainName ?: ""
Text(
"Connect to your ${tailnetName} tailnet",
Text(stringResource(id = R.string.connect_to_tailnet, tailnetName),
style = MaterialTheme.typography.bodyMedium
)
Button(onClick = connectAction) { Text(text = "Connect") }
Button(onClick = connectAction) { Text(text = stringResource(id = R.string.connect)) }
} else {
Button(onClick = loginAction) { Text(text = "Log In") }
Button(onClick = loginAction) { Text(text = stringResource(id = R.string.log_in)) }
}
}
}
Expand Down Expand Up @@ -216,7 +217,7 @@ fun PeerList(searchTerm: StateFlow<String>, peers: StateFlow<List<PeerSet>>, onN
peerList.value.forEach { peerSet ->
ListItem(headlineContent = {
Text(text = peerSet.user?.DisplayName
?: "Unknown User", style = MaterialTheme.typography.titleLarge)
?: stringResource(id = R.string.unknown_user), style = MaterialTheme.typography.titleLarge)
})
peerSet.peers.forEach { peer ->
ListItem(
Expand Down
Loading

0 comments on commit 4579ccd

Please sign in to comment.