Shortcut Helper - Retrieve app categories shortcuts from WindowManager

Before, we were hard coding the shortcuts. Now there is a new API from
WindowManager to retrieve these shortcuts, which are defined in a
config file.

Fixes: 341045436
Test: Manual
Test: AppCategoriesShortcutsSourceTest.kt
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Change-Id: I2d3234129c37b777e4e18e213f7f9af0ee8d781a
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 49817b2..495e8f3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -233,7 +233,7 @@
             Log.wtf(TAG, "Unsupported modifiers remaining: $remainingModifiers")
             return null
         }
-        if (info.keycode != 0) {
+        if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) {
             keys += toShortcutKey(keyCharacterMap, info.keycode, info.baseCharacter) ?: return null
         }
         if (keys.isEmpty()) {
@@ -253,7 +253,7 @@
             return ShortcutKey.Icon(iconResId)
         }
         if (baseCharacter > Char.MIN_VALUE) {
-            return ShortcutKey.Text(baseCharacter.toString())
+            return ShortcutKey.Text(baseCharacter.uppercase())
         }
         val specialKeyLabel = ShortcutHelperKeys.specialKeyLabels[keyCode]
         if (specialKeyLabel != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
index d7cb7db..d6c6d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
@@ -16,92 +16,29 @@
 
 package com.android.systemui.keyboard.shortcut.data.source
 
-import android.content.Intent
-import android.content.res.Resources
-import android.view.KeyEvent
 import android.view.KeyboardShortcutGroup
-import android.view.KeyboardShortcutInfo
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.res.R
-import com.android.systemui.util.icons.AppCategoryIconProvider
+import android.view.WindowManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.extensions.copy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
 
 class AppCategoriesShortcutsSource
 @Inject
 constructor(
-    private val appCategoryIconProvider: AppCategoryIconProvider,
-    @Main private val resources: Resources,
+    private val windowManager: WindowManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : KeyboardShortcutGroupsSource {
 
-    override suspend fun shortcutGroups(deviceId: Int) =
-        listOf(
-            KeyboardShortcutGroup(
-                /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications),
-                /* items = */ shortcuts()
-            )
-        )
-
-    private suspend fun shortcuts(): List<KeyboardShortcutInfo> =
-        listOfNotNull(
-                assistantAppShortcutInfo(),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_BROWSER,
-                    R.string.keyboard_shortcut_group_applications_browser,
-                    KeyEvent.KEYCODE_B
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CONTACTS,
-                    R.string.keyboard_shortcut_group_applications_contacts,
-                    KeyEvent.KEYCODE_C
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_EMAIL,
-                    R.string.keyboard_shortcut_group_applications_email,
-                    KeyEvent.KEYCODE_E
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CALENDAR,
-                    R.string.keyboard_shortcut_group_applications_calendar,
-                    KeyEvent.KEYCODE_K
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MAPS,
-                    R.string.keyboard_shortcut_group_applications_maps,
-                    KeyEvent.KEYCODE_M
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MUSIC,
-                    R.string.keyboard_shortcut_group_applications_music,
-                    KeyEvent.KEYCODE_P
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MESSAGING,
-                    R.string.keyboard_shortcut_group_applications_sms,
-                    KeyEvent.KEYCODE_S
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CALCULATOR,
-                    R.string.keyboard_shortcut_group_applications_calculator,
-                    KeyEvent.KEYCODE_U
-                ),
-            )
-            .sortedBy { it.label!!.toString().lowercase() }
-
-    private suspend fun assistantAppShortcutInfo(): KeyboardShortcutInfo? {
-        val assistantIcon = appCategoryIconProvider.assistantAppIcon() ?: return null
-        return KeyboardShortcutInfo(
-            /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications_assist),
-            /* icon = */ assistantIcon,
-            /* keycode = */ KeyEvent.KEYCODE_A,
-            /* modifiers = */ KeyEvent.META_META_ON,
-        )
-    }
-
-    private suspend fun appCategoryShortcutInfo(category: String, labelResId: Int, keycode: Int) =
-        KeyboardShortcutInfo(
-            /* label = */ resources.getString(labelResId),
-            /* icon = */ appCategoryIconProvider.categoryAppIcon(category),
-            /* keycode = */ keycode,
-            /* modifiers = */ KeyEvent.META_META_ON,
-        )
+    override suspend fun shortcutGroups(deviceId: Int): List<KeyboardShortcutGroup> =
+        withContext(backgroundDispatcher) {
+            val group = windowManager.getApplicationLaunchKeyboardShortcuts(deviceId)
+            return@withContext if (group == null) {
+                emptyList()
+            } else {
+                val sortedShortcutItems = group.items.sortedBy { it.label!!.toString().lowercase() }
+                listOf(group.copy(items = sortedShortcutItems))
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.kt
new file mode 100644
index 0000000..3a120bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.extensions
+
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+
+fun KeyboardShortcutGroup.copy(
+    label: CharSequence = getLabel(),
+    items: List<KeyboardShortcutInfo> = getItems(),
+    isSystemGroup: Boolean = isSystemGroup(),
+    packageName: CharSequence? = getPackageName(),
+) = KeyboardShortcutGroup(label, items, isSystemGroup).also { it.packageName = packageName }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
index e49e2b49..5d59208 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
@@ -16,21 +16,17 @@
 
 package com.android.systemui.keyboard.shortcut.data.source
 
-import android.content.Intent.CATEGORY_APP_BROWSER
-import android.content.Intent.CATEGORY_APP_CALCULATOR
-import android.content.Intent.CATEGORY_APP_CALENDAR
-import android.content.Intent.CATEGORY_APP_CONTACTS
-import android.content.Intent.CATEGORY_APP_EMAIL
-import android.content.Intent.CATEGORY_APP_MAPS
-import android.content.Intent.CATEGORY_APP_MESSAGING
-import android.content.Intent.CATEGORY_APP_MUSIC
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import android.view.mockWindowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
-import com.android.systemui.util.icons.fakeAppCategoryIconProvider
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -43,185 +39,43 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val defaultAppIconsProvider = kosmos.fakeAppCategoryIconProvider
-    private val source = kosmos.shortcutHelperAppCategoriesShortcutsSource
+    private val mockWindowManager = kosmos.mockWindowManager
+    private val source =
+        AppCategoriesShortcutsSource(kosmos.mockWindowManager, kosmos.testDispatcher)
+
+    private var appCategoriesGroup: KeyboardShortcutGroup? = null
 
     @Before
     fun setUp() {
-        categoryApps.forEach { categoryAppIcon ->
-            defaultAppIconsProvider.installCategoryApp(
-                categoryAppIcon.category,
-                categoryAppIcon.packageName,
-                categoryAppIcon.iconResId
-            )
-        }
+        whenever(mockWindowManager.getApplicationLaunchKeyboardShortcuts(TEST_DEVICE_ID))
+            .thenAnswer { appCategoriesGroup }
     }
 
     @Test
-    fun shortcutGroups_returnsSingleGroup() =
-        testScope.runTest { assertThat(source.shortcutGroups(TEST_DEVICE_ID)).hasSize(1) }
+    fun shortcutGroups_nullResult_returnsEmptyList() =
+        testScope.runTest {
+            appCategoriesGroup = null
+
+            assertThat(source.shortcutGroups(TEST_DEVICE_ID)).isEmpty()
+        }
 
     @Test
-    fun shortcutGroups_hasAssistantIcon() =
+    fun shortcutGroups_returnsSortedList() =
         testScope.runTest {
-            defaultAppIconsProvider.installAssistantApp(ASSISTANT_PACKAGE, ASSISTANT_ICON_RES_ID)
+            val testItems =
+                listOf(
+                    KeyboardShortcutInfo("Info 2", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                    KeyboardShortcutInfo("Info 1", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                    KeyboardShortcutInfo("Info 3", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                )
+            appCategoriesGroup = KeyboardShortcutGroup("Test Group", testItems)
 
             val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Assistant" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(ASSISTANT_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(ASSISTANT_ICON_RES_ID)
+            val shortcutLabels = shortcuts.map { it.label.toString() }
+            assertThat(shortcutLabels).containsExactly("Info 1", "Info 2", "Info 3").inOrder()
         }
 
-    @Test
-    fun shortcutGroups_hasBrowserIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Browser" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(BROWSER_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(BROWSER_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasContactsIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Contacts" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CONTACTS_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CONTACTS_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasEmailIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Email" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(EMAIL_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(EMAIL_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasCalendarIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Calendar" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALENDAR_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALENDAR_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMapsIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Maps" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MAPS_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MAPS_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMessagingIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "SMS" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MESSAGING_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MESSAGING_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMusicIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Music" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MUSIC_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MUSIC_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasCalculatorIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Calculator" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALCULATOR_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALCULATOR_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_shortcutsSortedByLabelIgnoringCase() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutLabels = shortcuts.map { it.label!!.toString() }
-            assertThat(shortcutLabels).isEqualTo(shortcutLabels.sortedBy { it.lowercase() })
-        }
-
-    @Test
-    fun shortcutGroups_noAssistantApp_excludesAssistantFromShortcuts() =
-        testScope.runTest {
-            val shortcutLabels =
-                source.shortcutGroups(TEST_DEVICE_ID).first().items.map { it.label!!.toString() }
-
-            assertThat(shortcutLabels).doesNotContain("Assistant")
-        }
-
-    private companion object {
-        private const val ASSISTANT_PACKAGE = "the.assistant.app"
-        private const val ASSISTANT_ICON_RES_ID = 123
-
-        private const val BROWSER_PACKAGE = "com.test.browser"
-        private const val BROWSER_ICON_RES_ID = 1
-
-        private const val CONTACTS_PACKAGE = "app.test.contacts"
-        private const val CONTACTS_ICON_RES_ID = 234
-
-        private const val EMAIL_PACKAGE = "email.app.test"
-        private const val EMAIL_ICON_RES_ID = 351
-
-        private const val CALENDAR_PACKAGE = "app.test.calendar"
-        private const val CALENDAR_ICON_RES_ID = 411
-
-        private const val MAPS_PACKAGE = "maps.app.package"
-        private const val MAPS_ICON_RES_ID = 999
-
-        private const val MUSIC_PACKAGE = "com.android.music"
-        private const val MUSIC_ICON_RES_ID = 101
-
-        private const val MESSAGING_PACKAGE = "my.sms.app"
-        private const val MESSAGING_ICON_RES_ID = 9191
-
-        private const val CALCULATOR_PACKAGE = "that.calculator.app"
-        private const val CALCULATOR_ICON_RES_ID = 314
-
-        private val categoryApps =
-            listOf(
-                CategoryApp(CATEGORY_APP_BROWSER, BROWSER_PACKAGE, BROWSER_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CONTACTS, CONTACTS_PACKAGE, CONTACTS_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_EMAIL, EMAIL_PACKAGE, EMAIL_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CALENDAR, CALENDAR_PACKAGE, CALENDAR_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MAPS, MAPS_PACKAGE, MAPS_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MUSIC, MUSIC_PACKAGE, MUSIC_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MESSAGING, MESSAGING_PACKAGE, MESSAGING_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CALCULATOR, CALCULATOR_PACKAGE, CALCULATOR_ICON_RES_ID),
-            )
-
+    companion object {
         private const val TEST_DEVICE_ID = 123
     }
-
-    private class CategoryApp(val category: String, val packageName: String, val iconResId: Int)
 }
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
index 2a05598..d5451ee 100644
--- a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
@@ -19,4 +19,6 @@
 import com.android.systemui.kosmos.Kosmos
 import org.mockito.Mockito.mock
 
-val Kosmos.windowManager by Kosmos.Fixture<WindowManager> { mock(WindowManager::class.java) }
+val Kosmos.mockWindowManager: WindowManager by Kosmos.Fixture { mock(WindowManager::class.java) }
+
+var Kosmos.windowManager: WindowManager by Kosmos.Fixture { mockWindowManager }
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 001b55b..c423b62 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
@@ -41,13 +41,12 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
 import com.android.systemui.settings.displayTracker
-import com.android.systemui.util.icons.fakeAppCategoryIconProvider
 
 var Kosmos.shortcutHelperAppCategoriesShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture {
         AppCategoriesShortcutsSource(
-            fakeAppCategoryIconProvider,
-            mainResources,
+            windowManager,
+            testDispatcher,
         )
     }