android: implement the bug reporting and about screen and localize (#198)
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> Co-authored-by: Andrea Gottardo <andrea@tailscale.com>pull/200/head
parent
0d867aedce
commit
94a4f55eb2
@ -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"
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.viewModel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.tailscale.ipn.ui.localapi.LocalApiClient
|
||||||
|
import com.tailscale.ipn.ui.model.BugReportID
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
class BugReportViewModel(localAPI: LocalApiClient) : ViewModel() {
|
||||||
|
private var _bugReportID: MutableStateFlow<BugReportID> = MutableStateFlow("")
|
||||||
|
var bugReportID: StateFlow<String> = _bugReportID
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
localAPI.getBugReportId {
|
||||||
|
when (it.successful) {
|
||||||
|
true -> _bugReportID.value = it.success ?: "(Error fetching ID)"
|
||||||
|
false -> _bugReportID.value = "(Error fetching ID)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,49 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
|
<!-- Generic Strings -->
|
||||||
|
<string name="log_in">Log In</string>
|
||||||
|
<string name="log_out">Log Out</string>
|
||||||
|
<string name="none">None</string>
|
||||||
|
<string name="connect">Connect</string>
|
||||||
|
<string name="unknown_user">Unknown User</string>
|
||||||
|
<string name="connected">Connected</string>
|
||||||
|
<string name="not_connected">Not Connected</string>
|
||||||
|
|
||||||
|
<!-- Strings for the about screen -->
|
||||||
<string name="app_name">Tailscale</string>
|
<string name="app_name">Tailscale</string>
|
||||||
<string name="tile_name">Tailscale</string>
|
<string name="tile_name">Tailscale</string>
|
||||||
|
<string name="about_view_title">Tailscale for Android</string>
|
||||||
|
<string name="acknowledgements">Acknowledgements</string>
|
||||||
|
<string name="privacy_policy">Privacy Policy</string>
|
||||||
|
<string name="terms_of_service">Terms of Service</string>
|
||||||
|
<string name="about_view_footnotes">WireGuard is a registered trademark of Jason A. Donenfeld.\n\n© 2024 Tailscale Inc. All rights reserved.\nTailscale is a registered trademark of Tailscale Inc.</string>
|
||||||
|
<string name="app_icon_content_description">The Tailscale App Icon</string>
|
||||||
|
|
||||||
|
<!-- Strings for the bug reporting screen -->
|
||||||
|
<string name="bug_report_title">Report a Bug</string>
|
||||||
|
<string name="bug_report_instructions_prefix">To report a bug, </string>
|
||||||
|
<string name="bug_report_instructions_linktext">contact our support team  </string>
|
||||||
|
<string name="bug_report_instructions_suffix">and include the ID below.</string>
|
||||||
|
<string name="bug_report_id_desc">This ID helps us find the event ino our diagnostic logs. This process does not share any of your personally-identifiable information.</string>
|
||||||
|
|
||||||
|
<!-- Strings for the settings screen -->
|
||||||
|
<string name="settings_title">Settings</string>
|
||||||
|
<string name="settings_admin_prefix">You can manage your account from the admin console. </string>
|
||||||
|
<string name="settings_admin_link">View admin console...</string>
|
||||||
|
<string name="about">About</string>
|
||||||
|
<string name="bug_report">Bug Report</string>
|
||||||
|
<string name="use_ts_dns">Use Tailscale DNS</string>
|
||||||
|
|
||||||
|
<!-- Strings for the main scren -->
|
||||||
|
<string name="exit_node">Exit Node</string>
|
||||||
|
<string name="starting">Staring...</string>
|
||||||
|
<string name="connect_to_tailnet">"Connect to your %1$s tailnet"</string>
|
||||||
|
|
||||||
|
<!-- Strings for peer details -->
|
||||||
|
<string name="addresses_section">TAILSCALE ADDRESSES</string>
|
||||||
|
<string name="os">OS</string>
|
||||||
|
<string name="key_expiry">Key Expiry</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue