Shortcut helper now shows correct application launch shortcuts.

Prior to this change, shortcut helper would build a list of application
launch shortuts based on the content of aosp bookmarks.xml. This list
would then be incorrect on devices with their own version of the file.

With this change, we request the shortcuts from system server after it
has parsed bookmarks.xml, so the set of shortcuts is complete and
correct.

Bug: 312452252
Flag: com.android.systemui.fetch_bookmarks_xml_keyboard_shortcuts
Test: atest KeyboardShortcutsTest KeyboardShortcutListSearchTest ModifierShortcutManagerTests ModifierShortcutTests
Change-Id: I4a3d34f93b0be12e5c48f5f8b3938c3e9f1618de
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index cb5a885..e5be531 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -53,6 +53,7 @@
 import android.view.IWindow;
 import android.view.IWindowSession;
 import android.view.IWindowSessionCallback;
+import android.view.KeyboardShortcutGroup;
 import android.view.KeyEvent;
 import android.view.InputEvent;
 import android.view.InsetsState;
@@ -1095,4 +1096,11 @@
 
     boolean transferTouchGesture(in InputTransferToken transferFromToken,
             in InputTransferToken transferToToken);
+
+    /**
+     * Request the application launch keyboard shortcuts the system has defined.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will handle the shortcut.
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
 }
diff --git a/core/java/android/view/KeyboardShortcutGroup.aidl b/core/java/android/view/KeyboardShortcutGroup.aidl
new file mode 100644
index 0000000..6f219db
--- /dev/null
+++ b/core/java/android/view/KeyboardShortcutGroup.aidl
@@ -0,0 +1,3 @@
+package android.view;
+
+@JavaOnlyStableParcelable parcelable KeyboardShortcutGroup;
diff --git a/core/java/android/view/KeyboardShortcutInfo.java b/core/java/android/view/KeyboardShortcutInfo.java
index 3f6fd64..3f49bf3 100644
--- a/core/java/android/view/KeyboardShortcutInfo.java
+++ b/core/java/android/view/KeyboardShortcutInfo.java
@@ -81,12 +81,29 @@
      *     {@link KeyEvent#META_SYM_ON}.
      */
     public KeyboardShortcutInfo(CharSequence label, char baseCharacter, int modifiers) {
+        this(label, null, baseCharacter, modifiers);
+    }
+
+    /**
+     * @param label The label that identifies the action performed by this shortcut.
+     * @param icon An icon that identifies the action performed by this shortcut.
+     * @param baseCharacter The character that triggers the shortcut.
+     * @param modifiers The set of modifiers that, combined with the key, trigger the shortcut.
+     *     These should be a combination of {@link KeyEvent#META_CTRL_ON},
+     *     {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_META_ON},
+     *     {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_FUNCTION_ON} and
+     *     {@link KeyEvent#META_SYM_ON}.
+     *
+     * @hide
+     */
+    public KeyboardShortcutInfo(
+            CharSequence label, @Nullable Icon icon, char baseCharacter, int modifiers) {
         mLabel = label;
         checkArgument(baseCharacter != MIN_VALUE);
         mBaseCharacter = baseCharacter;
         mKeycode = KeyEvent.KEYCODE_UNKNOWN;
         mModifiers = modifiers;
-        mIcon = null;
+        mIcon = icon;
     }
 
     private KeyboardShortcutInfo(Parcel source) {
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 18006bb..14978ed 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1668,6 +1668,15 @@
     public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId);
 
     /**
+     * Request the application launch keyboard shortcuts the system has defined.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will handle the shortcut.
+     *
+     * @hide
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
+
+    /**
      * Request for ime's keyboard shortcuts to be retrieved asynchronously.
      *
      * @param receiver The callback to be triggered when the result is ready.
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index b667427..330e46a 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -237,6 +237,16 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        try {
+            return WindowManagerGlobal.getWindowManagerService()
+                    .getApplicationLaunchKeyboardShortcuts(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
     public void requestImeKeyboardShortcuts(
             final KeyboardShortcutsReceiver receiver, int deviceId) {
         IResultReceiver resultReceiver = new IResultReceiver.Stub() {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6b71f97..46b15416 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6534,4 +6534,23 @@
     <string name="bg_user_sound_notification_button_mute">Mute</string>
     <!-- Notification text to mute the sound from the background user [CHAR LIMIT=NOTIF_BODY]-->
     <string name="bg_user_sound_notification_message">Tap to mute sound</string>
+
+    <!-- User visible title for the keyboard shortcut that takes the user to the browser app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_browser">Browser</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the contacts app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_contacts">Contacts</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the email app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_email">Email</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the SMS messaging app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_sms">SMS</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the music app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_music">Music</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the calendar app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_calendar">Calendar</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the calculator app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_calculator">Calculator</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the maps app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_maps">Maps</string>
+    <!-- User visible title for the keyboard shortcut group containing system-wide application launch shortcuts. [CHAR-LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications">Applications</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d25f59d..c50b961 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5561,4 +5561,15 @@
   <java-symbol type="string" name="bg_user_sound_notification_button_switch_user" />
   <java-symbol type="string" name="bg_user_sound_notification_button_mute" />
   <java-symbol type="string" name="bg_user_sound_notification_message" />
+
+  <!-- Keyboard Shortcut default category names. -->
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_browser" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_calculator" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_calendar" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_contacts" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_email" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_maps" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_music" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_sms" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications" />
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 5bb2936..c997ac5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -19,6 +19,7 @@
 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
 
+import static com.android.systemui.Flags.fetchBookmarksXmlKeyboardShortcuts;
 import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
 
 import android.annotation.NonNull;
@@ -149,7 +150,7 @@
     private KeyCharacterMap mBackupKeyCharacterMap;
 
     @VisibleForTesting
-    KeyboardShortcutListSearch(Context context, WindowManager windowManager) {
+    KeyboardShortcutListSearch(Context context, WindowManager windowManager, int deviceId) {
         this.mContext = new ContextThemeWrapper(
                 context, R.style.KeyboardShortcutHelper);
         this.mPackageManager = AppGlobals.getPackageManager();
@@ -159,12 +160,12 @@
             this.mWindowManager = mContext.getSystemService(WindowManager.class);
         }
         loadResources(this.mContext);
-        createHardcodedShortcuts();
+        createHardcodedShortcuts(deviceId);
     }
 
-    private static KeyboardShortcutListSearch getInstance(Context context) {
+    private static KeyboardShortcutListSearch getInstance(Context context, int deviceId) {
         if (sInstance == null) {
-            sInstance = new KeyboardShortcutListSearch(context, null);
+            sInstance = new KeyboardShortcutListSearch(context, null, deviceId);
         }
         return sInstance;
     }
@@ -176,7 +177,7 @@
             if (sInstance != null && !sInstance.mContext.equals(context)) {
                 dismiss();
             }
-            getInstance(context).showKeyboardShortcuts(deviceId);
+            getInstance(context, deviceId).showKeyboardShortcuts(deviceId);
         }
     }
 
@@ -367,7 +368,7 @@
                 KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
     }
 
-    private void createHardcodedShortcuts() {
+    private void createHardcodedShortcuts(int deviceId) {
         // Add system shortcuts
         mKeySearchResultMap.put(SHORTCUT_SYSTEM_INDEX, true);
         mSystemGroup.add(getMultiMappingSystemShortcuts(mContext));
@@ -377,7 +378,7 @@
         mInputGroup.add(getMultiMappingInputShortcuts(mContext));
         // Add open apps shortcuts
         final List<KeyboardShortcutMultiMappingGroup> appShortcuts =
-                Arrays.asList(getDefaultMultiMappingApplicationShortcuts());
+                Arrays.asList(getDefaultMultiMappingApplicationShortcuts(deviceId));
         if (appShortcuts != null && !appShortcuts.isEmpty()) {
             mOpenAppsGroup = appShortcuts;
             mKeySearchResultMap.put(SHORTCUT_OPENAPPS_INDEX, true);
@@ -739,35 +740,50 @@
                 shortcutMultiMappingInfoList);
     }
 
-    private KeyboardShortcutMultiMappingGroup getDefaultMultiMappingApplicationShortcuts() {
-        final int userId = mContext.getUserId();
-        PackageInfo assistPackageInfo = getAssistPackageInfo(mContext, mPackageManager, userId);
-        CharSequence categoryTitle =
-                mContext.getString(R.string.keyboard_shortcut_group_applications);
+    private KeyboardShortcutMultiMappingGroup getDefaultMultiMappingApplicationShortcuts(
+            int deviceId) {
         List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
+        CharSequence categoryTitle;
+        if (fetchBookmarksXmlKeyboardShortcuts()) {
+            KeyboardShortcutGroup apps =
+                    mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+            List<KeyboardShortcutMultiMappingGroup> shortcuts =
+                    reMapToKeyboardShortcutMultiMappingGroup(Arrays.asList(apps));
+            for (KeyboardShortcutMultiMappingGroup group : shortcuts) {
+                for (ShortcutMultiMappingInfo keyboardShortcutInfo : group.getItems()) {
+                    shortcutMultiMappingInfos.add(keyboardShortcutInfo);
+                }
+            }
+            categoryTitle = apps.getLabel();
+        } else {
+            // Show shortcuts based on AOSP bookmarks.xml
+            categoryTitle = mContext.getString(R.string.keyboard_shortcut_group_applications);
+            final int userId = mContext.getUserId();
+            PackageInfo assistPackageInfo =
+                    getAssistPackageInfo(mContext, mPackageManager, userId);
 
-        String[] intentCategories = {
-                Intent.CATEGORY_APP_BROWSER,
-                Intent.CATEGORY_APP_CONTACTS,
-                Intent.CATEGORY_APP_EMAIL,
-                Intent.CATEGORY_APP_CALENDAR,
-                Intent.CATEGORY_APP_MAPS,
-                Intent.CATEGORY_APP_MUSIC,
-                Intent.CATEGORY_APP_MESSAGING,
-                Intent.CATEGORY_APP_CALCULATOR,
+            String[] intentCategories = {
+                    Intent.CATEGORY_APP_BROWSER,
+                    Intent.CATEGORY_APP_CONTACTS,
+                    Intent.CATEGORY_APP_EMAIL,
+                    Intent.CATEGORY_APP_CALENDAR,
+                    Intent.CATEGORY_APP_MAPS,
+                    Intent.CATEGORY_APP_MUSIC,
+                    Intent.CATEGORY_APP_MESSAGING,
+                    Intent.CATEGORY_APP_CALCULATOR,
+            };
+            String[] shortcutLabels = {
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_calculator)
+            };
 
-        };
-        String[] shortcutLabels = {
-                mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_email),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_music),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_calculator)
-        };
-        int[] keyCodes = {
+            int[] keyCodes = {
                 KeyEvent.KEYCODE_B,
                 KeyEvent.KEYCODE_C,
                 KeyEvent.KEYCODE_E,
@@ -776,52 +792,44 @@
                 KeyEvent.KEYCODE_P,
                 KeyEvent.KEYCODE_S,
                 KeyEvent.KEYCODE_U,
-        };
+            };
 
-        // Assist.
-        if (assistPackageInfo != null) {
+            // Assist.
             if (assistPackageInfo != null) {
-                final Icon assistIcon = Icon.createWithResource(
-                        assistPackageInfo.applicationInfo.packageName,
-                        assistPackageInfo.applicationInfo.icon);
-                CharSequence assistLabel =
-                        mContext.getString(R.string.keyboard_shortcut_group_applications_assist);
-                KeyboardShortcutInfo assistShortcutInfo = new KeyboardShortcutInfo(
-                        assistLabel,
-                        assistIcon,
-                        KeyEvent.KEYCODE_A,
-                        KeyEvent.META_META_ON);
-                shortcutMultiMappingInfos.add(
-                        new ShortcutMultiMappingInfo(
-                                assistLabel,
-                                assistIcon,
-                                Arrays.asList(new ShortcutKeyGroup(assistShortcutInfo, null))));
+                if (assistPackageInfo != null) {
+                    final Icon assistIcon = Icon.createWithResource(
+                            assistPackageInfo.applicationInfo.packageName,
+                            assistPackageInfo.applicationInfo.icon);
+                    CharSequence assistLabel = mContext.getString(
+                            R.string.keyboard_shortcut_group_applications_assist);
+                    KeyboardShortcutInfo assistShortcutInfo = new KeyboardShortcutInfo(
+                            assistLabel,
+                            assistIcon,
+                            KeyEvent.KEYCODE_A,
+                            KeyEvent.META_META_ON);
+                    shortcutMultiMappingInfos.add(
+                            new ShortcutMultiMappingInfo(
+                                    assistLabel,
+                                    assistIcon,
+                                    Arrays.asList(new ShortcutKeyGroup(assistShortcutInfo, null))));
+                }
             }
-        }
 
-        // Browser (Chrome as default): Meta + B
-        // Contacts: Meta + C
-        // Email (Gmail as default): Meta + E
-        // Gmail: Meta + G
-        // Calendar: Meta + K
-        // Maps: Meta + M
-        // Music: Meta + P
-        // SMS: Meta + S
-        // Calculator: Meta + U
-        for (int i = 0; i < shortcutLabels.length; i++) {
-            final Icon icon = getIconForIntentCategory(intentCategories[i], userId);
-            if (icon != null) {
-                CharSequence label =
-                        shortcutLabels[i];
-                KeyboardShortcutInfo keyboardShortcutInfo = new KeyboardShortcutInfo(
-                        label,
-                        icon,
-                        keyCodes[i],
-                        KeyEvent.META_META_ON);
-                List<ShortcutKeyGroup> shortcutKeyGroups =
-                        Arrays.asList(new ShortcutKeyGroup(keyboardShortcutInfo, null));
-                shortcutMultiMappingInfos.add(
-                        new ShortcutMultiMappingInfo(label, icon, shortcutKeyGroups));
+            for (int i = 0; i < shortcutLabels.length; i++) {
+                final Icon icon = getIconForIntentCategory(intentCategories[i], userId);
+                if (icon != null) {
+                    CharSequence label =
+                            shortcutLabels[i];
+                    KeyboardShortcutInfo keyboardShortcutInfo = new KeyboardShortcutInfo(
+                            label,
+                            icon,
+                            keyCodes[i],
+                            KeyEvent.META_META_ON);
+                    List<ShortcutKeyGroup> shortcutKeyGroups =
+                            Arrays.asList(new ShortcutKeyGroup(keyboardShortcutInfo, null));
+                    shortcutMultiMappingInfos.add(
+                            new ShortcutMultiMappingInfo(label, icon, shortcutKeyGroups));
+                }
             }
         }
 
@@ -1221,7 +1229,8 @@
         String shortcutKeyString = null;
         Drawable shortcutKeyDrawable = null;
         if (info.getBaseCharacter() > Character.MIN_VALUE) {
-            shortcutKeyString = String.valueOf(info.getBaseCharacter());
+            shortcutKeyString = String.valueOf(info.getBaseCharacter())
+                    .toUpperCase(Locale.getDefault());
         } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index a49ca38..da89eea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -20,6 +20,7 @@
 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
 
+import static com.android.systemui.Flags.fetchBookmarksXmlKeyboardShortcuts;
 import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
 
 import android.annotation.NonNull;
@@ -75,6 +76,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Contains functionality for handling keyboard shortcuts.
@@ -133,6 +135,7 @@
 
     @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null;
     @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null;
+    @Nullable private KeyboardShortcutGroup mDefaultApplicationShortcuts = null;
 
     @VisibleForTesting
     KeyboardShortcuts(Context context, WindowManager windowManager) {
@@ -390,6 +393,7 @@
         mReceivedAppShortcutGroups = null;
         mReceivedImeShortcutGroups = null;
 
+        mDefaultApplicationShortcuts = getDefaultApplicationShortcuts(deviceId);
         mWindowManager.requestAppKeyboardShortcuts(
                 result -> {
                     mBackgroundHandler.post(() -> {
@@ -443,10 +447,8 @@
         mReceivedAppShortcutGroups = null;
         mReceivedImeShortcutGroups = null;
 
-        final KeyboardShortcutGroup defaultAppShortcuts =
-                getDefaultApplicationShortcuts();
-        if (defaultAppShortcuts != null) {
-            shortcutGroups.add(defaultAppShortcuts);
+        if (mDefaultApplicationShortcuts != null) {
+            shortcutGroups.add(mDefaultApplicationShortcuts);
         }
         shortcutGroups.add(getSystemShortcuts());
         showKeyboardShortcutsDialog(shortcutGroups);
@@ -499,7 +501,7 @@
         return systemGroup;
     }
 
-    private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
+    private KeyboardShortcutGroup getDefaultApplicationShortcuts(int deviceId) {
         final int userId = mContext.getUserId();
         List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
 
@@ -529,65 +531,77 @@
             }
         }
 
-        // Browser.
-        final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
-        if (browserIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
-                    browserIcon,
-                    KeyEvent.KEYCODE_B,
-                    KeyEvent.META_META_ON));
-        }
+        CharSequence categoryTitle;
+        if (fetchBookmarksXmlKeyboardShortcuts()) {
+            KeyboardShortcutGroup apps =
+                    mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+            categoryTitle = apps.getLabel();
+            keyboardShortcutInfoAppItems.addAll(apps.getItems());
+        } else {
+            categoryTitle = mContext.getString(R.string.keyboard_shortcut_group_applications);
+            // Browser.
+            final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
+            if (browserIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                        browserIcon,
+                        KeyEvent.KEYCODE_B,
+                        KeyEvent.META_META_ON));
+            }
 
 
-        // Contacts.
-        final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
-        if (contactsIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
-                    contactsIcon,
-                    KeyEvent.KEYCODE_C,
-                    KeyEvent.META_META_ON));
-        }
+            // Contacts.
+            final Icon contactsIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_CONTACTS, userId);
+            if (contactsIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                        contactsIcon,
+                        KeyEvent.KEYCODE_C,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Email.
-        final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
-        if (emailIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
-                    emailIcon,
-                    KeyEvent.KEYCODE_E,
-                    KeyEvent.META_META_ON));
-        }
+            // Email.
+            final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
+            if (emailIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                        emailIcon,
+                        KeyEvent.KEYCODE_E,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Messaging.
-        final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
-        if (messagingIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
-                    messagingIcon,
-                    KeyEvent.KEYCODE_S,
-                    KeyEvent.META_META_ON));
-        }
+            // Messaging.
+            final Icon messagingIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_MESSAGING, userId);
+            if (messagingIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                        messagingIcon,
+                        KeyEvent.KEYCODE_S,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Music.
-        final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
-        if (musicIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
-                    musicIcon,
-                    KeyEvent.KEYCODE_P,
-                    KeyEvent.META_META_ON));
-        }
+            // Music.
+            final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
+            if (musicIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                        musicIcon,
+                        KeyEvent.KEYCODE_P,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Calendar.
-        final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
-        if (calendarIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
-                    calendarIcon,
-                    KeyEvent.KEYCODE_K,
-                    KeyEvent.META_META_ON));
+            // Calendar.
+            final Icon calendarIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_CALENDAR, userId);
+            if (calendarIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                        calendarIcon,
+                        KeyEvent.KEYCODE_K,
+                        KeyEvent.META_META_ON));
+            }
         }
 
         final int itemsSize = keyboardShortcutInfoAppItems.size();
@@ -598,7 +612,7 @@
         // Sorts by label, case insensitive with nulls and/or empty labels last.
         Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
         return new KeyboardShortcutGroup(
-                mContext.getString(R.string.keyboard_shortcut_group_applications),
+                categoryTitle,
                 keyboardShortcutInfoAppItems,
                 true);
     }
@@ -777,7 +791,8 @@
         String shortcutKeyString = null;
         Drawable shortcutKeyDrawable = null;
         if (info.getBaseCharacter() > Character.MIN_VALUE) {
-            shortcutKeyString = String.valueOf(info.getBaseCharacter());
+            shortcutKeyString = String.valueOf(info.getBaseCharacter())
+                    .toUpperCase(Locale.getDefault());
         } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
         } else {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index b23dfdc..8595178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -21,6 +21,7 @@
 import android.graphics.Region;
 import android.os.IBinder;
 import android.view.Display;
+import android.view.KeyboardShortcutGroup;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -55,6 +56,11 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+    }
+
+    @Override
     public Region getCurrentImeTouchRegion() {
         return mWindowManager.getCurrentImeTouchRegion();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
index 6985a27..63e56ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
@@ -66,7 +66,9 @@
 
     @Before
     public void setUp() {
-        mKeyboardShortcutListSearch = new KeyboardShortcutListSearch(mContext, mWindowManager);
+        when(mWindowManager.getApplicationLaunchKeyboardShortcuts(anyInt())).thenReturn(
+                new KeyboardShortcutGroup("", Collections.emptyList()));
+        mKeyboardShortcutListSearch = new KeyboardShortcutListSearch(mContext, mWindowManager, -1);
         mKeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
         mKeyboardShortcutListSearch.mKeyboardShortcutsBottomSheetDialog = mBottomSheetDialog;
         mKeyboardShortcutListSearch.mContext = mContext;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
index 6ad8b8b..105cf16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
@@ -54,6 +54,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 @SmallTest
@@ -71,6 +72,8 @@
 
     @Before
     public void setUp() {
+        when(mWindowManager.getApplicationLaunchKeyboardShortcuts(anyInt())).thenReturn(
+                new KeyboardShortcutGroup("", Collections.emptyList()));
         mKeyboardShortcuts = new KeyboardShortcuts(mContext, mWindowManager);
         KeyboardShortcuts.sInstance = mKeyboardShortcuts;
         mKeyboardShortcuts.mKeyboardShortcutsDialog = mDialog;
diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
index 3a79d0d..fde23b7 100644
--- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java
+++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
@@ -22,8 +22,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Icon;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -36,7 +38,11 @@
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.util.XmlUtils;
 import com.android.server.input.KeyboardMetricsCollector;
@@ -46,7 +52,9 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -183,8 +191,12 @@
                 String rolePackage = mRoleManager.getDefaultApplication(role);
                 if (rolePackage != null) {
                     intent = mPackageManager.getLaunchIntentForPackage(rolePackage);
-                    intent.putExtra(EXTRA_ROLE, role);
-                    mRoleIntents.put(role, intent);
+                    if (intent != null) {
+                        intent.putExtra(EXTRA_ROLE, role);
+                        mRoleIntents.put(role, intent);
+                    } else {
+                        Log.w(TAG, "No launch intent for role " + role);
+                    }
                 } else {
                     Log.w(TAG, "No default application for role " + role);
                 }
@@ -198,8 +210,7 @@
     private void loadShortcuts() {
 
         try {
-            XmlResourceParser parser = mContext.getResources().getXml(
-                    com.android.internal.R.xml.bookmarks);
+            XmlResourceParser parser = mContext.getResources().getXml(R.xml.bookmarks);
             XmlUtils.beginDocument(parser, TAG_BOOKMARKS);
 
             while (true) {
@@ -270,6 +281,9 @@
                         continue;
                     }
                     intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, categoryName);
+                    if (intent == null) {
+                        Log.w(TAG, "Null selector intent for " + categoryName);
+                    }
                 } else if (roleName != null) {
                     // We can't resolve the role at the time of this file being parsed as the
                     // device hasn't finished booting, so we will look it up lazily.
@@ -466,4 +480,131 @@
 
         return false;
     }
+
+    /**
+     * @param deviceId The input device id of the input device that will handle the shortcuts.
+     *
+     * @return a {@link KeyboardShortcutGroup} containing the application launch keyboard
+     *         shortcuts parsed at boot time from {@code bookmarks.xml}.
+     */
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        List<KeyboardShortcutInfo> shortcuts = new ArrayList();
+        for (int i = 0; i <  mIntentShortcuts.size(); i++) {
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mIntentShortcuts.keyAt(i)), mIntentShortcuts.valueAt(i), false);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mShiftShortcuts.size(); i++) {
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mShiftShortcuts.keyAt(i)), mShiftShortcuts.valueAt(i), true);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mRoleShortcuts.size(); i++) {
+            String role = mRoleShortcuts.valueAt(i);
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), false);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mShiftRoleShortcuts.size(); i++) {
+            String role = mShiftRoleShortcuts.valueAt(i);
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mShiftRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), true);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        return new KeyboardShortcutGroup(
+                mContext.getString(R.string.keyboard_shortcut_group_applications),
+                shortcuts);
+    }
+
+    /**
+     * Given an intent to launch an application and the character and shift state that should
+     * trigger it, return a suitable {@link KeyboardShortcutInfo} that contains the label and
+     * icon for the target application.
+     *
+     * @param baseChar the character that triggers the shortcut
+     * @param intent the application launch intent
+     * @param shift whether the shift key is required to be presed.
+     */
+    @VisibleForTesting
+    KeyboardShortcutInfo shortcutInfoFromIntent(char baseChar, Intent intent, boolean shift) {
+        if (intent == null) {
+            return null;
+        }
+
+        CharSequence label;
+        Icon icon;
+        ActivityInfo resolvedActivity = intent.resolveActivityInfo(
+                mPackageManager, PackageManager.MATCH_DEFAULT_ONLY);
+        if (resolvedActivity == null) {
+            return null;
+        }
+        boolean isResolver = com.android.internal.app.ResolverActivity.class.getName().equals(
+                resolvedActivity.name);
+        if (isResolver) {
+            label = getIntentCategoryLabel(mContext,
+                    intent.getSelector().getCategories().iterator().next());
+            if (label == null) {
+                return null;
+            }
+            icon = Icon.createWithResource(mContext, R.drawable.sym_def_app_icon);
+
+        } else {
+            label = resolvedActivity.loadLabel(mPackageManager);
+            icon = Icon.createWithResource(
+                    resolvedActivity.packageName, resolvedActivity.getIconResource());
+        }
+        int modifiers = KeyEvent.META_META_ON;
+        if (shift) {
+            modifiers |= KeyEvent.META_SHIFT_ON;
+        }
+        return new KeyboardShortcutInfo(label, icon, baseChar, modifiers);
+    }
+
+    @VisibleForTesting
+    static String getIntentCategoryLabel(Context context, CharSequence category) {
+        int resid;
+        switch (category.toString()) {
+            case Intent.CATEGORY_APP_BROWSER:
+                resid = R.string.keyboard_shortcut_group_applications_browser;
+                break;
+            case Intent.CATEGORY_APP_CONTACTS:
+                resid = R.string.keyboard_shortcut_group_applications_contacts;
+                break;
+            case Intent.CATEGORY_APP_EMAIL:
+                resid = R.string.keyboard_shortcut_group_applications_email;
+                break;
+            case Intent.CATEGORY_APP_CALENDAR:
+                resid = R.string.keyboard_shortcut_group_applications_calendar;
+                break;
+            case Intent.CATEGORY_APP_MAPS:
+                resid = R.string.keyboard_shortcut_group_applications_maps;
+                break;
+            case Intent.CATEGORY_APP_MUSIC:
+                resid = R.string.keyboard_shortcut_group_applications_music;
+                break;
+            case Intent.CATEGORY_APP_MESSAGING:
+                resid = R.string.keyboard_shortcut_group_applications_sms;
+                break;
+            case Intent.CATEGORY_APP_CALCULATOR:
+                resid = R.string.keyboard_shortcut_group_applications_calculator;
+                break;
+            default:
+                Log.e(TAG, ("No label for app category " + category));
+                return null;
+        }
+        return context.getString(resid);
+    };
+
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9d0c0e9..8dc9756 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -189,6 +189,7 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyCharacterMap.FallbackAction;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -3321,6 +3322,11 @@
                 eventToLog).sendToTarget();
     }
 
