From deda3d4737c0227da0608ee79cee7803b4a99619 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Oct 2022 15:43:34 +0800 Subject: [PATCH] Add SearchScaffold for SpaLib This is used in App List to search the applications in list. Bug: 235727273 Test: Unit test & Manual with Settings App Change-Id: If75226f34b61c8c9fe311b38b135d2e91709c36c --- .../spa/framework/compose/Keyboards.kt | 57 ++++++ .../spa/framework/theme/SettingsOpacity.kt | 1 + .../spa/widget/scaffold/Actions.kt | 41 +++- .../spa/widget/scaffold/SearchScaffold.kt | 189 ++++++++++++++++++ .../spa/widget/scaffold/SettingsScaffold.kt | 30 +-- .../spa/widget/scaffold/SettingsTopAppBar.kt | 57 ++++++ .../spa/framework => }/OverridableFlowTest.kt | 3 +- .../spa/widget/scaffold/SearchScaffoldTest.kt | 146 ++++++++++++++ .../spaprivileged/template/app/AppList.kt | 4 +- .../spaprivileged/template/app/AppListPage.kt | 48 ++--- 10 files changed, 508 insertions(+), 68 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt rename packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/{com/android/settingslib/spa/framework => }/OverridableFlowTest.kt (92%) create mode 100644 packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt new file mode 100644 index 0000000000000..8d0313fdce9f3 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardActionScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter + +/** + * An action when run, hides the keyboard if it's open. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun hideKeyboardAction(): KeyboardActionScope.() -> Unit { + val keyboardController = LocalSoftwareKeyboardController.current + return { keyboardController?.hide() } +} + +/** + * Creates a [LazyListState] that is remembered across compositions. + * + * And when user scrolling the lazy list, hides the keyboard if it's open. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun rememberLazyListStateAndHideKeyboardWhenStartScroll(): LazyListState { + val listState = rememberLazyListState() + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(listState) { + snapshotFlow { listState.isScrollInProgress } + .distinctUntilChanged() + .filter { it } + .collect { keyboardController?.hide() } + } + return listState +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt index 69ddf01b6170d..c8faef6d67034 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt @@ -21,4 +21,5 @@ object SettingsOpacity { const val Disabled = 0.38f const val Divider = 0.2f const val SurfaceTone = 0.14f + const val Hint = 0.9f } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt index 6a88f2dd369ce..764973f2a8f51 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt @@ -16,9 +16,12 @@ package com.android.settingslib.spa.widget.scaffold +import androidx.appcompat.R import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon @@ -31,17 +34,23 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.android.settingslib.spa.framework.compose.LocalNavController +/** Action that navigates back to last page. */ @Composable internal fun NavigateBack() { val navController = LocalNavController.current - val contentDescription = stringResource( - id = androidx.appcompat.R.string.abc_action_bar_up_description, - ) + val contentDescription = stringResource(R.string.abc_action_bar_up_description) BackAction(contentDescription) { navController.navigateBack() } } +/** Action that collapses the search bar. */ +@Composable +internal fun CollapseAction(onClick: () -> Unit) { + val contentDescription = stringResource(R.string.abc_toolbar_collapse_description) + BackAction(contentDescription, onClick) +} + @Composable private fun BackAction(contentDescription: String, onClick: () -> Unit) { IconButton(onClick) { @@ -52,6 +61,28 @@ private fun BackAction(contentDescription: String, onClick: () -> Unit) { } } +/** Action that expends the search bar. */ +@Composable +internal fun SearchAction(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Outlined.FindInPage, + contentDescription = stringResource(R.string.search_menu_title), + ) + } +} + +/** Action that clear the search query. */ +@Composable +internal fun ClearAction(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = stringResource(R.string.abc_searchview_description_clear), + ) + } +} + @Composable fun MoreOptionsAction( content: @Composable ColumnScope.(onDismissRequest: () -> Unit) -> Unit, @@ -71,9 +102,7 @@ private fun MoreOptionsActionButton(onClick: () -> Unit) { IconButton(onClick) { Icon( imageVector = Icons.Outlined.MoreVert, - contentDescription = stringResource( - id = androidx.appcompat.R.string.abc_action_menu_overflow_description, - ) + contentDescription = stringResource(R.string.abc_action_menu_overflow_description), ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt new file mode 100644 index 0000000000000..4f83ad6bd2912 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.android.settingslib.spa.widget.scaffold + +import androidx.activity.compose.BackHandler +import androidx.appcompat.R +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.settingslib.spa.framework.compose.hideKeyboardAction +import com.android.settingslib.spa.framework.theme.SettingsOpacity +import com.android.settingslib.spa.framework.theme.SettingsTheme + +/** + * A [Scaffold] which content is can be full screen, and with a search feature built-in. + */ +@Composable +fun SearchScaffold( + title: String, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (searchQuery: State) -> Unit, +) { + val viewModel: SearchScaffoldViewModel = viewModel() + + Scaffold( + topBar = { + SearchableTopAppBar( + title = title, + actions = actions, + searchQuery = viewModel.searchQuery, + ) { viewModel.searchQuery = it } + }, + ) { paddingValues -> + Box( + Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + val searchQuery = remember { + derivedStateOf { viewModel.searchQuery?.text ?: "" } + } + content(searchQuery) + } + } +} + +internal class SearchScaffoldViewModel : ViewModel() { + var searchQuery: TextFieldValue? by mutableStateOf(null) +} + +@Composable +private fun SearchableTopAppBar( + title: String, + actions: @Composable RowScope.() -> Unit, + searchQuery: TextFieldValue?, + onSearchQueryChange: (TextFieldValue?) -> Unit, +) { + if (searchQuery != null) { + SearchTopAppBar( + query = searchQuery, + onQueryChange = onSearchQueryChange, + onClose = { onSearchQueryChange(null) }, + actions = actions, + ) + } else { + SettingsTopAppBar(title) { + SearchAction { onSearchQueryChange(TextFieldValue()) } + actions() + } + } +} + +@Composable +private fun SearchTopAppBar( + query: TextFieldValue, + onQueryChange: (TextFieldValue) -> Unit, + onClose: () -> Unit, + actions: @Composable RowScope.() -> Unit = {}, +) { + TopAppBar( + title = { SearchBox(query, onQueryChange) }, + modifier = Modifier.statusBarsPadding(), + navigationIcon = { CollapseAction(onClose) }, + actions = { + if (query.text.isNotEmpty()) { + ClearAction { onQueryChange(TextFieldValue()) } + } + actions() + }, + colors = settingsTopAppBarColors(), + ) + BackHandler { onClose() } +} + +@Composable +private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) { + val focusRequester = remember { FocusRequester() } + val textStyle = MaterialTheme.typography.bodyLarge + TextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + textStyle = textStyle, + placeholder = { + Text( + text = stringResource(R.string.abc_search_hint), + modifier = Modifier.alpha(SettingsOpacity.Hint), + style = textStyle, + ) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = hideKeyboardAction()), + singleLine = true, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } +} + +@Preview +@Composable +private fun SearchTopAppBarPreview() { + SettingsTheme { + SearchTopAppBar(query = TextFieldValue(), onQueryChange = {}, onClose = {}) {} + } +} + +@Preview +@Composable +private fun SearchScaffoldPreview() { + SettingsTheme { + SearchScaffold(title = "App notifications") {} + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index b50cb3ed84d0a..3bc3dd72d3532 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -18,17 +18,10 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme /** @@ -42,32 +35,11 @@ fun SettingsScaffold( content: @Composable (PaddingValues) -> Unit, ) { Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = title, - modifier = Modifier.padding(SettingsDimension.itemPaddingAround), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - }, - navigationIcon = { NavigateBack() }, - actions = actions, - colors = settingsTopAppBarColors(), - ) - }, + topBar = { SettingsTopAppBar(title, actions) }, content = content, ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( - containerColor = SettingsTheme.colorScheme.surfaceHeader, - scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, -) - @Preview @Composable private fun SettingsScaffoldPreview() { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt new file mode 100644 index 0000000000000..93535203b1b94 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.scaffold + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsTopAppBar( + title: String, + actions: @Composable RowScope.() -> Unit, +) { + TopAppBar( + title = { + Text( + text = title, + modifier = Modifier.padding(SettingsDimension.itemPaddingAround), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + navigationIcon = { NavigateBack() }, + actions = actions, + colors = settingsTopAppBarColors(), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( + containerColor = SettingsTheme.colorScheme.surfaceHeader, + scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, +) diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt similarity index 92% rename from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt rename to packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt index 8f460f30cf364..c94572b16198f 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.framework.compose.com.android.settingslib.spa.framework +package com.android.settingslib.spa.framework.compose import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.OverridableFlow import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt new file mode 100644 index 0000000000000..ec3379dd46eeb --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.scaffold + +import android.content.Context +import androidx.appcompat.R +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SearchScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun initialState_titleIsDisplayed() { + composeTestRule.setContent { + SearchScaffold(title = TITLE) {} + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun initialState_clearButtonNotExist() { + setContent() + + onClearButton().assertDoesNotExist() + } + + @Test + fun initialState_searchQueryIsEmpty() { + val searchQuery = setContent() + + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canEnterSearchMode() { + val searchQuery = setContent() + + clickSearchButton() + + composeTestRule.onNodeWithText(TITLE).assertDoesNotExist() + onSearchHint().assertIsDisplayed() + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canExitSearchMode() { + val searchQuery = setContent() + + clickSearchButton() + composeTestRule.onNodeWithContentDescription( + context.getString(R.string.abc_toolbar_collapse_description) + ).performClick() + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + onSearchHint().assertDoesNotExist() + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canEnterSearchQuery() { + val searchQuery = setContent() + + clickSearchButton() + onSearchHint().performTextInput(QUERY) + + onClearButton().assertIsDisplayed() + assertThat(searchQuery.value).isEqualTo(QUERY) + } + + @Test + fun canClearSearchQuery() { + val searchQuery = setContent() + + clickSearchButton() + onSearchHint().performTextInput(QUERY) + onClearButton().performClick() + + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + private fun setContent(): State { + lateinit var actualSearchQuery: State + composeTestRule.setContent { + SearchScaffold(title = TITLE) { searchQuery -> + SideEffect { + actualSearchQuery = searchQuery + } + } + } + return actualSearchQuery + } + + private fun clickSearchButton() { + composeTestRule.onNodeWithContentDescription( + context.getString(R.string.search_menu_title) + ).performClick() + } + + private fun onSearchHint() = composeTestRule.onNodeWithText( + context.getString(R.string.abc_search_hint) + ) + + private fun onClearButton() = composeTestRule.onNodeWithContentDescription( + context.getString(R.string.abc_searchview_description_clear) + ) + + private companion object { + const val TITLE = "title" + const val QUERY = "query" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index c5ad1814a5180..408b9df5e3ef6 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -19,7 +19,6 @@ package com.android.settingslib.spaprivileged.template.app import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState @@ -29,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.LogCompositions import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer +import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.ui.PlaceholderTitle @@ -76,7 +76,7 @@ private fun AppListWidget( } LazyColumn( modifier = Modifier.fillMaxSize(), - state = rememberLazyListState(), + state = rememberLazyListStateAndHideKeyboardWhenStartScroll(), contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical), ) { items(count = list.size, key = { option to list[it].record.app.packageName }) { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 0caa8213e1630..99376b0005e42 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -16,10 +16,8 @@ package com.android.settingslib.spaprivileged.template.app -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,9 +26,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction -import com.android.settingslib.spa.widget.scaffold.SettingsScaffold +import com.android.settingslib.spa.widget.scaffold.SearchScaffold import com.android.settingslib.spa.widget.ui.Spinner import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppListConfig @@ -50,35 +47,28 @@ fun AppListPage( appItem: @Composable (itemState: AppListItemModel) -> Unit, ) { val showSystem = rememberSaveable { mutableStateOf(false) } - // TODO: Use SearchScaffold here. - SettingsScaffold( + SearchScaffold( title = title, actions = { ShowSystemAction(showSystem.value) { showSystem.value = it } }, - ) { paddingValues -> - Box( - Modifier - .padding(paddingValues) - .fillMaxSize() - ) { - WorkProfilePager(primaryUserOnly) { userInfo -> - Column(Modifier.fillMaxSize()) { - val options = remember { listModel.getSpinnerOptions() } - val selectedOption = rememberSaveable { mutableStateOf(0) } - Spinner(options, selectedOption.value) { selectedOption.value = it } - AppList( - appListConfig = AppListConfig( - userId = userInfo.id, - showInstantApps = showInstantApps, - ), - listModel = listModel, - showSystem = showSystem, - option = selectedOption, - searchQuery = stateOf(""), - appItem = appItem, - ) - } + ) { searchQuery -> + WorkProfilePager(primaryUserOnly) { userInfo -> + Column(Modifier.fillMaxSize()) { + val options = remember { listModel.getSpinnerOptions() } + val selectedOption = rememberSaveable { mutableStateOf(0) } + Spinner(options, selectedOption.value) { selectedOption.value = it } + AppList( + appListConfig = AppListConfig( + userId = userInfo.id, + showInstantApps = showInstantApps, + ), + listModel = listModel, + showSystem = showSystem, + option = selectedOption, + searchQuery = searchQuery, + appItem = appItem, + ) } } }