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

Commit 3316261c authored by Chris Göllner's avatar Chris Göllner Committed by Josh
Browse files

Shortcut Helper - Connect UI layer to shortcuts

Test: unit tests in this CL
Test: manually
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Bug: 341045652
Change-Id: I86e51ab65fb3f64ed12e920d5f82970eafe42d32
parent ee9db8e9
Loading
Loading
Loading
Loading
+84 −37
Original line number Diff line number Diff line
@@ -44,7 +44,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tv
import androidx.compose.material.icons.filled.VerticalSplit
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@@ -73,6 +76,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
@@ -83,19 +87,42 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import com.android.systemui.res.R

@Composable
fun ShortcutHelper(
    onKeyboardSettingsClicked: () -> Unit,
    modifier: Modifier = Modifier,
    categories: List<ShortcutHelperCategory> = ShortcutHelperTemporaryData.categories,
    shortcutsUiState: ShortcutsUiState,
    useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() },
) {
    when (shortcutsUiState) {
        is ShortcutsUiState.Active -> {
            if (useSinglePane()) {
        ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked)
                ShortcutHelperSinglePane(
                    modifier,
                    shortcutsUiState.shortcutCategories,
                    onKeyboardSettingsClicked
                )
            } else {
        ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked)
                ShortcutHelperTwoPane(
                    modifier,
                    shortcutsUiState.shortcutCategories,
                    shortcutsUiState.defaultSelectedCategory,
                    onKeyboardSettingsClicked
                )
            }
        }
        is ShortcutsUiState.Inactive -> {
            // No-op for now.
        }
    }
}

