From 52037d7e1393af1f618af58201fda7643d2efe43 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:54:57 -0800 Subject: [PATCH] android: put new search behind flag (#587) Until the WIP feature is ready Updates tailscale/corp#18973 Signed-off-by: kari-ts Signed-off-by: kari-ts <135075563+kari-ts@users.noreply.github.com> (cherry picked from commit e500111fb9ed4b02c309a230759fd613679b703d) --- .../src/main/java/com/tailscale/ipn/App.kt | 2 + .../com/tailscale/ipn/ui/view/MainView.kt | 57 +++++++++++++++++-- .../com/tailscale/ipn/util/FeatureFlags.kt | 26 +++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/util/FeatureFlags.kt diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 0091c71c7f..ca666906bd 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -35,6 +35,7 @@ import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory import com.tailscale.ipn.util.TSLog +import com.tailscale.ipn.util.FeatureFlags import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -190,6 +191,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { applicationScope.launch { val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() } + FeatureFlags.initialize(mapOf("enable_new_search" to false)) } private fun initViewModels() { 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 419ccda11f..ef07e9654a 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 @@ -29,7 +29,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu @@ -41,6 +44,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults @@ -64,7 +68,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -92,6 +95,7 @@ import com.tailscale.ipn.ui.theme.exitNodeToggleButton import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.minTextSize import com.tailscale.ipn.ui.theme.primaryListItem +import com.tailscale.ipn.ui.theme.searchBarColors import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.short import com.tailscale.ipn.ui.theme.surfaceContainerListItem @@ -107,6 +111,7 @@ import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.viewModel.IpnViewModel.NodeState import com.tailscale.ipn.ui.viewModel.MainViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModel +import com.tailscale.ipn.util.FeatureFlags // Navigation actions for the MainView data class MainViewNavigation( @@ -220,6 +225,7 @@ fun MainView( PeerList( viewModel = viewModel, onNavigateToPeerDetails = navigation.onNavigateToPeerDetails, + onSearchBarClick = navigation.onNavigateToSearch, onSearch = { viewModel.searchPeers(it) }) } Ipn.State.NoState, @@ -523,6 +529,7 @@ fun ConnectView( fun PeerList( viewModel: MainViewModel, onNavigateToPeerDetails: (Tailcfg.Node) -> Unit, + onSearchBarClick: () -> Unit, onSearch: (String) -> Unit ) { val peerList by viewModel.peers.collectAsState(initial = emptyList()) @@ -532,17 +539,59 @@ fun PeerList( val netmap = viewModel.netmap.collectAsState() val focusManager = LocalFocusManager.current - var isFocussed by remember { mutableStateOf(false) } + var isSearchFocussed by remember { mutableStateOf(false) } var isListFocussed by remember { mutableStateOf(false) } val expandedPeer = viewModel.expandedMenuPeer.collectAsState() val localClipboardManager = LocalClipboardManager.current val enableSearch = !isAndroidTV() Column(modifier = Modifier.fillMaxSize()) { - if (enableSearch) { - SearchWithDynamicSuggestions(viewModel, onSearch) + if (enableSearch && FeatureFlags.isEnabled("enable_new_search")) { + Search(onSearchBarClick) Spacer(modifier = Modifier.height(if (showNoResults) 0.dp else 8.dp)) + } else { + if (enableSearch) { + Box( + modifier = + Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) { + OutlinedTextField( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 0.dp) + .onFocusChanged { isSearchFocussed = it.isFocused }, + singleLine = true, + shape = MaterialTheme.shapes.extraLarge, + colors = MaterialTheme.colorScheme.searchBarColors, + leadingIcon = { + Icon(imageVector = Icons.Outlined.Search, contentDescription = "search") + }, + trailingIcon = { + if (isSearchFocussed) { + IconButton( + onClick = { + focusManager.clearFocus() + onSearch("") + }) { + Icon( + imageVector = + if (searchTermStr.isEmpty()) Icons.Outlined.Close + else Icons.Outlined.Clear, + contentDescription = "clear search", + tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + }, + placeholder = { + Text( + text = stringResource(id = R.string.search), + style = MaterialTheme.typography.bodyLarge, + maxLines = 1) + }, + value = searchTermStr, + onValueChange = { onSearch(it) }) + } + } } // Peers display diff --git a/android/src/main/java/com/tailscale/ipn/util/FeatureFlags.kt b/android/src/main/java/com/tailscale/ipn/util/FeatureFlags.kt new file mode 100644 index 0000000000..af3f9b7823 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/util/FeatureFlags.kt @@ -0,0 +1,26 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package com.tailscale.ipn.util + +object FeatureFlags { + + // Map to hold the feature flags + private val flags: MutableMap = mutableMapOf() + + fun initialize(defaults: Map) { + flags.clear() + flags.putAll(defaults) + } + + fun enable(feature: String) { + flags[feature] = true + } + + fun disable(feature: String) { + flags[feature] = false + } + + fun isEnabled(feature: String): Boolean { + return flags[feature] ?: false + } +}