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,
                     )
                 }
             }