Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c6f0070e authored by Josh's avatar Josh
Browse files

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
parent ad2f1b84
Loading
Loading
Loading
Loading
+61 −16
Original line number Diff line number Diff line
@@ -473,11 +473,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
    @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 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
    @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 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
    }

    @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 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {

            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,
+10 −0
Original line number Diff line number Diff line
@@ -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] -->
+111 −55
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew
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.modifiers.thenIf
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.keyboard.shortcut.ui.model.ShortcutsUiState
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 @@ private fun ActiveShortcutHelper(
        )
    } 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 @@ private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: Shor

@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 @@ private fun ShortcutHelperTwoPane(
            // 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 @@ private fun ShortcutHelperTwoPane(
            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 @@ private fun DoneButton(onClick: () -> Unit) {

@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 @@ private fun EndSidePanel(
        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)
                    onShortcutCustomizationRequestedInSubCategory(
                        requestInfo,
                        onShortcutCustomizationRequested,
                        category.type,
                    )

                        ShortcutCustomizationRequestInfo.Reset ->
                            onShortcutCustomizationRequested(requestInfo)
                    }
                },
                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 EndSidePanel(
    }
}

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 @@ private fun SubCategoryContainerDualPane(
        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 @@ private fun SubCategoryContainerDualPane(
}

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

+1 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ sealed interface ShortcutsUiState {
        val shouldShowResetButton: Boolean = false,
        val isCustomizationModeEnabled: Boolean = false,
        val allowExtendedAppShortcutsCustomization: Boolean = true,
        val shouldShowCustomAppsShortcutLimitHeader: Boolean = false,
    ) : ShortcutsUiState

    data object Inactive : ShortcutsUiState
+11 −4
Original line number Diff line number Diff line
@@ -98,6 +98,12 @@ constructor(
                    val filteredCategories =
                        filterCategoriesBySearchQuery(query, categoriesWithLauncherExcluded)
                    val shortcutCategoriesUi = convertCategoriesModelToUiModel(filteredCategories)

                    val allowExtendedAppShortcutsCustomization =
                        !isExtendedAppsShortcutCustomizationLimitReached(
                            shortcutCategories = categoriesWithLauncherExcluded
                        )

                    ShortcutsUiState.Active(
                        searchQuery = query,
                        shortcutCategories = shortcutCategoriesUi,
@@ -107,10 +113,11 @@ constructor(
                        isExtendedAppCategoryFlagEnabled = extendedAppsShortcutCategory(),
                        shouldShowResetButton = shouldShowResetButton(shortcutCategoriesUi),
                        isCustomizationModeEnabled = isCustomizationModeEnabled,
                        allowExtendedAppShortcutsCustomization =
                            !isExtendedAppsShortcutCustomizationLimitReached(
                                categoriesWithLauncherExcluded
                            ),
                        allowExtendedAppShortcutsCustomization,
                        shouldShowCustomAppsShortcutLimitHeader =
                            isCustomizationModeEnabled &&
                                extendedAppsShortcutCategory() &&
                                !allowExtendedAppShortcutsCustomization,
                    )
                }
            }