Merge "Added Dialog for Shortcut Customization" into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
index e9b7335..d58e1bf 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
@@ -98,19 +98,20 @@
theme: Int = SystemUIDialog.DEFAULT_THEME,
dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
@GravityInt dialogGravity: Int? = null,
+ dialogDelegate: DialogDelegate<SystemUIDialog> =
+ object : DialogDelegate<SystemUIDialog> {
+ override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ super.onCreate(dialog, savedInstanceState)
+ dialogGravity?.let { dialog.window?.setGravity(it) }
+ }
+ },
content: @Composable (SystemUIDialog) -> Unit,
): ComponentSystemUIDialog {
return create(
context = context,
theme = theme,
dismissOnDeviceLock = dismissOnDeviceLock,
- delegate =
- object : DialogDelegate<SystemUIDialog> {
- override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
- super.onCreate(dialog, savedInstanceState)
- dialogGravity?.let { dialog.window?.setGravity(it) }
- }
- },
+ delegate = dialogDelegate,
content = content,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
index d1431ee..1580ea5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.shortcutCustomizationDialogStarterFactory
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
@@ -71,7 +72,13 @@
private val starter: ShortcutHelperDialogStarter =
with(kosmos) {
- ShortcutHelperDialogStarter(coroutineScope, viewModel, dialogFactory, activityStarter)
+ ShortcutHelperDialogStarter(
+ coroutineScope,
+ viewModel,
+ shortcutCustomizationDialogStarterFactory,
+ dialogFactory,
+ activityStarter,
+ )
}
@Before
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 5a8417d..d30f73f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3743,6 +3743,11 @@
is a component that shows the user which keyboard shortcuts they can use.
[CHAR LIMIT=NONE] -->
<string name="shortcut_helper_customize_mode_title">Customize keyboard shortcuts</string>
+ <!-- Sub title at the top of the keyboard shortcut helper customization dialog. Explains to the
+ user what action they need to take in the customization dialog to assign a new custom shortcut.
+ The helper is a component that shows the user which keyboard shortcuts they can use.
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_customize_mode_sub_title">Press key to assign shortcut</string>
<!-- Placeholder text shown in the search box of the keyboard shortcut helper, when the user
hasn't typed in anything in the search box yet. The helper is a component that shows the
user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
@@ -3754,6 +3759,16 @@
use. The helper shows shortcuts in categories, which can be collapsed or expanded.
[CHAR LIMIT=NONE] -->
<string name="shortcut_helper_content_description_collapse_icon">Collapse icon</string>
+ <!-- Content description of the Meta key (also called Action Key) icon that prompts users to
+ press some key combination starting with meta key to assign new key combination to shortcut
+ in shortcut helper customization dialog. The helper is a component that shows the user
+ which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_content_description_meta_key">Action or Meta key icon</string>
+ <!-- Content description of the plus icon after the meta key icon prompts users to
+ press some key combination starting with meta key to assign new key combination to shortcut
+ in shortcut helper customization dialog. The helper is a component that shows the user
+ which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_content_description_plus_icon">Plus icon</string>
<!-- Description text of the button that allows user to customize shortcuts in keyboard
shortcut helper The helper is a component that shows the user which keyboard shortcuts
they can use. [CHAR LIMIT=NONE] -->
@@ -3784,6 +3799,24 @@
open keyboard settings while in shortcut helper. The helper is a component that shows the
user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
<string name="shortcut_helper_keyboard_settings_buttons_label">Keyboard Settings</string>
+ <!-- Label on the set shortcut button in keyboard shortcut helper customize dialog, that allows user to
+ confirm and assign key combination to selected shortcut. The helper is a component that
+ shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_customize_dialog_set_shortcut_button_label">Set shortcut</string>
+ <!-- Label on the cancel button in keyboard shortcut helper customize dialog, that allows user to
+ cancel and exit shortcut customization dialog, returning to the main shortcut helper page.
+ The helper is a component that shows the user which keyboard shortcuts they can use.
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_customize_dialog_cancel_button_label">Cancel</string>
+ <!-- Placeholder text, prompting user to Press key combination assign to shortcut. This is shown
+ in shortcut helper's "Add Custom Shortcut" Dialog text field when user hasn't pressed
+ any key yet. The helper is a component that shows the user which keyboard shortcuts
+ they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_add_shortcut_dialog_placeholder">Press key</string>
+ <!-- Error message displayed when the user select a key combination that is already in use while
+ assigning a new custom key combination to a shortcut in shortcut helper. The helper is a
+ component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_customize_dialog_error_message">Key combination already in use. Try another key.</string>
<!-- Keyboard touchpad tutorial scheduler-->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
new file mode 100644
index 0000000..85d2214
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.domain.interactor
+
+import android.view.KeyEvent.META_META_ON
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import javax.inject.Inject
+
+class ShortcutCustomizationInteractor @Inject constructor() {
+ fun getDefaultCustomShortcutModifierKey(): ShortcutKey.Icon.ResIdIcon {
+ return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.keyIcons[META_META_ON]!!)
+ }
+}
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/ShortcutInfo.kt
new file mode 100644
index 0000000..e4ccc2c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutInfo.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.shared.model
+
+data class ShortcutInfo(
+ val label: String,
+ val categoryType: ShortcutCategoryType,
+ val subCategoryLabel: String,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogDelegate.kt
new file mode 100644
index 0000000..c98472e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogDelegate.kt
@@ -0,0 +1,39 @@
+/*
+ * 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
+
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import com.android.systemui.statusbar.phone.DialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+class ShortcutCustomizationDialogDelegate : DialogDelegate<SystemUIDialog> {
+
+ override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ super.onCreate(dialog, savedInstanceState)
+ dialog.window?.apply { setGravity(Gravity.CENTER) }
+ }
+
+ override fun getWidth(dialog: SystemUIDialog): Int {
+ return WindowManager.LayoutParams.WRAP_CONTENT
+ }
+
+ override fun getHeight(dialog: SystemUIDialog): Int {
+ return WindowManager.LayoutParams.WRAP_CONTENT
+ }
+}
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
new file mode 100644
index 0000000..02e206e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -0,0 +1,84 @@
+/*
+ * 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
+
+import android.app.Dialog
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.runtime.getValue
+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.ui.composable.AssignNewShortcutDialog
+import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.create
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class ShortcutCustomizationDialogStarter
+@AssistedInject
+constructor(
+ viewModelFactory: ShortcutCustomizationViewModel.Factory,
+ private val dialogFactory: SystemUIDialogFactory,
+) : ExclusiveActivatable() {
+
+ private var dialog: Dialog? = null
+ private val viewModel = viewModelFactory.create()
+
+ override suspend fun onActivated(): Nothing {
+ viewModel.shortcutCustomizationUiState.collect { uiState ->
+ if (
+ uiState is ShortcutCustomizationUiState.AddShortcutDialog &&
+ !uiState.isDialogShowing
+ ) {
+ dialog = createAddShortcutDialog().also { it.show() }
+ viewModel.onAddShortcutDialogShown()
+ } else if (uiState is ShortcutCustomizationUiState.Inactive) {
+ dialog?.dismiss()
+ dialog = null
+ }
+ }
+ }
+
+ fun onAddShortcutDialogRequested(shortcutBeingCustomized: ShortcutInfo) {
+ viewModel.onAddShortcutDialogRequested(shortcutBeingCustomized)
+ }
+
+ private fun createAddShortcutDialog(): Dialog {
+ return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog
+ ->
+ val uiState by viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle()
+ AssignNewShortcutDialog(
+ uiState = uiState,
+ modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
+ onKeyPress = { viewModel.onKeyPressed(it) },
+ onCancel = { dialog.dismiss() },
+ )
+ dialog.setOnDismissListener { viewModel.onAddShortcutDialogDismissed() }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): ShortcutCustomizationDialogStarter
+ }
+}
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 d33ab2a..807c70b 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
@@ -24,6 +24,7 @@
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -47,15 +48,18 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val viewModel: ShortcutHelperViewModel,
+ private val shortcutHelperViewModel: ShortcutHelperViewModel,
+ shortcutCustomizationDialogStarterFactory: ShortcutCustomizationDialogStarter.Factory,
private val dialogFactory: SystemUIDialogFactory,
private val activityStarter: ActivityStarter,
) : CoreStartable {
@VisibleForTesting var dialog: Dialog? = null
+ private val shortcutCustomizationDialogStarter =
+ shortcutCustomizationDialogStarterFactory.create()
override fun start() {
- viewModel.shouldShow
+ shortcutHelperViewModel.shouldShow
.map { shouldShow ->
if (shouldShow) {
dialog = createShortcutHelperDialog().also { it.show() }
@@ -69,16 +73,21 @@
private fun createShortcutHelperDialog(): Dialog {
return dialogFactory.createBottomSheet(
content = { dialog ->
- val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
+ val shortcutsUiState by
+ shortcutHelperViewModel.shortcutsUiState.collectAsStateWithLifecycle()
+ LaunchedEffect(Unit) { shortcutCustomizationDialogStarter.activate() }
ShortcutHelper(
modifier = Modifier.width(getWidth()),
shortcutsUiState = shortcutsUiState,
onKeyboardSettingsClicked = { onKeyboardSettingsClicked(dialog) },
- onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
+ onSearchQueryChanged = { shortcutHelperViewModel.onSearchQueryChanged(it) },
+ onCustomizationRequested = {
+ shortcutCustomizationDialogStarter.onAddShortcutDialogRequested(it)
+ },
)
- dialog.setOnDismissListener { viewModel.onViewClosed() }
+ dialog.setOnDismissListener { shortcutHelperViewModel.onViewClosed() }
},
- maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape
+ maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
new file mode 100644
index 0000000..43f0f20
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -0,0 +1,272 @@
+/*
+ * 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.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.ErrorOutline
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
+import com.android.systemui.res.R
+
+@Composable
+fun AssignNewShortcutDialog(
+ uiState: ShortcutCustomizationUiState,
+ modifier: Modifier = Modifier,
+ onKeyPress: (KeyEvent) -> Boolean,
+ onCancel: () -> Unit,
+) {
+ if (uiState is ShortcutCustomizationUiState.AddShortcutDialog) {
+ Column(modifier = modifier) {
+ Title(
+ uiState.shortcutLabel,
+ modifier = Modifier.padding(horizontal = 24.dp).width(316.dp),
+ )
+ Description(
+ modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp).width(316.dp)
+ )
+ PromptShortcutModifier(
+ modifier =
+ Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp)
+ .width(131.dp)
+ .height(48.dp),
+ defaultModifierKey = uiState.defaultCustomShortcutModifierKey,
+ )
+ SelectedKeyCombinationContainer(
+ shouldShowErrorMessage = uiState.shouldShowErrorMessage,
+ onKeyPress = onKeyPress,
+ )
+ KeyCombinationAlreadyInUseErrorMessage(uiState.shouldShowErrorMessage)
+ DialogButtons(onCancel, isValidKeyCombination = uiState.isValidKeyCombination)
+ }
+ }
+}
+
+@Composable
+fun DialogButtons(onCancel: () -> Unit, isValidKeyCombination: Boolean) {
+ Row(
+ modifier =
+ Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
+ .sizeIn(minWidth = 316.dp, minHeight = 48.dp),
+ verticalAlignment = Alignment.Bottom,
+ horizontalArrangement = Arrangement.End,
+ ) {
+ ShortcutHelperButton(
+ shape = RoundedCornerShape(50.dp),
+ onClick = onCancel,
+ color = Color.Transparent,
+ width = 80.dp,
+ contentColor = MaterialTheme.colorScheme.primary,
+ text = stringResource(R.string.shortcut_helper_customize_dialog_cancel_button_label),
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ ShortcutHelperButton(
+ onClick = {},
+ color = MaterialTheme.colorScheme.primary,
+ width = 116.dp,
+ contentColor = MaterialTheme.colorScheme.onPrimary,
+ text =
+ stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
+ enabled = isValidKeyCombination,
+ )
+ }
+}
+
+@Composable
+fun KeyCombinationAlreadyInUseErrorMessage(shouldShowErrorMessage: Boolean) {
+ if (shouldShowErrorMessage) {
+ Box(modifier = Modifier.padding(horizontal = 16.dp).width(332.dp).height(40.dp)) {
+ Text(
+ text = stringResource(R.string.shortcut_helper_customize_dialog_error_message),
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(start = 24.dp).width(252.dp),
+ )
+ }
+ }
+}
+
+@Composable
+fun SelectedKeyCombinationContainer(
+ keyCombination: String =
+ stringResource(R.string.shortcut_helper_add_shortcut_dialog_placeholder),
+ shouldShowErrorMessage: Boolean,
+ onKeyPress: (KeyEvent) -> Boolean,
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+ val outlineColor =
+ if (!isFocused) MaterialTheme.colorScheme.outline
+ else if (shouldShowErrorMessage) MaterialTheme.colorScheme.error
+ else MaterialTheme.colorScheme.primary
+
+ ClickableShortcutSurface(
+ onClick = {},
+ color = Color.Transparent,
+ shape = RoundedCornerShape(50.dp),
+ modifier =
+ Modifier.padding(all = 16.dp)
+ .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
+ .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
+ .onPreviewKeyEvent { onKeyPress(it) },
+ interactionSource = interactionSource,
+ ) {
+ Row(
+ modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = keyCombination,
+ style = MaterialTheme.typography.headlineSmall,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.width(252.dp),
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ if (shouldShowErrorMessage) {
+ Icon(
+ imageVector = Icons.Default.ErrorOutline,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.error,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Title(title: String, modifier: Modifier = Modifier) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineSmall,
+ fontSize = 24.sp,
+ modifier = modifier.wrapContentSize(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurface,
+ lineHeight = 32.sp,
+ )
+}
+
+@Composable
+private fun Description(modifier: Modifier = Modifier) {
+ Text(
+ text = stringResource(id = R.string.shortcut_helper_customize_mode_sub_title),
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ modifier = modifier.wrapContentSize(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+}
+
+@Composable
+private fun PromptShortcutModifier(
+ modifier: Modifier,
+ defaultModifierKey: ShortcutKey.Icon.ResIdIcon,
+) {
+ Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
+ ActionKeyContainer(defaultModifierKey)
+ PlusIconContainer()
+ }
+}
+
+@Composable
+private fun ActionKeyContainer(defaultModifierKey: ShortcutKey.Icon.ResIdIcon) {
+ Row(
+ modifier =
+ Modifier.height(48.dp)
+ .width(105.dp)
+ .background(
+ color = MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(16.dp),
+ )
+ .padding(all = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ ActionKeyIcon(defaultModifierKey)
+ ActionKeyText()
+ }
+}
+
+@Composable
+fun ActionKeyText() {
+ Text(
+ text = "Action",
+ style = MaterialTheme.typography.titleMedium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ modifier = Modifier.wrapContentSize(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+}
+
+@Composable
+private fun ActionKeyIcon(defaultModifierKey: ShortcutKey.Icon.ResIdIcon) {
+ Icon(
+ painter = painterResource(id = defaultModifierKey.drawableResId),
+ contentDescription = stringResource(R.string.shortcut_helper_content_description_meta_key),
+ modifier = Modifier.size(24.dp).wrapContentSize(Alignment.Center),
+ )
+}
+
+@Composable
+private fun PlusIconContainer() {
+ Icon(
+ tint = MaterialTheme.colorScheme.onSurface,
+ imageVector = Icons.Default.Add,
+ contentDescription =
+ stringResource(id = R.string.shortcut_helper_content_description_plus_icon),
+ modifier = Modifier.padding(vertical = 12.dp).size(24.dp).wrapContentSize(Alignment.Center),
+ )
+}
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 abddc70..13934ea 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
@@ -110,6 +110,7 @@
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.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
@@ -124,6 +125,7 @@
modifier: Modifier = Modifier,
shortcutsUiState: ShortcutsUiState,
useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() },
+ onCustomizationRequested: (ShortcutInfo) -> Unit = {},
) {
when (shortcutsUiState) {
is ShortcutsUiState.Active -> {
@@ -133,6 +135,7 @@
onSearchQueryChanged,
modifier,
onKeyboardSettingsClicked,
+ onCustomizationRequested,
)
}
else -> {
@@ -148,6 +151,7 @@
onSearchQueryChanged: (String) -> Unit,
modifier: Modifier,
onKeyboardSettingsClicked: () -> Unit,
+ onCustomizationRequested: (ShortcutInfo) -> Unit = {},
) {
var selectedCategoryType by
remember(shortcutsUiState.defaultSelectedCategory) {
@@ -173,6 +177,7 @@
onCategorySelected = { selectedCategoryType = it },
onKeyboardSettingsClicked,
shortcutsUiState.isShortcutCustomizerFlagEnabled,
+ onCustomizationRequested,
)
}
}
@@ -362,6 +367,7 @@
onCategorySelected: (ShortcutCategoryType?) -> Unit,
onKeyboardSettingsClicked: () -> Unit,
isShortcutCustomizerFlagEnabled: Boolean,
+ onCustomizationRequested: (ShortcutInfo) -> Unit = {},
) {
val selectedCategory = categories.fastFirstOrNull { it.type == selectedCategoryType }
var isCustomizeModeEntered by remember { mutableStateOf(false) }
@@ -400,6 +406,7 @@
Modifier.fillMaxSize().padding(top = 8.dp),
selectedCategory,
isCustomizing = isCustomizing,
+ onCustomizationRequested = onCustomizationRequested,
)
}
}
@@ -434,6 +441,7 @@
modifier: Modifier,
category: ShortcutCategoryUi?,
isCustomizing: Boolean,
+ onCustomizationRequested: (ShortcutInfo) -> Unit = {},
) {
val listState = rememberLazyListState()
LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
@@ -447,6 +455,15 @@
searchQuery = searchQuery,
subCategory = subcategory,
isCustomizing = isCustomizing,
+ onCustomizationRequested = { label, subCategoryLabel ->
+ onCustomizationRequested(
+ ShortcutInfo(
+ label = label,
+ subCategoryLabel = subCategoryLabel,
+ categoryType = category.type,
+ )
+ )
+ },
)
Spacer(modifier = Modifier.height(8.dp))
}
@@ -476,6 +493,7 @@
searchQuery: String,
subCategory: ShortcutSubCategory,
isCustomizing: Boolean,
+ onCustomizationRequested: (String, String) -> Unit = { _: String, _: String -> },
) {
Surface(
modifier = Modifier.fillMaxWidth(),
@@ -497,6 +515,7 @@
searchQuery = searchQuery,
shortcut = shortcut,
isCustomizing = isCustomizing,
+ onCustomizationRequested = { onCustomizationRequested(it, subCategory.label) },
)
}
}
@@ -518,6 +537,7 @@
searchQuery: String,
shortcut: ShortcutModel,
isCustomizing: Boolean = false,
+ onCustomizationRequested: (String) -> Unit = {},
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
@@ -541,7 +561,12 @@
ShortcutDescriptionText(searchQuery = searchQuery, shortcut = shortcut)
}
Spacer(modifier = Modifier.width(24.dp))
- ShortcutKeyCombinations(modifier = Modifier.weight(1f), shortcut = shortcut, isCustomizing)
+ ShortcutKeyCombinations(
+ modifier = Modifier.weight(1f),
+ shortcut = shortcut,
+ isCustomizing = isCustomizing,
+ onAddShortcutClicked = { onCustomizationRequested(shortcut.label) },
+ )
}
}
@@ -569,6 +594,7 @@
modifier: Modifier = Modifier,
shortcut: ShortcutModel,
isCustomizing: Boolean = false,
+ onAddShortcutClicked: () -> Unit = {},
) {
FlowRow(
modifier = modifier,
@@ -590,7 +616,7 @@
color = MaterialTheme.colorScheme.outline,
shape = CircleShape,
),
- onClick = {},
+ onClick = { onAddShortcutClicked() },
color = Color.Transparent,
width = 32.dp,
height = 32.dp,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
index 435968e..e761c73 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
@@ -44,6 +44,7 @@
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTonalElevationEnabled
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.minimumInteractiveComponentSize
@@ -283,6 +284,108 @@
}
@Composable
+fun ShortcutHelperButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ shape: Shape = RoundedCornerShape(360.dp),
+ color: Color,
+ width: Dp,
+ height: Dp = 40.dp,
+ iconSource: IconSource = IconSource(),
+ text: String? = null,
+ contentColor: Color,
+ contentPaddingHorizontal: Dp = 16.dp,
+ contentPaddingVertical: Dp = 10.dp,
+ enabled: Boolean = true,
+) {
+ ShortcutHelperButtonSurface(
+ onClick = onClick,
+ shape = shape,
+ color = color,
+ modifier = modifier,
+ enabled = enabled,
+ width = width,
+ height = height,
+ ) {
+ Row(
+ modifier =
+ Modifier.padding(
+ horizontal = contentPaddingHorizontal,
+ vertical = contentPaddingVertical,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ if (iconSource.imageVector != null) {
+ Icon(
+ tint = contentColor,
+ imageVector = iconSource.imageVector,
+ contentDescription =
+ null, // TODO this probably should not be null for accessibility.
+ modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center),
+ )
+ }
+
+ if (iconSource.imageVector != null && text != null)
+ Spacer(modifier = Modifier.weight(1f))
+
+ if (text != null) {
+ Text(
+ text,
+ color = contentColor,
+ fontSize = 14.sp,
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.wrapContentSize(Alignment.Center),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ShortcutHelperButtonSurface(
+ onClick: () -> Unit,
+ shape: Shape,
+ color: Color,
+ modifier: Modifier = Modifier,
+ enabled: Boolean,
+ width: Dp,
+ height: Dp,
+ content: @Composable () -> Unit,
+) {
+ if (enabled) {
+ ClickableShortcutSurface(
+ onClick = onClick,
+ shape = shape,
+ color = color,
+ modifier = modifier.semantics { role = Role.Button }.width(width).height(height),
+ interactionsConfig =
+ InteractionsConfig(
+ hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
+ hoverOverlayAlpha = 0.11f,
+ pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
+ pressedOverlayAlpha = 0.15f,
+ focusOutlineColor = MaterialTheme.colorScheme.secondary,
+ focusOutlineStrokeWidth = 3.dp,
+ focusOutlinePadding = 2.dp,
+ surfaceCornerRadius = 28.dp,
+ focusOutlineCornerRadius = 33.dp,
+ ),
+ ) {
+ content()
+ }
+ } else {
+ Surface(
+ shape = shape,
+ color = color.copy(0.38f),
+ modifier = modifier.semantics { role = Role.Button }.width(width).height(height),
+ ) {
+ content()
+ }
+ }
+}
+
+@Composable
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
return MaterialTheme.colorScheme.applyTonalElevation(color, elevation)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
new file mode 100644
index 0000000..e9f2a3b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.ShortcutKey
+
+sealed interface ShortcutCustomizationUiState {
+ data class AddShortcutDialog(
+ val shortcutLabel: String,
+ val shouldShowErrorMessage: Boolean,
+ val isValidKeyCombination: Boolean,
+ val defaultCustomShortcutModifierKey: ShortcutKey.Icon.ResIdIcon,
+ val isDialogShowing: Boolean,
+ ) : ShortcutCustomizationUiState
+
+ data object Inactive : ShortcutCustomizationUiState
+}
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
new file mode 100644
index 0000000..b925387
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.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.ui.model.ShortcutCustomizationUiState
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+class ShortcutCustomizationViewModel
+@AssistedInject
+constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor) {
+ private val _shortcutBeingCustomized = mutableStateOf<ShortcutInfo?>(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 onAddShortcutDialogShown() {
+ _shortcutCustomizationUiState.update { uiState ->
+ (uiState as? ShortcutCustomizationUiState.AddShortcutDialog)
+ ?.let { it.copy(isDialogShowing = true) }
+ ?: uiState
+ }
+ }
+
+ fun onAddShortcutDialogDismissed() {
+ _shortcutBeingCustomized.value = null
+ _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive
+ }
+
+ fun onKeyPressed(keyEvent: KeyEvent): Boolean {
+ // TODO Not yet implemented b/373638584
+ return false
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): ShortcutCustomizationViewModel
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index c41493e..8022e6e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -31,8 +31,11 @@
import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource
import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
+import com.android.systemui.keyboard.shortcut.ui.ShortcutCustomizationDialogStarter
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.kosmos.Kosmos
@@ -42,6 +45,7 @@
import com.android.systemui.model.sysUiState
import com.android.systemui.settings.displayTracker
import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
var Kosmos.shortcutHelperAppCategoriesShortcutsSource: KeyboardShortcutGroupsSource by
Kosmos.Fixture { AppCategoriesShortcutsSource(windowManager, testDispatcher) }
@@ -121,3 +125,26 @@
shortcutHelperCategoriesInteractor,
)
}
+
+val Kosmos.shortcutCustomizationDialogStarterFactory by
+ Kosmos.Fixture {
+ object : ShortcutCustomizationDialogStarter.Factory {
+ override fun create(): ShortcutCustomizationDialogStarter {
+ return ShortcutCustomizationDialogStarter(
+ shortcutCustomizationViewModelFactory,
+ systemUIDialogFactory,
+ )
+ }
+ }
+ }
+
+val Kosmos.shortcutCustomizationInteractor by Kosmos.Fixture { ShortcutCustomizationInteractor() }
+
+val Kosmos.shortcutCustomizationViewModelFactory by
+ Kosmos.Fixture {
+ object : ShortcutCustomizationViewModel.Factory {
+ override fun create(): ShortcutCustomizationViewModel {
+ return ShortcutCustomizationViewModel(shortcutCustomizationInteractor)
+ }
+ }
+ }