summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/Spa/build.gradle3
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt11
-rw-r--r--packages/SettingsLib/Spa/spa/build.gradle2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt57
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt65
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt1
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt10
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt41
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt189
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt30
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt57
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt10
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt (renamed from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt)3
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt146
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt4
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt13
17 files changed, 578 insertions, 67 deletions
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 68c63dad6726..4fb77d7257ca 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -18,9 +18,8 @@ buildscript {
ext {
spa_min_sdk = 21
spa_target_sdk = 33
- jetpack_compose_version = '1.2.0-alpha04'
+ jetpack_compose_version = '1.3.0'
jetpack_compose_compiler_version = '1.3.2'
- jetpack_compose_material3_version = '1.0.0-alpha06'
}
}
plugins {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
index e09ebdad7a1f..b38178b0e6f4 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -17,7 +17,10 @@
package com.android.settingslib.spa.gallery.page
import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
@@ -48,9 +51,11 @@ object SettingsPagerPageProvider : SettingsPageProvider {
@Composable
override fun Page(arguments: Bundle?) {
- SettingsScaffold(title = TITLE) {
- SettingsPager(listOf("Personal", "Work")) {
- PlaceholderTitle("Page $it")
+ SettingsScaffold(title = TITLE) { paddingValues ->
+ Box(Modifier.padding(paddingValues)) {
+ SettingsPager(listOf("Personal", "Work")) {
+ PlaceholderTitle("Page $it")
+ }
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 3bafcf27fed9..2820ed7bc96e 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -55,7 +55,7 @@ android {
dependencies {
api "androidx.appcompat:appcompat:1.7.0-alpha01"
- api "androidx.compose.material3:material3:$jetpack_compose_material3_version"
+ api "androidx.compose.material3:material3:1.1.0-alpha01"
api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
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 000000000000..8d0313fdce9f
--- /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/compose/Pager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
index bf338574c42d..4df7794e8759 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
@@ -40,7 +40,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
@@ -214,6 +213,7 @@ internal fun Pager(
horizontalAlignment = horizontalAlignment,
reverseLayout = reverseLayout,
contentPadding = contentPadding,
+ userScrollEnabled = false,
modifier = modifier,
) {
items(
@@ -241,6 +241,7 @@ internal fun Pager(
horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
reverseLayout = reverseLayout,
contentPadding = contentPadding,
+ userScrollEnabled = false,
modifier = modifier,
) {
items(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
new file mode 100644
index 000000000000..8e8805aa49d2
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
@@ -0,0 +1,65 @@
+/*
+ * 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(ExperimentalTextApi::class)
+
+package com.android.settingslib.spa.framework.theme
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.DeviceFontFamilyName
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+
+internal data class SettingsFontFamily(
+ val brand: FontFamily = FontFamily.Default,
+ val plain: FontFamily = FontFamily.Default,
+)
+
+private fun Context.getSettingsFontFamily(inInspection: Boolean): SettingsFontFamily {
+ if (inInspection) {
+ return SettingsFontFamily()
+ }
+ return SettingsFontFamily(
+ brand = FontFamily(
+ Font(getFontFamilyName("config_headlineFontFamily"), FontWeight.Normal),
+ Font(getFontFamilyName("config_headlineFontFamilyMedium"), FontWeight.Medium),
+ ),
+ plain = FontFamily(
+ Font(getFontFamilyName("config_bodyFontFamily"), FontWeight.Normal),
+ Font(getFontFamilyName("config_bodyFontFamilyMedium"), FontWeight.Medium),
+ ),
+ )
+}
+
+private fun Context.getFontFamilyName(configName: String): DeviceFontFamilyName {
+ @SuppressLint("DiscouragedApi")
+ val configId = resources.getIdentifier(configName, "string", "android")
+ return DeviceFontFamilyName(resources.getString(configId))
+}
+
+@Composable
+internal fun rememberSettingsFontFamily(): SettingsFontFamily {
+ val context = LocalContext.current
+ val inInspection = LocalInspectionMode.current
+ return remember { context.getSettingsFontFamily(inInspection) }
+}
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 69ddf01b6170..c8faef6d6703 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/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 07f09ba95ca3..03699bf4d4c0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -20,14 +20,13 @@ import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
-private class SettingsTypography {
- private val brand = FontFamily.Default
- private val plain = FontFamily.Default
+private class SettingsTypography(settingsFontFamily: SettingsFontFamily) {
+ private val brand = settingsFontFamily.brand
+ private val plain = settingsFontFamily.plain
val typography = Typography(
displayLarge = TextStyle(
@@ -140,5 +139,6 @@ private class SettingsTypography {
@Composable
internal fun rememberSettingsTypography(): Typography {
- return remember { SettingsTypography().typography }
+ val settingsFontFamily = rememberSettingsFontFamily()
+ return remember { SettingsTypography(settingsFontFamily).typography }
}
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 6a88f2dd369c..764973f2a8f5 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 000000000000..4f83ad6bd291
--- /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<String>) -> 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 d17e4645ee5e..3bc3dd72d353 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.SmallTopAppBar
-import androidx.compose.material3.Text
-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 = {
- SmallTopAppBar(
- 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
-internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
- 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 000000000000..93535203b1b9
--- /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/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
index b9690762845e..9831b918894b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
@@ -16,30 +16,26 @@
package com.android.settingslib.spa.widget.ui
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsSwitch(
checked: State<Boolean?>,
changeable: State<Boolean>,
onCheckedChange: ((newChecked: Boolean) -> Unit)? = null,
) {
- // TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is
- // updated to date.
val checkedValue = checked.value
if (checkedValue != null) {
- Checkbox(
+ Switch(
checked = checkedValue,
onCheckedChange = wrapOnSwitchWithLog(onCheckedChange),
enabled = changeable.value,
)
} else {
- Checkbox(
+ Switch(
checked = false,
onCheckedChange = null,
enabled = false,
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
index 8f460f30cf36..c94572b16198 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 000000000000..ec3379dd46ee
--- /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<String> {
+ lateinit var actualSearchQuery: State<String>
+ 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 c5ad1814a518..408b9df5e3ef 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 <T : AppRecord> 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 2be1d1c6cce6..99376b0005e4 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
@@ -17,9 +17,7 @@
package com.android.settingslib.spaprivileged.template.app
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
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,14 +47,12 @@ fun <T : AppRecord> AppListPage(
appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
) {
val showSystem = rememberSaveable { mutableStateOf(false) }
- // TODO: Use SearchScaffold here.
- SettingsScaffold(
+ SearchScaffold(
title = title,
actions = {
ShowSystemAction(showSystem.value) { showSystem.value = it }
},
- ) { paddingValues ->
- Spacer(Modifier.padding(paddingValues))
+ ) { searchQuery ->
WorkProfilePager(primaryUserOnly) { userInfo ->
Column(Modifier.fillMaxSize()) {
val options = remember { listModel.getSpinnerOptions() }
@@ -71,7 +66,7 @@ fun <T : AppRecord> AppListPage(
listModel = listModel,
showSystem = showSystem,
option = selectedOption,
- searchQuery = stateOf(""),
+ searchQuery = searchQuery,
appItem = appItem,
)
}