+    @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+    }
+
     // TODO(b/117479243): handle it in InputPolicy
     // TODO (b/283241997): Add the remaining keyboard shortcut logging after refactoring
     /** {@inheritDoc} */
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 9ca4e27..6c05d70 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -82,6 +82,7 @@
 import android.view.Display;
 import android.view.IDisplayFoldListener;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowManagerPolicyConstants;
@@ -698,6 +699,15 @@
     public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags);
 
     /**
+     * Return the set of applicaition launch keyboard shortcuts the system supports.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will trigger the shortcut.
+     *
+     * @return {@link KeyboardShortcutGroup} containing the shortcuts.
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
+
+    /**
      * Called from the input reader thread before a motion is enqueued when the device is in a
      * non-interactive state.
      *
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8033122..700c069 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -283,6 +283,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.MagnificationSpec;
 import android.view.RemoteAnimationAdapter;
 import android.view.ScrollCaptureResponse;
@@ -334,8 +335,8 @@
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.internal.protolog.LegacyProtoLogImpl;
-import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FrameworkStatsLog;
@@ -7440,6 +7441,16 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            return mPolicy.getApplicationLaunchKeyboardShortcuts(deviceId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
     public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
         enforceRegisterWindowManagerListenersPermission("requestAppKeyboardShortcuts");
 
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 777f618..6e6b70d 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -52,6 +52,7 @@
     <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"/>
     <uses-permission android:name="android.permission.MANAGE_DEFAULT_APPLICATIONS"/>
     <uses-permission android:name="android.permission.DUMP"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
     <application android:debuggable="true"
diff --git a/services/tests/wmtests/res/xml/bookmarks.xml b/services/tests/wmtests/res/xml/bookmarks.xml
new file mode 100644
index 0000000..88419e9
--- /dev/null
+++ b/services/tests/wmtests/res/xml/bookmarks.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<bookmarks>
+    <bookmark
+        role="android.app.role.BROWSER"
+        shortcut="b" />
+    <bookmark
+        category="android.intent.category.APP_CONTACTS"
+        shortcut="c" />
+    <bookmark
+        category="android.intent.category.APP_EMAIL"
+        shortcut="e" />
+    <bookmark
+        category="android.intent.category.APP_CALENDAR"
+        shortcut="k" />
+    <bookmark
+        category="android.intent.category.APP_MAPS"
+        shortcut="m" />
+    <bookmark
+        category="android.intent.category.APP_MUSIC"
+        shortcut="p" />
+    <bookmark
+        role="android.app.role.SMS"
+        shortcut="s" />
+    <bookmark
+        category="android.intent.category.APP_CALCULATOR"
+        shortcut="u" />
+</bookmarks>
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
new file mode 100644
index 0000000..8c375d4
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
@@ -0,0 +1,167 @@
+/*
+ * 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.server.policy;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+/**
+ * Test class for {@link ModifierShortcutManager}.
+ *
+ * Build/Install/Run:
+ *  atest ModifierShortcutManagerTests
+ */
+
+@SmallTest
+public class ModifierShortcutManagerTests {
+    private ModifierShortcutManager mModifierShortcutManager;
+    private Handler mHandler;
+    private Context mContext;
+    private Resources mResources;
+
+    @Before
+    public void setUp() {
+        mHandler = new Handler(Looper.getMainLooper());
+        mContext = spy(getInstrumentation().getTargetContext());
+        mResources = spy(mContext.getResources());
+
+        XmlResourceParser testBookmarks = mResources.getXml(
+                com.android.frameworks.wmtests.R.xml.bookmarks);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks);
+
+        mModifierShortcutManager = new ModifierShortcutManager(mContext, mHandler);
+    }
+
+    @Test
+    public void test_getApplicationLaunchKeyboardShortcuts() {
+        KeyboardShortcutGroup group =
+                mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1);
+        assertEquals(8, group.getItems().size());
+    }
+
+    @Test
+    public void test_shortcutInfoFromIntent_appIntent() {
+        Intent mockIntent = mock(Intent.class);
+        ActivityInfo mockActivityInfo = mock(ActivityInfo.class);
+        when(mockActivityInfo.loadLabel(anyObject())).thenReturn("label");
+        mockActivityInfo.packageName = "android";
+        when(mockActivityInfo.getIconResource()).thenReturn(R.drawable.sym_def_app_icon);
+        when(mockIntent.resolveActivityInfo(anyObject(), anyInt())).thenReturn(mockActivityInfo);
+
+        KeyboardShortcutInfo info = mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, true);
+
+        assertEquals("label", info.getLabel().toString());
+        assertEquals('a', info.getBaseCharacter());
+        assertEquals(R.drawable.sym_def_app_icon, info.getIcon().getResId());
+        assertEquals(KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON, info.getModifiers());
+
+    }
+
+    @Test
+    public void test_shortcutInfoFromIntent_resolverIntent() {
+        Intent mockIntent = mock(Intent.class);
+        Intent mockSelector = mock(Intent.class);
+        ActivityInfo mockActivityInfo = mock(ActivityInfo.class);
+        mockActivityInfo.name = com.android.internal.app.ResolverActivity.class.getName();
+        when(mockIntent.resolveActivityInfo(anyObject(), anyInt())).thenReturn(mockActivityInfo);
+        when(mockIntent.getSelector()).thenReturn(mockSelector);
+        when(mockSelector.getCategories()).thenReturn(
+                Collections.singleton(Intent.CATEGORY_APP_BROWSER));
+
+        KeyboardShortcutInfo info = mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, false);
+
+        assertEquals(mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                info.getLabel().toString());
+        assertEquals('a', info.getBaseCharacter());
+        assertEquals(R.drawable.sym_def_app_icon, info.getIcon().getResId());
+        assertEquals(KeyEvent.META_META_ON, info.getModifiers());
+
+        // validate that an unknown category that we can't present a label to the user for
+        // returns null shortcut info.
+        when(mockSelector.getCategories()).thenReturn(
+                Collections.singleton("not_a_category"));
+        assertEquals(null,  mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, false));
+    }
+
+    @Test
+    public void test_getIntentCategoryLabel() {
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_BROWSER));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CONTACTS));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_EMAIL));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CALENDAR));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MAPS));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MUSIC));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MESSAGING));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_calculator),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CALCULATOR));
+        assertEquals(null, ModifierShortcutManager.getIntentCategoryLabel(mContext, "foo"));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 00a8842..38ad9a7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -27,6 +27,7 @@
 import android.os.PowerManager.WakeReason;
 import android.util.proto.ProtoOutputStream;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.WindowManager;
 import android.view.animation.Animation;
 
@@ -35,6 +36,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 
 import java.io.PrintWriter;
+import java.util.Collections;
 
 class TestWindowManagerPolicy implements WindowManagerPolicy {
 
@@ -362,4 +364,9 @@
     public boolean isGlobalKey(int keyCode) {
         return false;
     }
+
+    @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return new KeyboardShortcutGroup("", Collections.emptyList());
+    }
 }