Added UI for displaying existing custom shortcuts
+ Added button for deleting custom shortcuts commands if they exist for
a particular shortcut
Test: manual - ensure custom shortcuts UI corresponds with UX mocks
Test: ShortcutCustomizationViewModelTest
Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer
Bug: 373620793
Change-Id: Ifa76e379959fbf6fcf332d99a520acf156770a2b
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/Shortcut.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/Shortcut.kt
index 5f8570c..bf7df7e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/Shortcut.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/Shortcut.kt
@@ -20,7 +20,9 @@
val label: String,
val commands: List<ShortcutCommand>,
val icon: ShortcutIcon? = null,
-)
+) {
+ val containsCustomShortcutCommands: Boolean = commands.any { it.isCustom }
+}
class ShortcutBuilder(private val label: String) {
val commands = mutableListOf<ShortcutCommand>()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutInfo.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
similarity index 74%
rename from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutInfo.kt
rename to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
index e4ccc2c..203228b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
@@ -16,8 +16,10 @@
package com.android.systemui.keyboard.shortcut.shared.model
-data class ShortcutInfo(
- val label: String,
- val categoryType: ShortcutCategoryType,
- val subCategoryLabel: String,
-)
+sealed interface ShortcutCustomizationRequestInfo {
+ data class Add(
+ val label: String,
+ val categoryType: ShortcutCategoryType,
+ val subCategoryLabel: String,
+ ) : ShortcutCustomizationRequestInfo
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index 02e206e..e44bfe3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -24,7 +24,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutInfo
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.ui.composable.AssignNewShortcutDialog
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel
@@ -59,8 +59,8 @@
}
}
- fun onAddShortcutDialogRequested(shortcutBeingCustomized: ShortcutInfo) {
- viewModel.onAddShortcutDialogRequested(shortcutBeingCustomized)
+ fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
+ viewModel.onShortcutCustomizationRequested(requestInfo)
}
private fun createAddShortcutDialog(): Dialog {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
index 10a201e..fa03883 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
@@ -84,7 +84,7 @@
onKeyboardSettingsClicked = { onKeyboardSettingsClicked(dialog) },
onSearchQueryChanged = { shortcutHelperViewModel.onSearchQueryChanged(it) },
onCustomizationRequested = {
- shortcutCustomizationDialogStarter.onAddShortcutDialogRequested(it)
+ shortcutCustomizationDialogStarter.onShortcutCustomizationRequested(it)
},
)
dialog.setOnDismissListener { shortcutHelperViewModel.onViewClosed() }
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 13934ea..71734a2 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
@@ -43,6 +43,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -53,6 +54,7 @@
import androidx.compose.material.icons.Icons
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.Search
import androidx.compose.material.icons.filled.Tune
@@ -71,7 +73,6 @@
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -109,8 +110,8 @@
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.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutInfo
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.IconSource
@@ -125,7 +126,7 @@
modifier: Modifier = Modifier,
shortcutsUiState: ShortcutsUiState,
useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() },
- onCustomizationRequested: (ShortcutInfo) -> Unit = {},
+ onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
when (shortcutsUiState) {
is ShortcutsUiState.Active -> {
@@ -138,6 +139,7 @@
onCustomizationRequested,
)
}
+
else -> {
// No-op for now.
}
@@ -151,7 +153,7 @@
onSearchQueryChanged: (String) -> Unit,
modifier: Modifier,
onKeyboardSettingsClicked: () -> Unit,
- onCustomizationRequested: (ShortcutInfo) -> Unit = {},
+ onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
var selectedCategoryType by
remember(shortcutsUiState.defaultSelectedCategory) {
@@ -367,14 +369,10 @@
onCategorySelected: (ShortcutCategoryType?) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
isShortcutCustomizerFlagEnabled: Boolean,
- onCustomizationRequested: (ShortcutInfo) -> Unit = {},
+ onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
- var isCustomizeModeEntered by remember { mutableStateOf(false) }
- val isCustomizing by
- remember(isCustomizeModeEntered, isShortcutCustomizerFlagEnabled) {
- derivedStateOf { isCustomizeModeEntered && isCustomizeModeEntered }
- }
+ var isCustomizing by remember { mutableStateOf(false) }
Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@@ -383,10 +381,10 @@
}
Spacer(modifier = Modifier.weight(1f))
if (isShortcutCustomizerFlagEnabled) {
- if (isCustomizeModeEntered) {
- DoneButton(onClick = { isCustomizeModeEntered = false })
+ if (isCustomizing) {
+ DoneButton(onClick = { isCustomizing = false })
} else {
- CustomizeButton(onClick = { isCustomizeModeEntered = true })
+ CustomizeButton(onClick = { isCustomizing = true })
}
}
}
@@ -441,7 +439,7 @@
modifier: Modifier,
category: ShortcutCategoryUi?,
isCustomizing: Boolean,
- onCustomizationRequested: (ShortcutInfo) -> Unit = {},
+ onCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
val listState = rememberLazyListState()
LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
@@ -457,7 +455,7 @@
isCustomizing = isCustomizing,
onCustomizationRequested = { label, subCategoryLabel ->
onCustomizationRequested(
- ShortcutInfo(
+ ShortcutCustomizationRequestInfo.Add(
label = label,
subCategoryLabel = subCategoryLabel,
categoryType = category.type,
@@ -565,7 +563,7 @@
modifier = Modifier.weight(1f),
shortcut = shortcut,
isCustomizing = isCustomizing,
- onAddShortcutClicked = { onCustomizationRequested(shortcut.label) },
+ onAddShortcutRequested = { onCustomizationRequested(shortcut.label) },
)
}
}
@@ -594,42 +592,92 @@
modifier: Modifier = Modifier,
shortcut: ShortcutModel,
isCustomizing: Boolean = false,
- onAddShortcutClicked: () -> Unit = {},
+ onAddShortcutRequested: () -> Unit = {},
+ onDeleteShortcutRequested: () -> Unit = {},
) {
FlowRow(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
+ itemVerticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
shortcut.commands.forEachIndexed { index, command ->
if (index > 0) {
ShortcutOrSeparator(spacing = 16.dp)
}
- ShortcutCommand(command)
+ ShortcutCommandContainer(showBackground = command.isCustom) { ShortcutCommand(command) }
}
if (isCustomizing) {
Spacer(modifier = Modifier.width(16.dp))
- ShortcutHelperButton(
- modifier =
- Modifier.border(
- width = 1.dp,
- color = MaterialTheme.colorScheme.outline,
- shape = CircleShape,
- ),
- onClick = { onAddShortcutClicked() },
- color = Color.Transparent,
- width = 32.dp,
- height = 32.dp,
- iconSource = IconSource(imageVector = Icons.Default.Add),
- contentColor = MaterialTheme.colorScheme.primary,
- contentPaddingVertical = 0.dp,
- contentPaddingHorizontal = 0.dp,
- )
+ if (shortcut.containsCustomShortcutCommands) {
+ DeleteShortcutButton(onDeleteShortcutRequested)
+ } else {
+ AddShortcutButton(onAddShortcutRequested)
+ }
}
}
}
@Composable
+private fun AddShortcutButton(onClick: () -> Unit) {
+ ShortcutHelperButton(
+ modifier =
+ Modifier.border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outline,
+ shape = CircleShape,
+ ),
+ onClick = onClick,
+ color = Color.Transparent,
+ width = 32.dp,
+ height = 32.dp,
+ iconSource = IconSource(imageVector = Icons.Default.Add),
+ contentColor = MaterialTheme.colorScheme.primary,
+ contentPaddingVertical = 0.dp,
+ contentPaddingHorizontal = 0.dp,
+ )
+}
+
+@Composable
+private fun DeleteShortcutButton(onClick: () -> Unit) {
+ ShortcutHelperButton(
+ modifier =
+ Modifier.border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outline,
+ shape = CircleShape,
+ ),
+ onClick = onClick,
+ color = Color.Transparent,
+ width = 32.dp,
+ height = 32.dp,
+ iconSource = IconSource(imageVector = Icons.Default.DeleteOutline),
+ contentColor = MaterialTheme.colorScheme.primary,
+ contentPaddingVertical = 0.dp,
+ contentPaddingHorizontal = 0.dp,
+ )
+}
+
+@Composable
+private fun ShortcutCommandContainer(showBackground: Boolean, content: @Composable () -> Unit) {
+ if (showBackground) {
+ Box(
+ modifier =
+ Modifier.wrapContentSize()
+ .background(
+ color = MaterialTheme.colorScheme.outlineVariant,
+ shape = RoundedCornerShape(16.dp),
+ )
+ .padding(4.dp)
+ ) {
+ content()
+ }
+ } else {
+ content()
+ }
+}
+
+@Composable
private fun ShortcutCommand(command: ShortcutCommand) {
Row {
command.keys.forEachIndexed { keyIndex, key ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index b925387..e86da5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -19,7 +19,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.input.key.KeyEvent
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutInfo
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -30,32 +30,35 @@
class ShortcutCustomizationViewModel
@AssistedInject
constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor) {
- private val _shortcutBeingCustomized = mutableStateOf<ShortcutInfo?>(null)
+ private val _shortcutBeingCustomized = mutableStateOf<ShortcutCustomizationRequestInfo?>(null)
private val _shortcutCustomizationUiState =
MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)
val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow()
- fun onAddShortcutDialogRequested(shortcutBeingCustomized: ShortcutInfo) {
- _shortcutCustomizationUiState.value =
- ShortcutCustomizationUiState.AddShortcutDialog(
- shortcutLabel = shortcutBeingCustomized.label,
- shouldShowErrorMessage = false,
- isValidKeyCombination = false,
- defaultCustomShortcutModifierKey =
- shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(),
- isDialogShowing = false,
- )
-
- _shortcutBeingCustomized.value = shortcutBeingCustomized
+ fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
+ when (requestInfo) {
+ is ShortcutCustomizationRequestInfo.Add -> {
+ _shortcutCustomizationUiState.value =
+ ShortcutCustomizationUiState.AddShortcutDialog(
+ shortcutLabel = requestInfo.label,
+ shouldShowErrorMessage = false,
+ isValidKeyCombination = false,
+ defaultCustomShortcutModifierKey =
+ shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(),
+ isDialogShowing = false,
+ )
+ _shortcutBeingCustomized.value = requestInfo
+ }
+ }
}
fun onAddShortcutDialogShown() {
_shortcutCustomizationUiState.update { uiState ->
- (uiState as? ShortcutCustomizationUiState.AddShortcutDialog)
- ?.let { it.copy(isDialogShowing = true) }
- ?: uiState
+ (uiState as? ShortcutCustomizationUiState.AddShortcutDialog)?.copy(
+ isDialogShowing = true
+ ) ?: uiState
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
new file mode 100644
index 0000000..f8d8481
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import com.android.systemui.keyboard.shortcut.shortcutCustomizationViewModelFactory
+import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutCustomizationViewModelTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+ private val viewModel = kosmos.shortcutCustomizationViewModelFactory.create()
+
+ @Test
+ fun uiState_inactiveByDefault() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+
+ assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive)
+ }
+ }
+
+ @Test
+ fun uiState_correctlyUpdatedWhenAddShortcutCustomizationIsRequested() {
+ testScope.runTest {
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+
+ assertThat(uiState).isEqualTo(expectedStandardAddShortcutUiState)
+ }
+ }
+
+ @Test
+ fun uiState_consumedOnAddDialogShown() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+ viewModel.onAddShortcutDialogShown()
+
+ assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).isDialogShowing)
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun uiState_inactiveAfterDialogIsDismissed() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+ viewModel.onAddShortcutDialogShown()
+ viewModel.onAddShortcutDialogDismissed()
+ assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive)
+ }
+ }
+
+ private val standardAddShortcutRequest =
+ ShortcutCustomizationRequestInfo.Add(
+ label = "Standard shortcut",
+ categoryType = ShortcutCategoryType.System,
+ subCategoryLabel = "Standard subcategory",
+ )
+
+ private val expectedStandardAddShortcutUiState =
+ ShortcutCustomizationUiState.AddShortcutDialog(
+ shortcutLabel = "Standard shortcut",
+ shouldShowErrorMessage = false,
+ isValidKeyCombination = false,
+ defaultCustomShortcutModifierKey =
+ ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+ isDialogShowing = false,
+ )
+}