diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 434f2a600a..ecc0ec1eec 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -52,7 +52,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @@ -174,8 +173,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns) initViewModels() applicationScope.launch { - val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager - MDMSettings.update(get(), rm) + val restrictionsManager = + getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + MDMSettings.update(get(), restrictionsManager) } applicationScope.launch { Notifier.state.collect { _ -> @@ -208,9 +208,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { } } } - applicationScope.launch { - val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() - } TSLog.init(this) FeatureFlags.initialize(mapOf("enable_new_search" to true)) } diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 7b7181e0d2..a338dc8011 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -337,7 +337,7 @@ class MainActivity : ComponentActivity() { loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel, - appViewModel = appViewModel) + ) } composable("search") { val autoFocus = viewModel.autoFocusSearch @@ -473,17 +473,13 @@ class MainActivity : ComponentActivity() { if (this::navController.isInitialized) { val previousEntry = navController.previousBackStackEntry TSLog.d("MainActivity", "onNewIntent: previousBackStackEntry = $previousEntry") - if (this::navController.isInitialized) { - val previousEntry = navController.previousBackStackEntry - TSLog.d("MainActivity", "onNewIntent: previousBackStackEntry = $previousEntry") - if (previousEntry != null) { - navController.popBackStack(route = "main", inclusive = false) - } else { - TSLog.e( - "MainActivity", - "onNewIntent: No previous back stack entry, navigating directly to 'main'") - navController.navigate("main") { popUpTo("main") { inclusive = true } } - } + if (previousEntry != null) { + navController.popBackStack(route = "main", inclusive = false) + } else { + TSLog.e( + "MainActivity", + "onNewIntent: No previous back stack entry, navigating directly to 'main'") + navController.navigate("main") { popUpTo("main") { inclusive = true } } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt index 71415c0c3b..8cbc393e80 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt @@ -4,6 +4,7 @@ package com.tailscale.ipn.ui.util import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -95,7 +95,10 @@ object Lists { headlineContent = { Box(modifier = Modifier.padding(vertical = 4.dp)) { onClick?.let { - ClickableText(text = text as AnnotatedString, style = style, onClick = { onClick() }) + Text( + text = text as AnnotatedString, + style = style, + modifier = Modifier.clickable { onClick() }) } ?: run { Text(text as String, style = style) } } }) 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 fce52b476c..5d452a071c 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 @@ -8,17 +8,17 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll 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.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration @@ -36,7 +36,6 @@ import com.tailscale.ipn.ui.viewModel.BugReportViewModel @Composable fun BugReportView(backToSettings: BackNavigation, model: BugReportViewModel = viewModel()) { - val handler = LocalUriHandler.current val bugReportID by model.bugReportID.collectAsState() Scaffold(topBar = { Header(R.string.bug_report_title, onBack = backToSettings) }) { innerPadding @@ -48,10 +47,7 @@ fun BugReportView(backToSettings: BackNavigation, model: BugReportViewModel = vi .fillMaxHeight() .verticalScroll(rememberScrollState())) { Lists.MultilineDescription { - ClickableText( - text = contactText(), - style = MaterialTheme.typography.bodyMedium, - onClick = { handler.openUri(Links.SUPPORT_URL) }) + Text(text = contactText(), style = MaterialTheme.typography.bodyMedium) } ClipboardValueView(bugReportID, title = stringResource(R.string.bug_report_id)) @@ -68,7 +64,7 @@ fun contactText(): AnnotatedString { append(stringResource(id = R.string.bug_report_instructions_prefix)) } - pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL) + pushLink(LinkAnnotation.Url(Links.SUPPORT_URL)) withStyle( style = SpanStyle( 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 d2701e46a8..4a1161ece2 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 @@ -129,7 +129,6 @@ fun MainView( loginAtUrl: (String) -> Unit, navigation: MainViewNavigation, viewModel: MainViewModel, - appViewModel: AppViewModel ) { val currentPingDevice by viewModel.pingViewModel.peer.collectAsState() val healthIcon by viewModel.healthIcon.collectAsState() @@ -829,5 +828,5 @@ fun MainViewPreview() { onNavigateToHealth = {}, onNavigateToSearch = {}), vm, - appViewModel) + ) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SearchView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SearchView.kt index 3cc69fbc0b..b5e7897e32 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SearchView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SearchView.kt @@ -23,7 +23,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -31,6 +31,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -99,8 +100,8 @@ fun SearchView( DisposableEffect(Unit) { val dispatcher = context.onBackInvokedDispatcher - dispatcher?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, callback) - onDispose { dispatcher?.unregisterOnBackInvokedCallback(callback) } + dispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, callback) + onDispose { dispatcher.unregisterOnBackInvokedCallback(callback) } } LaunchedEffect(searchTerm, filteredPeers) { @@ -123,113 +124,121 @@ fun SearchView( Column(modifier = Modifier.fillMaxWidth().focusRequester(focusRequester)) { SearchBar( modifier = Modifier.fillMaxWidth(), - query = searchTerm, - onQueryChange = { newQuery -> - // Create a new TextFieldValue with updated text and set cursor to the end. - searchFieldValue = TextFieldValue(newQuery, selection = TextRange(newQuery.length)) - viewModel.updateSearchTerm(newQuery) - expanded = true - }, - onSearch = { newQuery -> - searchFieldValue = TextFieldValue(newQuery, selection = TextRange(newQuery.length)) - viewModel.updateSearchTerm(newQuery) - focusManager.clearFocus() - keyboardController?.hide() - }, - placeholder = { Text(text = stringResource(R.string.search)) }, - leadingIcon = { - IconButton( - onClick = { + inputField = { + SearchBarDefaults.InputField( + query = searchTerm, + onQueryChange = { newQuery -> + // Create a new TextFieldValue with updated text and set cursor to the end. + searchFieldValue = + TextFieldValue(newQuery, selection = TextRange(newQuery.length)) + viewModel.updateSearchTerm(newQuery) + expanded = true + }, + onSearch = { newQuery -> + searchFieldValue = + TextFieldValue(newQuery, selection = TextRange(newQuery.length)) + viewModel.updateSearchTerm(newQuery) focusManager.clearFocus() - onNavigateBack() - viewModel.updateSearchTerm("") - }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = stringResource(R.string.search), - tint = MaterialTheme.colorScheme.onSurfaceVariant) - } - }, - trailingIcon = { - if (searchTerm.isNotEmpty()) { - IconButton( - onClick = { - searchFieldValue = TextFieldValue("", selection = TextRange(0)) - viewModel.updateSearchTerm("") - focusManager.clearFocus() - keyboardController?.hide() - }) { - Icon( - Icons.Default.Clear, - contentDescription = stringResource(R.string.clear_search)) + keyboardController?.hide() + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text(text = stringResource(R.string.search)) }, + leadingIcon = { + IconButton( + onClick = { + focusManager.clearFocus() + onNavigateBack() + viewModel.updateSearchTerm("") + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + }, + trailingIcon = { + if (searchTerm.isNotEmpty()) { + IconButton( + onClick = { + searchFieldValue = TextFieldValue("", selection = TextRange(0)) + viewModel.updateSearchTerm("") + focusManager.clearFocus() + keyboardController?.hide() + }) { + Icon( + Icons.Default.Clear, + contentDescription = stringResource(R.string.clear_search)) + } } - } + }, + ) }, - active = expanded, - onActiveChange = { expanded = it }, - content = { - LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { - if (filteredPeers.isEmpty()) { - // When there are no filtered peers, show a "No results" message. - item { - Box(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - Lists.LargeTitle( - stringResource(id = R.string.no_results), - bottomPadding = 8.dp, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Light, - backgroundColor = noResultsBackground, - fontColor = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } else { - var firstGroup = true - filteredPeers.forEach { peerSet -> - if (!firstGroup) { - item { Lists.ItemDivider() } - } - firstGroup = false + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { + if (filteredPeers.isEmpty()) { + // When there are no filtered peers, show a "No results" message. + item { + Box(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Lists.LargeTitle( + stringResource(id = R.string.no_results), + bottomPadding = 8.dp, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light, + backgroundColor = noResultsBackground, + fontColor = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } else { + var firstGroup = true + filteredPeers.forEach { peerSet -> + if (!firstGroup) { + item { Lists.ItemDivider() } + } + firstGroup = false - val userName = peerSet.user?.DisplayName ?: "Unknown User" - peerSet.peers.forEachIndexed { index, peer -> - if (index > 0) { - item(key = "divider_${peer.StableID}") { Lists.ItemDivider() } - } - item(key = "peer_${peer.StableID}") { - ListItem( - colors = MaterialTheme.colorScheme.listItem, - headlineContent = { - Column { - Row(verticalAlignment = Alignment.CenterVertically) { - val onlineColor = peer.connectedColor(netmap) - Box( - modifier = - Modifier.size(10.dp) - .background(onlineColor, RoundedCornerShape(50))) - Spacer(modifier = Modifier.size(8.dp)) - Text(peer.displayName ?: "Unknown Device") - } - } - }, - supportingContent = { - Column { - Text(userName) - Text(peer.Addresses?.firstOrNull()?.split("/")?.first() ?: "No IP") - } - }, - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = 4.dp, vertical = 0.dp) - .clickable { - viewModel.disableSearchAutoFocus() - navController.navigate("peerDetails/${peer.StableID}") - }) - } - } + val userName = peerSet.user?.DisplayName ?: "Unknown User" + peerSet.peers.forEachIndexed { index, peer -> + if (index > 0) { + item(key = "divider_${peer.StableID}") { Lists.ItemDivider() } + } + item(key = "peer_${peer.StableID}") { + ListItem( + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + val onlineColor = peer.connectedColor(netmap) + Box( + modifier = + Modifier.size(10.dp) + .background(onlineColor, RoundedCornerShape(50))) + Spacer(modifier = Modifier.size(8.dp)) + Text(peer.displayName) + } + } + }, + supportingContent = { + Column { + Text(userName) + Text(peer.Addresses?.firstOrNull()?.split("/")?.first() ?: "No IP") + } + }, + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 0.dp) + .clickable { + viewModel.disableSearchAutoFocus() + navController.navigate("peerDetails/${peer.StableID}") + }) } } } - }) + } + } + } } } } 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 index e8e851aa1c..d0f94bff5d 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding 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.ListItem import androidx.compose.material3.MaterialTheme @@ -22,10 +21,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration @@ -96,13 +95,8 @@ fun TailnetLockSetupView( @Composable private fun ExplainerView() { - val handler = LocalUriHandler.current - Lists.MultilineDescription { - ClickableText( - explainerText(), - onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, - style = MaterialTheme.typography.bodyMedium) + Text(text = explainerText(), style = MaterialTheme.typography.bodyMedium) } } @@ -113,7 +107,7 @@ fun explainerText(): AnnotatedString { append(stringResource(id = R.string.tailnet_lock_explainer)) } - pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) + pushLink(LinkAnnotation.Url(Links.TAILNET_LOCK_KB_URL)) withStyle( style = diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index 64d4bd4d9c..2e1b4e4a2b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.AlertDialog @@ -34,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -168,10 +168,7 @@ fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = vi title = { Text(text = stringResource(R.string.delete_tailnet)) }, text = { if (isOwner) { - OwnerDeleteDialogText { - val uri = Uri.parse("https://login.tailscale.com/admin/settings/general") - context.startActivity(Intent(Intent.ACTION_VIEW, uri)) - } + OwnerDeleteDialogText() } else { Text(stringResource(R.string.request_deletion_nonowner)) } @@ -223,7 +220,7 @@ fun FusMenu( } @Composable -fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) { +fun OwnerDeleteDialogText() { val part1 = stringResource(R.string.request_deletion_owner_part1) val part2a = stringResource(R.string.request_deletion_owner_part2a) val part2b = stringResource(R.string.request_deletion_owner_part2b) @@ -231,8 +228,7 @@ fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) { val annotatedText = buildAnnotatedString { append(part1 + " ") - pushStringAnnotation( - tag = "settings", annotation = "https://login.tailscale.com/admin/settings/general") + pushLink(LinkAnnotation.Url("https://login.tailscale.com/admin/settings/general")) withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { append("Settings > General") } @@ -242,19 +238,9 @@ fun OwnerDeleteDialogText(onSettingsClick: () -> Unit) { append(part2b) } - val context = LocalContext.current - ClickableText( + Text( text = annotatedText, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), - onClick = { offset -> - annotatedText - .getStringAnnotations(tag = "settings", start = offset, end = offset) - .firstOrNull() - ?.let { annotation -> - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item)) - context.startActivity(intent) - } - }) + style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface)) } @Composable diff --git a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt index d14f7911cb..c8a4506fa1 100644 --- a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt +++ b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt @@ -124,9 +124,6 @@ object ShareFileHelper : libtailscale.ShareFileHelper { override fun openFileWriter(fileName: String, offset: Long): libtailscale.OutputStream { runBlocking { waitUntilTaildropDirReady() } val (uri, stream) = openWriterFD(fileName, offset) - if (stream == null) { - throw IOException("Failed to open file writer for $fileName") - } currentUri[fileName] = uri return OutputStreamAdapter(stream) } @@ -212,12 +209,12 @@ object ShareFileHelper : libtailscale.ShareFileHelper { override fun deleteFile(uri: String) { runBlocking { waitUntilTaildropDirReady() } val ctx = appContext ?: throw IOException("DeleteFile: not initialized") - val uri = Uri.parse(uri) + val parsedUri = Uri.parse(uri) val doc = - DocumentFile.fromSingleUri(ctx, uri) - ?: throw IOException("DeleteFile: cannot resolve URI $uri") + DocumentFile.fromSingleUri(ctx, parsedUri) + ?: throw IOException("DeleteFile: cannot resolve URI $parsedUri") if (!doc.delete()) { - throw IOException("DeleteFile: delete() returned false for $uri") + throw IOException("DeleteFile: delete() returned false for $parsedUri") } } diff --git a/android/src/main/java/com/tailscale/ipn/util/TSLog.kt b/android/src/main/java/com/tailscale/ipn/util/TSLog.kt index c078b064ca..3450e0eaa3 100644 --- a/android/src/main/java/com/tailscale/ipn/util/TSLog.kt +++ b/android/src/main/java/com/tailscale/ipn/util/TSLog.kt @@ -44,7 +44,7 @@ object TSLog { libtailscaleWrapper.sendLog(tag, message) } else { Log.e(tag, message, throwable) - libtailscaleWrapper.sendLog(tag, "$message ${throwable?.localizedMessage}") + libtailscaleWrapper.sendLog(tag, "$message ${throwable.localizedMessage}") } }