Added sticky header, when custom app shortcuts limit is used up
Flag: com.android.systemui.extended_apps_shortcut_category
Test: ShortcutHelperViewModelTest + manual UI test.
Fix: 405985619
Change-Id: Ib8e79ba5dd9671aef21f2c281b386a56175b8c24
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
index 747e689..cde9edd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -473,11 +473,7 @@
@Test
fun allowExtendedAppShortcutsCustomization_true_WhenExtraAppsShortcutsCustomizedIsBelowLimit() {
testScope.runTest {
- setupShortcutHelperWithExtendedAppsShortcutCustomizations(
- numberOfDefaultAppsShortcuts = 3,
- numberOfCustomShortcutsForDefaultApps = 3,
- numberOfCustomShortcutsForExtendedApps = 3,
- )
+ openShortcutHelper()
underTest.toggleCustomizationMode(true)
val uiState by collectLastValue(underTest.shortcutsUiState)
@@ -491,10 +487,8 @@
@Test
fun allowExtendedAppShortcutsCustomization_false_WhenExtraAppsShortcutsCustomizedIsAtLimit() {
testScope.runTest {
- setupShortcutHelperWithExtendedAppsShortcutCustomizations(
- numberOfDefaultAppsShortcuts = 3,
- numberOfCustomShortcutsForDefaultApps = 3,
- numberOfCustomShortcutsForExtendedApps = EXTENDED_APPS_SHORTCUT_CUSTOMIZATION_LIMIT,
+ openShortcutHelper(
+ customShortcutsCountForExtendedApps = EXTENDED_APPS_SHORTCUT_CUSTOMIZATION_LIMIT
)
underTest.toggleCustomizationMode(true)
@@ -507,13 +501,10 @@
}
@Test
- fun allowExtendedAppShortcutsCustomization_false_WhenExtraAppsShortcutsCustomizedIsAboveLimit() {
+ fun showsCustomAppsShortcutLimitHeader_whenAtLimit_customizationModeEnabled() {
testScope.runTest {
- setupShortcutHelperWithExtendedAppsShortcutCustomizations(
- numberOfDefaultAppsShortcuts = 3,
- numberOfCustomShortcutsForDefaultApps = 3,
- numberOfCustomShortcutsForExtendedApps =
- EXTENDED_APPS_SHORTCUT_CUSTOMIZATION_LIMIT + 3,
+ openShortcutHelper(
+ customShortcutsCountForExtendedApps = EXTENDED_APPS_SHORTCUT_CUSTOMIZATION_LIMIT
)
underTest.toggleCustomizationMode(true)
@@ -521,10 +512,64 @@
val activeUiState = uiState as ShortcutsUiState.Active
- assertThat(activeUiState.allowExtendedAppShortcutsCustomization).isFalse()
+ assertThat(activeUiState.shouldShowCustomAppsShortcutLimitHeader).isTrue()
}
}
+ @Test
+ fun doesNotShowCustomAppsShortcutLimitHeader_whenBelowLimit_customizationModeEnabled() {
+ testScope.runTest {
+ openShortcutHelper()
+
+ underTest.toggleCustomizationMode(true)
+ val uiState by collectLastValue(underTest.shortcutsUiState)
+
+ val activeUiState = uiState as ShortcutsUiState.Active
+
+ assertThat(activeUiState.shouldShowCustomAppsShortcutLimitHeader).isFalse()
+ }
+ }
+
+ @Test
+ fun doesNotShowCustomAppsShortcutLimitHeader_whenAtLimit_customizationModeDisabled() {
+ testScope.runTest {
+ openShortcutHelper(
+ customShortcutsCountForExtendedApps = EXTENDED_APPS_SHORTCUT_CUSTOMIZATION_LIMIT
+ )
+ underTest.toggleCustomizationMode(false)
+ val uiState by collectLastValue(underTest.shortcutsUiState)
+
+ val activeUiState = uiState as ShortcutsUiState.Active
+
+ assertThat(activeUiState.shouldShowCustomAppsShortcutLimitHeader).isFalse()
+ }
+ }
+
+ @Test
+ fun doesNotShowCustomAppsShortcutLimitHeader_whenBelowLimit_customizationModeDisabled() {
+ testScope.runTest {
+ openShortcutHelper()
+ underTest.toggleCustomizationMode(false)
+ val uiState by collectLastValue(underTest.shortcutsUiState)
+
+ val activeUiState = uiState as ShortcutsUiState.Active
+
+ assertThat(activeUiState.shouldShowCustomAppsShortcutLimitHeader).isFalse()
+ }
+ }
+
+ private fun openShortcutHelper(
+ customShortcutsCountForExtendedApps: Int = 0,
+ customShortcutsCountForDefaultApps: Int = 3,
+ defaultShortcutsCount: Int = 3,
+ ) {
+ setupShortcutHelperWithExtendedAppsShortcutCustomizations(
+ numberOfDefaultAppsShortcuts = defaultShortcutsCount,
+ numberOfCustomShortcutsForDefaultApps = customShortcutsCountForDefaultApps,
+ numberOfCustomShortcutsForExtendedApps = customShortcutsCountForExtendedApps,
+ )
+ }
+
private fun setupShortcutHelperWithExtendedAppsShortcutCustomizations(
numberOfDefaultAppsShortcuts: Int,
numberOfCustomShortcutsForDefaultApps: Int,
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f4b205a..ed46f08 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -4194,6 +4194,16 @@
The helper is a component that shows the user which keyboard shortcuts they can use.
[CHAR LIMIT=NONE] -->
<string name="shortcut_helper_delete_shortcut_button_label">Delete shortcut</string>
+ <!-- Message displayed at the top of shortcut helper when user has already added 10 custom
+ keyboard shortcuts for extra applications in shortcut helper. The helper is a component
+ that shows the user which keyboard shortcuts they can use and allows users to customize
+ their keyboard shortcuts-->
+ <string name="shortcut_helper_app_custom_shortcut_limit_exceeded">10 app custom shortcut limit has been used up</string>
+ <!-- Instruction displayed at the top of shortcut helper when user has already added 10 custom
+ keyboard shortcuts for extra applications in shortcut helper. The helper is a component
+ that shows the user which keyboard shortcuts they can use and allows users to customize
+ their keyboard shortcuts-->
+ <string name="shortcut_helper_app_custom_shortcut_limit_exceeded_instruction">Delete a shortcut to be able to add a new one</string>
<!-- Keyboard touchpad tutorial scheduler-->
<!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 26035fb..d3e74aa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -57,6 +57,7 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tune
@@ -121,6 +122,7 @@
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
@@ -132,6 +134,7 @@
import com.android.systemui.res.R
import kotlinx.coroutines.delay
+// TODO break down this file into smaller files for readability. b/424757065
@Composable
fun ShortcutHelper(
onSearchQueryChanged: (String) -> Unit,
@@ -187,18 +190,12 @@
)
} else {
ShortcutHelperTwoPane(
- shortcutsUiState.searchQuery,
onSearchQueryChanged,
- shortcutsUiState.shortcutCategories,
selectedCategoryType,
onCategorySelected = { selectedCategoryType = it },
onKeyboardSettingsClicked,
- shortcutsUiState.isShortcutCustomizerFlagEnabled,
- shortcutsUiState.shouldShowResetButton,
- shortcutsUiState.isCustomizationModeEnabled,
onCustomizationModeToggled,
- shortcutsUiState.isExtendedAppCategoryFlagEnabled,
- shortcutsUiState.allowExtendedAppShortcutsCustomization,
+ shortcutsUiState,
modifier,
onShortcutCustomizationRequested,
)
@@ -382,22 +379,17 @@
@Composable
private fun ShortcutHelperTwoPane(
- searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
- categories: List<ShortcutCategoryUi>,
selectedCategoryType: ShortcutCategoryType?,
onCategorySelected: (ShortcutCategoryType?) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
- isShortcutCustomizerFlagEnabled: Boolean,
- shouldShowResetButton: Boolean,
- isCustomizationModeEnabled: Boolean,
onCustomizationModeToggled: (isCustomizing: Boolean) -> Unit,
- isExtendedAppCategoryFlagEnabled: Boolean,
- allowExtendedAppShortcutsCustomization: Boolean,
+ uiState: ShortcutsUiState.Active,
modifier: Modifier = Modifier,
onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
- val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
+ val selectedCategory =
+ uiState.shortcutCategories.fastFirstOrNull { it.type == selectedCategoryType }
Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
Row(
@@ -408,19 +400,19 @@
// Keep title centered whether customize button is visible or not.
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.width(412.dp), contentAlignment = Alignment.Center) {
- TitleBar(isCustomizationModeEnabled)
+ TitleBar(uiState.isCustomizationModeEnabled)
}
- if (isShortcutCustomizerFlagEnabled) {
+ if (uiState.isShortcutCustomizerFlagEnabled) {
CustomizationButtonsContainer(
modifier = Modifier.weight(1f),
- isCustomizing = isCustomizationModeEnabled,
+ isCustomizing = uiState.isCustomizationModeEnabled,
onToggleCustomizationMode = {
- onCustomizationModeToggled(!isCustomizationModeEnabled)
+ onCustomizationModeToggled(!uiState.isCustomizationModeEnabled)
},
onReset = {
onShortcutCustomizationRequested(ShortcutCustomizationRequestInfo.Reset)
},
- shouldShowResetButton = shouldShowResetButton,
+ shouldShowResetButton = uiState.shouldShowResetButton,
)
} else {
Spacer(modifier = Modifier.weight(1f))
@@ -431,20 +423,16 @@
StartSidePanel(
onSearchQueryChanged = onSearchQueryChanged,
modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true },
- categories = categories,
+ categories = uiState.shortcutCategories,
onKeyboardSettingsClicked = onKeyboardSettingsClicked,
selectedCategory = selectedCategoryType,
onCategoryClicked = { onCategorySelected(it.type) },
)
Spacer(modifier = Modifier.width(24.dp))
EndSidePanel(
- searchQuery,
- isCustomizationModeEnabled,
+ uiState,
onCustomizationModeToggled,
selectedCategory,
- isCustomizing = isCustomizationModeEnabled,
- isExtendedAppCategoryFlagEnabled,
- allowExtendedAppShortcutsCustomization,
Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true },
onShortcutCustomizationRequested,
)
@@ -511,17 +499,14 @@
@Composable
private fun EndSidePanel(
- searchQuery: String,
- isCustomizationModeEnabled: Boolean,
+ uiState: ShortcutsUiState.Active,
onCustomizationModeToggled: (isCustomizing: Boolean) -> Unit,
category: ShortcutCategoryUi?,
- isCustomizing: Boolean,
- isExtendedAppCategoryFlagEnabled: Boolean,
- allowExtendedAppShortcutsCustomization: Boolean,
modifier: Modifier = Modifier,
onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
val listState = rememberLazyListState()
+
LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
if (category == null) {
NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false)
@@ -532,36 +517,39 @@
state = listState,
horizontalAlignment = Alignment.CenterHorizontally,
) {
+ stickyHeader {
+ Column {
+ AnimatedVisibility(
+ category.type == AppCategories &&
+ uiState.shouldShowCustomAppsShortcutLimitHeader
+ ) {
+ AppCustomShortcutLimitContainer(Modifier.padding(8.dp))
+ }
+ }
+ }
items(category.subCategories) { subcategory ->
SubCategoryContainerDualPane(
- searchQuery = searchQuery,
- subCategory = subcategory,
- isCustomizing = isCustomizing and category.type.includeInCustomization,
+ uiState.searchQuery,
+ subcategory,
+ isCustomizing =
+ uiState.isCustomizationModeEnabled && category.type.includeInCustomization,
onShortcutCustomizationRequested = { requestInfo ->
- when (requestInfo) {
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
- onShortcutCustomizationRequested(
- requestInfo.copy(categoryType = category.type)
- )
-
- is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
- onShortcutCustomizationRequested(
- requestInfo.copy(categoryType = category.type)
- )
-
- ShortcutCustomizationRequestInfo.Reset ->
- onShortcutCustomizationRequested(requestInfo)
- }
+ onShortcutCustomizationRequestedInSubCategory(
+ requestInfo,
+ onShortcutCustomizationRequested,
+ category.type,
+ )
},
- allowExtendedAppShortcutsCustomization = allowExtendedAppShortcutsCustomization,
+ uiState.allowExtendedAppShortcutsCustomization,
)
Spacer(modifier = Modifier.height(8.dp))
}
+
if (
- category.type == ShortcutCategoryType.AppCategories &&
- !isCustomizationModeEnabled &&
- isExtendedAppCategoryFlagEnabled &&
- allowExtendedAppShortcutsCustomization
+ category.type == AppCategories &&
+ !uiState.isCustomizationModeEnabled &&
+ uiState.isExtendedAppCategoryFlagEnabled &&
+ uiState.allowExtendedAppShortcutsCustomization
) {
item {
ShortcutHelperButton(
@@ -577,6 +565,73 @@
}
}
+private fun onShortcutCustomizationRequestedInSubCategory(
+ requestInfo: ShortcutCustomizationRequestInfo,
+ onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit,
+ categoryType: ShortcutCategoryType,
+) {
+ when (requestInfo) {
+ is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
+ onShortcutCustomizationRequested(requestInfo.copy(categoryType = categoryType))
+
+ is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
+ onShortcutCustomizationRequested(requestInfo.copy(categoryType = categoryType))
+
+ ShortcutCustomizationRequestInfo.Reset -> onShortcutCustomizationRequested(requestInfo)
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun AppCustomShortcutLimitContainer(modifier: Modifier = Modifier) {
+ Row(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .background(
+ color = MaterialTheme.colorScheme.secondaryContainer,
+ shape = RoundedCornerShape(40.dp),
+ )
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Surface(
+ shape = CircleShape,
+ modifier = Modifier.size(40.dp),
+ color = MaterialTheme.colorScheme.secondary,
+ ) {
+ Icon(
+ imageVector = Icons.Default.Info,
+ tint = MaterialTheme.colorScheme.onSecondary,
+ modifier = Modifier.size(24.dp).padding(8.dp),
+ contentDescription = null,
+ )
+ }
+
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.shortcut_helper_app_custom_shortcut_limit_exceeded),
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ text =
+ stringResource(
+ R.string.shortcut_helper_app_custom_shortcut_limit_exceeded_instruction
+ ),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+}
+
@Composable
private fun NoSearchResultsText(horizontalPadding: Dp, fillHeight: Boolean) {
var modifier = Modifier.fillMaxWidth()
@@ -609,7 +664,7 @@
color = MaterialTheme.colorScheme.surfaceBright,
) {
Column(Modifier.padding(16.dp)) {
- SubCategoryTitle(subCategory.label)
+ SubCategoryTitle(subCategory.label, Modifier.padding(8.dp))
Spacer(Modifier.height(8.dp))
subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
if (index > 0) {
@@ -647,11 +702,12 @@
}
@Composable
-private fun SubCategoryTitle(title: String) {
+private fun SubCategoryTitle(title: String, modifier: Modifier = Modifier) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
+ modifier = modifier,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt
index 45fa95b..077acfd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutsUiState.kt
@@ -29,6 +29,7 @@
val shouldShowResetButton: Boolean = false,
val isCustomizationModeEnabled: Boolean = false,
val allowExtendedAppShortcutsCustomization: Boolean = true,
+ val shouldShowCustomAppsShortcutLimitHeader: Boolean = false,
) : ShortcutsUiState
data object Inactive : ShortcutsUiState
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
index eef0aa5..4f008d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -98,6 +98,12 @@
val filteredCategories =
filterCategoriesBySearchQuery(query, categoriesWithLauncherExcluded)
val shortcutCategoriesUi = convertCategoriesModelToUiModel(filteredCategories)
+
+ val allowExtendedAppShortcutsCustomization =
+ !isExtendedAppsShortcutCustomizationLimitReached(
+ shortcutCategories = categoriesWithLauncherExcluded
+ )
+
ShortcutsUiState.Active(
searchQuery = query,
shortcutCategories = shortcutCategoriesUi,
@@ -107,10 +113,11 @@
isExtendedAppCategoryFlagEnabled = extendedAppsShortcutCategory(),
shouldShowResetButton = shouldShowResetButton(shortcutCategoriesUi),
isCustomizationModeEnabled = isCustomizationModeEnabled,
- allowExtendedAppShortcutsCustomization =
- !isExtendedAppsShortcutCustomizationLimitReached(
- categoriesWithLauncherExcluded
- ),
+ allowExtendedAppShortcutsCustomization,
+ shouldShowCustomAppsShortcutLimitHeader =
+ isCustomizationModeEnabled &&
+ extendedAppsShortcutCategory() &&
+ !allowExtendedAppShortcutsCustomization,
)
}
}