@@ -107,7 +134,7 @@ private fun shouldUseSinglePane() =
@Composable
private fun ShortcutHelperSinglePane(
    modifier: Modifier = Modifier,
    categories: List<ShortcutHelperCategory>,
    categories: List<ShortcutCategory>,
    onKeyboardSettingsClicked: () -> Unit,
) {
    Column(
@@ -129,9 +156,9 @@ private fun ShortcutHelperSinglePane(

@Composable
private fun CategoriesPanelSinglePane(
    categories: List<ShortcutHelperCategory>,
    categories: List<ShortcutCategory>,
) {
    var expandedCategory by remember { mutableStateOf<ShortcutHelperCategory?>(null) }
    var expandedCategory by remember { mutableStateOf<ShortcutCategory?>(null) }
    Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
        categories.fastForEachIndexed { index, category ->
            val isExpanded = expandedCategory == category
@@ -162,7 +189,7 @@ private fun CategoriesPanelSinglePane(

@Composable
private fun CategoryItemSinglePane(
    category: ShortcutHelperCategory,
    category: ShortcutCategory,
    isExpanded: Boolean,
    onClick: () -> Unit,
    shape: Shape,
@@ -188,6 +215,22 @@ private fun CategoryItemSinglePane(
    }
}

private val ShortcutCategory.icon: ImageVector
    get() =
        when (type) {
            ShortcutCategoryType.SYSTEM -> Icons.Default.Tv
            ShortcutCategoryType.MULTI_TASKING -> Icons.Default.VerticalSplit
            ShortcutCategoryType.IME -> Icons.Default.Keyboard
        }

private val ShortcutCategory.labelResId: Int
    get() =
        when (type) {
            ShortcutCategoryType.SYSTEM -> R.string.shortcut_helper_category_system
            ShortcutCategoryType.MULTI_TASKING -> R.string.shortcut_helper_category_system
            ShortcutCategoryType.IME -> R.string.shortcut_helper_category_input
        }

@Composable
private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
    val expandIconRotationDegrees by
@@ -219,7 +262,7 @@ private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
}

@Composable
private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) {
private fun ShortcutCategoryDetailsSinglePane(category: ShortcutCategory) {
    Column(Modifier.padding(horizontal = 16.dp)) {
        category.subCategories.fastForEach { subCategory ->
            ShortcutSubCategorySinglePane(subCategory)
@@ -228,7 +271,7 @@ private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory)
}

@Composable
private fun ShortcutSubCategorySinglePane(subCategory: SubCategory) {
private fun ShortcutSubCategorySinglePane(subCategory: ShortcutSubCategory) {
    // This @Composable is expected to be in a Column.
    SubCategoryTitle(subCategory.label)
    subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
@@ -251,10 +294,12 @@ private fun ShortcutSinglePane(shortcut: Shortcut) {
@Composable
private fun ShortcutHelperTwoPane(
    modifier: Modifier = Modifier,
    categories: List<ShortcutHelperCategory>,
    categories: List<ShortcutCategory>,
    defaultSelectedCategory: ShortcutCategoryType,
    onKeyboardSettingsClicked: () -> Unit,
) {
    var selectedCategory by remember { mutableStateOf(categories.first()) }
    var selectedCategoryType by remember { mutableStateOf(defaultSelectedCategory) }
    val selectedCategory = categories.first { it.type == selectedCategoryType }
    Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) {
        TitleBar()
        Spacer(modifier = Modifier.height(12.dp))
@@ -262,8 +307,8 @@ private fun ShortcutHelperTwoPane(
            StartSidePanel(
                modifier = Modifier.fillMaxWidth(fraction = 0.32f),
                categories = categories,
                selectedCategory = selectedCategory,
                onCategoryClicked = { selectedCategory = it },
                selectedCategory = selectedCategoryType,
                onCategoryClicked = { selectedCategoryType = it.type },
                onKeyboardSettingsClicked = onKeyboardSettingsClicked,
            )
            Spacer(modifier = Modifier.width(24.dp))
@@ -273,7 +318,7 @@ private fun ShortcutHelperTwoPane(
}

@Composable
private fun EndSidePanel(modifier: Modifier, category: ShortcutHelperCategory) {
private fun EndSidePanel(modifier: Modifier, category: ShortcutCategory) {
    LazyColumn(modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
        items(items = category.subCategories, key = { item -> item.label }) {
            SubCategoryContainerDualPane(it)
@@ -283,7 +328,7 @@ private fun EndSidePanel(modifier: Modifier, category: ShortcutHelperCategory) {
}

@Composable
private fun SubCategoryContainerDualPane(subCategory: SubCategory) {
private fun SubCategoryContainerDualPane(subCategory: ShortcutSubCategory) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(28.dp),
@@ -315,11 +360,12 @@ private fun SubCategoryTitle(title: String) {
private fun ShortcutViewDualPane(shortcut: Shortcut) {
    Row(Modifier.padding(vertical = 16.dp)) {
        ShortcutDescriptionText(
            modifier = Modifier.weight(0.25f).align(Alignment.CenterVertically),
            modifier = Modifier.width(160.dp).align(Alignment.CenterVertically),
            shortcut = shortcut,
        )
        Spacer(modifier = Modifier.width(16.dp))
        ShortcutKeyCombinations(
            modifier = Modifier.weight(0.75f),
            modifier = Modifier.weight(1f),
            shortcut = shortcut,
        )
    }
@@ -343,7 +389,7 @@ private fun ShortcutKeyCombinations(

@Composable
private fun ShortcutCommand(command: ShortcutCommand) {
    // This @Composable is expected to be in a Row or FlowRow.
    Row {
        command.keys.forEachIndexed { keyIndex, key ->
            if (keyIndex > 0) {
                Spacer(Modifier.width(4.dp))
@@ -357,6 +403,7 @@ private fun ShortcutCommand(command: ShortcutCommand) {
            }
        }
    }
}

@Composable
private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) {
@@ -384,7 +431,7 @@ private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) {
@Composable
private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) {
    Icon(
        imageVector = key.value,
        painter = painterResource(key.drawableResId),
        contentDescription = null,
        modifier = Modifier.align(Alignment.Center).padding(6.dp)
    )
@@ -418,10 +465,10 @@ private fun ShortcutDescriptionText(
@Composable
private fun StartSidePanel(
    modifier: Modifier,
    categories: List<ShortcutHelperCategory>,
    categories: List<ShortcutCategory>,
    onKeyboardSettingsClicked: () -> Unit,
    selectedCategory: ShortcutHelperCategory,
    onCategoryClicked: (ShortcutHelperCategory) -> Unit,
    selectedCategory: ShortcutCategoryType,
    onCategoryClicked: (ShortcutCategory) -> Unit,
) {
    Column(modifier) {
        ShortcutsSearchBar()
@@ -434,16 +481,16 @@ private fun StartSidePanel(

@Composable
private fun CategoriesPanelTwoPane(
    categories: List<ShortcutHelperCategory>,
    selectedCategory: ShortcutHelperCategory,
    onCategoryClicked: (ShortcutHelperCategory) -> Unit
    categories: List<ShortcutCategory>,
    selectedCategory: ShortcutCategoryType,
    onCategoryClicked: (ShortcutCategory) -> Unit
) {
    Column {
        categories.fastForEach {
            CategoryItemTwoPane(
                label = stringResource(it.labelResId),
                icon = it.icon,
                selected = selectedCategory == it,
                selected = selectedCategory == it.type,
                onClick = { onCategoryClicked(it) }
            )
        }
+0 −251
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.keyboard.shortcut.ui.composable

import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Accessibility
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.KeyboardCommandKey
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.Tv
import androidx.compose.material.icons.filled.VerticalSplit
import androidx.compose.ui.graphics.vector.ImageVector
import com.android.systemui.res.R

/** Temporary data classes and data below just to populate the UI. */
data class ShortcutHelperCategory(
    @StringRes val labelResId: Int,
    val icon: ImageVector,
    val subCategories: List<SubCategory>,
)

data class SubCategory(
    val label: String,
    val shortcuts: List<Shortcut>,
)

data class Shortcut(val label: String, val commands: List<ShortcutCommand>)

data class ShortcutCommand(val keys: List<ShortcutKey>)

sealed interface ShortcutKey {
    data class Text(val value: String) : ShortcutKey

    data class Icon(val value: ImageVector) : ShortcutKey
}

// DSL Builder Functions
private fun shortcutHelperCategory(
    labelResId: Int,
    icon: ImageVector,
    block: ShortcutHelperCategoryBuilder.() -> Unit
): ShortcutHelperCategory = ShortcutHelperCategoryBuilder(labelResId, icon).apply(block).build()

private fun ShortcutHelperCategoryBuilder.subCategory(
    label: String,
    block: SubCategoryBuilder.() -> Unit
) {
    subCategories.add(SubCategoryBuilder(label).apply(block).build())
}

private fun SubCategoryBuilder.shortcut(label: String, block: ShortcutBuilder.() -> Unit) {
    shortcuts.add(ShortcutBuilder(label).apply(block).build())
}

private fun ShortcutBuilder.command(block: ShortcutCommandBuilder.() -> Unit) {
    commands.add(ShortcutCommandBuilder().apply(block).build())
}

private fun ShortcutCommandBuilder.key(value: String) {
    keys.add(ShortcutKey.Text(value))
}

private fun ShortcutCommandBuilder.key(value: ImageVector) {
    keys.add(ShortcutKey.Icon(value))
}

private class ShortcutHelperCategoryBuilder(
    private val labelResId: Int,
    private val icon: ImageVector
) {
    val subCategories = mutableListOf<SubCategory>()

    fun build() = ShortcutHelperCategory(labelResId, icon, subCategories)
}

private class SubCategoryBuilder(private val label: String) {
    val shortcuts = mutableListOf<Shortcut>()

    fun build() = SubCategory(label, shortcuts)
}

private class ShortcutBuilder(private val label: String) {
    val commands = mutableListOf<ShortcutCommand>()

    fun build() = Shortcut(label, commands)
}

private class ShortcutCommandBuilder {
    val keys = mutableListOf<ShortcutKey>()

    fun build() = ShortcutCommand(keys)
}

object ShortcutHelperTemporaryData {

    // Some shortcuts and their strings below are made up just to populate the UI for now.
    // For this reason they are not in translatable resources yet.
    val categories =
        listOf(
            shortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv) {
                subCategory("System controls") {
                    shortcut("Go to home screen") {
                        command { key(Icons.Default.RadioButtonUnchecked) }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("H")
                        }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Return")
                        }
                    }
                    shortcut("View recent apps") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Tab")
                        }
                    }
                    shortcut("All apps search") {
                        command { key(Icons.Default.KeyboardCommandKey) }
                    }
                }
                subCategory("System apps") {
                    shortcut("Go back") {
                        command { key(Icons.Default.ArrowBackIosNew) }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Left arrow")
                        }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("ESC")
                        }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Backspace")
                        }
                    }
                    shortcut("View notifications") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("N")
                        }
                    }
                    shortcut("Take a screenshot") {
                        command { key(Icons.Default.KeyboardCommandKey) }
                        command { key("CTRL") }
                        command { key("S") }
                    }
                    shortcut("Open Settings") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("I")
                        }
                    }
                }
            },
            shortcutHelperCategory(
                R.string.shortcut_helper_category_multitasking,
                Icons.Default.VerticalSplit
            ) {
                subCategory("Multitasking & windows") {
                    shortcut("Take a screenshot") {
                        command { key(Icons.Default.KeyboardCommandKey) }
                        command { key("CTRL") }
                        command { key("S") }
                    }
                }
            },
            shortcutHelperCategory(
                R.string.shortcut_helper_category_input,
                Icons.Default.Keyboard
            ) {
                subCategory("Input") {
                    shortcut("Open Settings") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("I")
                        }
                    }
                    shortcut("View notifications") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("N")
                        }
                    }
                }
            },
            shortcutHelperCategory(
                R.string.shortcut_helper_category_app_shortcuts,
                Icons.Default.Apps
            ) {
                subCategory("App shortcuts") {
                    shortcut("Open Settings") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("I")
                        }
                    }
                    shortcut("Go back") {
                        command { key(Icons.Default.ArrowBackIosNew) }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Left arrow")
                        }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("ESC")
                        }
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Backspace")
                        }
                    }
                }
            },
            shortcutHelperCategory(
                R.string.shortcut_helper_category_a11y,
                Icons.Default.Accessibility
            ) {
                subCategory("Accessibility shortcuts") {
                    shortcut("View recent apps") {
                        command {
                            key(Icons.Default.KeyboardCommandKey)
                            key("Tab")
                        }
                    }
                    shortcut("All apps search") {
                        command { key(Icons.Default.KeyboardCommandKey) }
                    }
                }
            }
        )
}
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.keyboard.shortcut.ui.model

import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType

sealed interface ShortcutsUiState {

    data class Active(
        val shortcutCategories: List<ShortcutCategory>,
        val defaultSelectedCategory: ShortcutCategoryType,
    ) : ShortcutsUiState

    data object Inactive : ShortcutsUiState
}
+8 −0
Original line number Diff line number Diff line
@@ -26,12 +26,15 @@ import android.view.WindowInsets
import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.updatePadding
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.compose.theme.PlatformTheme
import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.res.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -78,7 +81,12 @@ constructor(
        requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
            setContent {
                PlatformTheme {
                    val shortcutsUiState by
                        viewModel.shortcutsUiState.collectAsStateWithLifecycle(
                            initialValue = ShortcutsUiState.Inactive
                        )
                    ShortcutHelper(
                        shortcutsUiState = shortcutsUiState,
                        onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
                    )
                }
+15 −0
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.systemui.keyboard.shortcut.ui.viewmodel

import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -30,6 +32,7 @@ class ShortcutHelperViewModel
constructor(
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val stateInteractor: ShortcutHelperStateInteractor,
    categoriesInteractor: ShortcutHelperCategoriesInteractor,
) {

    val shouldShow =
@@ -38,6 +41,18 @@ constructor(
            .distinctUntilChanged()
            .flowOn(backgroundDispatcher)

    val shortcutsUiState =
        categoriesInteractor.shortcutCategories.map {
            if (it.isEmpty()) {
                ShortcutsUiState.Inactive
            } else {
                ShortcutsUiState.Active(
                    shortcutCategories = it,
                    defaultSelectedCategory = it.first().type,
                )
            }
        }

    fun onViewClosed() {
        stateInteractor.onViewClosed()
    }
Loading