Implement new shortcut search list UI
1. Add an EditText for shortcuts search purpose.
2. Add new shortcut categories.
3. Create N to 1 shortcut mapping mechanism.
4. Fn based shortcuts shouldn't be showed.
Demo New UI: go/shortcutlist_demo
Flag: SHORTCUT_LIST_SEARCH_LAYOUT
Bug: 259352579
Test: manual and atest
Change-Id: I688365d5c408ba07b457eec66b3b5cc497390498
diff --git a/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml
new file mode 100644
index 0000000..1b12e74
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2022 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
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M39.8,41.95 L26.65,28.8Q25.15,30.1 23.15,30.825Q21.15,31.55 18.9,31.55Q13.5,31.55 9.75,27.8Q6,24.05 6,18.75Q6,13.45 9.75,9.7Q13.5,5.95 18.85,5.95Q24.15,5.95 27.875,9.7Q31.6,13.45 31.6,18.75Q31.6,20.9 30.9,22.9Q30.2,24.9 28.8,26.65L42,39.75ZM18.85,28.55Q22.9,28.55 25.75,25.675Q28.6,22.8 28.6,18.75Q28.6,14.7 25.75,11.825Q22.9,8.95 18.85,8.95Q14.75,8.95 11.875,11.825Q9,14.7 9,18.75Q9,22.8 11.875,25.675Q14.75,28.55 18.85,28.55Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_shortcutlist_search_button_cancel.xml b/packages/SystemUI/res/drawable/ic_shortcutlist_search_button_cancel.xml
new file mode 100644
index 0000000..e3b1ab2
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_shortcutlist_search_button_cancel.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2022 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
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+ android:fillColor="@android:color/white"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/shortcut_button_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_colored.xml
new file mode 100644
index 0000000..a21489c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/shortcut_button_colored.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="16dp"/>
+ <solid android:color="?androidprv:attr/colorSurface"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml
new file mode 100644
index 0000000..2ff32b6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="16dp"/>
+ <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml
new file mode 100644
index 0000000..6ce3eae
--- /dev/null
+++ b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground"/>
+ <corners android:topLeftRadius="16dp"
+ android:topRightRadius="16dp"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"/>
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/shortcut_search_background.xml b/packages/SystemUI/res/drawable/shortcut_search_background.xml
new file mode 100644
index 0000000..66fc191
--- /dev/null
+++ b/packages/SystemUI/res/drawable/shortcut_search_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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
+ -->
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorSurface" />
+ <corners android:radius="24dp" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml
new file mode 100644
index 0000000..530e46e
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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
+ -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="0dp"
+ style="@style/ShortcutHorizontalDivider" />
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_icon_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_icon_view.xml
new file mode 100644
index 0000000..a037cb2
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_icon_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/ksh_item_padding"
+ android:layout_marginLeft="0dp"
+ android:layout_marginRight="0dp"
+ android:scaleType="fitXY"
+ android:tint="?android:attr/textColorPrimary"
+ style="@style/ShortcutItemBackground" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_view.xml
new file mode 100644
index 0000000..12b4e15
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_key_new_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/ksh_item_padding"
+ android:layout_marginStart="@dimen/ksh_item_margin_start"
+ style="@style/ShortcutItemBackground"
+ android:textColor="?android:attr/textColorPrimary"
+ android:singleLine="false"
+ android:gravity="center"
+ android:textSize="@dimen/ksh_item_text_size"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_key_plus_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_key_plus_view.xml
new file mode 100644
index 0000000..727f2c1
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_key_plus_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/ksh_item_padding"
+ android:layout_marginLeft="0dp"
+ android:layout_marginRight="0dp"
+ android:text="+"
+ style="@style/ShortcutItemBackground"
+ android:textColor="?android:attr/textColorPrimary"
+ android:singleLine="true"
+ android:gravity="center"
+ android:textSize="@dimen/ksh_item_text_size"
+ android:textAllCaps="true"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_key_vertical_bar_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_key_vertical_bar_view.xml
new file mode 100644
index 0000000..00ef947
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_key_vertical_bar_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/ksh_item_padding"
+ android:layout_marginLeft="0dp"
+ android:layout_marginRight="0dp"
+ android:text="|"
+ style="@style/ShortcutItemBackground"
+ android:textColor="?android:attr/textColorPrimary"
+ android:singleLine="true"
+ android:gravity="center"
+ android:textSize="@dimen/ksh_item_text_size"
+ android:textAllCaps="true"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
new file mode 100644
index 0000000..8a66f50
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="@drawable/shortcut_dialog_bg"
+ android:layout_width="@dimen/ksh_layout_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/shortcut_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="40dp"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorPrimary"
+ android:text="@string/keyboard_shortcut_search_list_title"/>
+
+ <FrameLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <EditText
+ android:id="@+id/keyboard_shortcuts_search"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginBottom="24dp"
+ android:layout_marginStart="49dp"
+ android:layout_marginEnd="49dp"
+ android:padding="16dp"
+ android:background="@drawable/shortcut_search_background"
+ android:drawableStart="@drawable/ic_shortcutlist_search"
+ android:drawablePadding="15dp"
+ android:singleLine="true"
+ android:textColor="?android:attr/textColorPrimary"
+ android:inputType="text"
+ android:textDirection="locale"
+ android:textAlignment="viewStart"
+ android:hint="@string/keyboard_shortcut_search_list_hint"
+ android:textColorHint="?android:attr/textColorTertiary" />
+
+ <ImageView
+ android:id="@+id/keyboard_shortcuts_search_cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:layout_marginTop="24dp"
+ android:layout_marginBottom="24dp"
+ android:layout_marginEnd="49dp"
+ android:padding="16dp"
+ android:contentDescription="@string/keyboard_shortcut_clear_text"
+ android:src="@drawable/ic_shortcutlist_search_button_cancel" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="37dp"/>
+
+ <Button
+ android:id="@+id/shortcut_system"
+ android:layout_width="120dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ style="@style/ShortCutButton"
+ android:text="@string/keyboard_shortcut_search_category_system"/>
+
+ <Button
+ android:id="@+id/shortcut_input"
+ android:layout_width="120dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ style="@style/ShortCutButton"
+ android:text="@string/keyboard_shortcut_search_category_input"/>
+
+ <Button
+ android:id="@+id/shortcut_open_apps"
+ android:layout_width="120dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ style="@style/ShortCutButton"
+ android:text="@string/keyboard_shortcut_search_category_open_apps"/>
+
+ <Button
+ android:id="@+id/shortcut_specific_app"
+ android:layout_width="120dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ style="@style/ShortCutButton"
+ android:text="@string/keyboard_shortcut_search_category_current_app"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/shortcut_search_no_result"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="50dp"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimary"
+ android:text="@string/keyboard_shortcut_search_list_no_result"/>
+
+ <ScrollView
+ android:id="@+id/keyboard_shortcuts_scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="25dp"
+ android:layout_marginEnd="25dp">
+ <LinearLayout
+ android:id="@+id/keyboard_shortcuts_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"/>
+ </ScrollView>
+ <!-- Required for stretching to full available height when the items in the scroll view
+ occupy less space then the full height -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+</LinearLayout>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 7360c79..d569279 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -118,6 +118,7 @@
<color name="ksh_application_group_color">#fff44336</color>
<color name="ksh_key_item_color">@color/material_grey_600</color>
<color name="ksh_key_item_background">@color/material_grey_100</color>
+ <color name="ksh_key_item_new_background">@color/transparent</color>
<color name="instant_apps_color">#ff4d5a64</color>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 1720357..251bf43 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1698,34 +1698,105 @@
<string name="keyboard_shortcut_group_system">System</string>
<!-- User visible title for the keyboard shortcut that takes the user to the home screen. -->
<string name="keyboard_shortcut_group_system_home">Home</string>
- <!-- User visible title for the the keyboard shortcut that takes the user to the recents screen. -->
+ <!-- User visible title for the keyboard shortcut that takes the user to the recents screen. -->
<string name="keyboard_shortcut_group_system_recents">Recents</string>
- <!-- User visible title for the the keyboard shortcut that triggers the back action. -->
+ <!-- User visible title for the keyboard shortcut that triggers the back action. -->
<string name="keyboard_shortcut_group_system_back">Back</string>
- <!-- User visible title for the the keyboard shortcut that triggers the notification shade. -->
+ <!-- User visible title for the keyboard shortcut that triggers the notification shade. -->
<string name="keyboard_shortcut_group_system_notifications">Notifications</string>
- <!-- User visible title for the the keyboard shortcut that triggers the keyboard shortcuts helper. -->
+ <!-- User visible title for the keyboard shortcut that triggers the keyboard shortcuts helper. -->
<string name="keyboard_shortcut_group_system_shortcuts_helper">Keyboard Shortcuts</string>
- <!-- User visible title for the the keyboard shortcut that switches to the next hardware keyboard layout. [CHAR LIMIT=30] -->
+ <!-- User visible title for the keyboard shortcut that switches to the next hardware keyboard layout. -->
<string name="keyboard_shortcut_group_system_switch_input">Switch keyboard layout</string>
- <!-- User visible title for the system-wide applications keyboard shortcuts list. -->
+ <!-- Content description for the clear text button in shortcut search list. [CHAR LIMIT=NONE] -->
+ <string name="keyboard_shortcut_clear_text">Clear text</string>
+ <!-- The title for keyboard shortcut search list [CHAR LIMIT=25] -->
+ <string name="keyboard_shortcut_search_list_title">Shortcuts</string>
+ <!-- The hint for keyboard shortcut search list [CHAR LIMIT=25] -->
+ <string name="keyboard_shortcut_search_list_hint">Search shortcuts</string>
+ <!-- The description for no shortcuts results [CHAR LIMIT=25] -->
+ <string name="keyboard_shortcut_search_list_no_result">No shortcuts found</string>
+ <!-- The title of system category in shortcut search list. [CHAR LIMIT=15] -->
+ <string name="keyboard_shortcut_search_category_system">System</string>
+ <!-- The title of input category in shortcut search list. [CHAR LIMIT=15] -->
+ <string name="keyboard_shortcut_search_category_input">Input</string>
+ <!-- The title of open apps category in shortcut search list. [CHAR LIMIT=15] -->
+ <string name="keyboard_shortcut_search_category_open_apps">Open apps</string>
+ <!-- The title of current app category in shortcut search list. [CHAR LIMIT=15] -->
+ <string name="keyboard_shortcut_search_category_current_app">Current app</string>
+
+ <!-- User visible title for the keyboard shortcut that triggers the notification shade. [CHAR LIMIT=70] -->
+ <string name="group_system_access_notification_shade">Access notification shade</string>
+ <!-- User visible title for the keyboard shortcut that takes a full screenshot. [CHAR LIMIT=70] -->
+ <string name="group_system_full_screenshot">Take a full screenshot</string>
+ <!-- User visible title for the keyboard shortcut that access list of system / apps shortcuts. [CHAR LIMIT=70] -->
+ <string name="group_system_access_system_app_shortcuts">Access list of system / apps shortcuts</string>
+ <!-- User visible title for the keyboard shortcut that goes back to previous state. [CHAR LIMIT=70] -->
+ <string name="group_system_go_back">Back: go back to previous state (back button)</string>
+ <!-- User visible title for the keyboard shortcut that takes the user to the home screen. [CHAR LIMIT=70] -->
+ <string name="group_system_access_home_screen">Access home screen</string>
+ <!-- User visible title for the keyboard shortcut that triggers overview of open apps. [CHAR LIMIT=70] -->
+ <string name="group_system_overview_open_apps">Overview of open apps</string>
+ <!-- User visible title for the keyboard shortcut that cycles through recent apps (forward). [CHAR LIMIT=70] -->
+ <string name="group_system_cycle_forward">Cycle through recent apps (forward)</string>
+ <!-- User visible title for the keyboard shortcut that cycles through recent apps (back). [CHAR LIMIT=70] -->
+ <string name="group_system_cycle_back">Cycle through recent apps (back)</string>
+ <!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
+ <string name="group_system_access_all_apps_search">Access list of all apps and search (i.e. Search/Launcher)</string>
+ <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
+ <string name="group_system_hide_reshow_taskbar">Hide and (re)show taskbar</string>
+ <!-- User visible title for the keyboard shortcut that accesses system settings. [CHAR LIMIT=70] -->
+ <string name="group_system_access_system_settings">Access system settings</string>
+ <!-- User visible title for the keyboard shortcut that accesses Google Assistant. [CHAR LIMIT=70] -->
+ <string name="group_system_access_google_assistant">Access Google Assistant</string>
+ <!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] -->
+ <string name="group_system_lock_screen">Lock screen</string>
+ <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
+ <string name="group_system_quick_memo">Pull up Notes app for quick memo</string>
+
+ <!-- User visible title for the system multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
+ <string name="keyboard_shortcut_group_system_multitasking">System multitasking</string>
+ <!-- User visible title for the keyboard shortcut that enters split screen with current app to RHS [CHAR LIMIT=70] -->
+ <string name="system_multitasking_rhs">Enter Split screen with current app to RHS</string>
+ <!-- User visible title for the keyboard shortcut that enters split screen with current app to LHS [CHAR LIMIT=70] -->
+ <string name="system_multitasking_lhs">Enter Split screen with current app to LHS</string>
+ <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
+ <string name="system_multitasking_full_screen">Switch from Split screen to full screen</string>
+ <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
+ <string name="system_multitasking_replace">During Split screen: replace an app from one to another</string>
+
+ <!-- User visible title for the input keyboard shortcuts list. [CHAR LIMIT=70] -->
+ <string name="keyboard_shortcut_group_input">Input</string>
+ <!-- User visible title for the keyboard shortcut that switches input language (next language). [CHAR LIMIT=70] -->
+ <string name="input_switch_input_language_next">Switch input language (next language)</string>
+ <!-- User visible title for the keyboard shortcut that switches input language (previous language). [CHAR LIMIT=70] -->
+ <string name="input_switch_input_language_previous">Switch input language (previous language)</string>
+ <!-- User visible title for the keyboard shortcut that accesses emoji. [CHAR LIMIT=70] -->
+ <string name="input_access_emoji">Access emoji</string>
+ <!-- User visible title for the keyboard shortcut that accesses voice typing. [CHAR LIMIT=70] -->
+ <string name="input_access_voice_typing">Access voice typing</string>
+
+ <!-- User visible title for the system-wide applications keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_applications">Applications</string>
- <!-- User visible title for the keyboard shortcut that takes the user to the assist app. -->
+ <!-- User visible title for the keyboard shortcut that takes the user to the assist app. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_applications_assist">Assist</string>
- <!-- User visible title for the keyboard shortcut that takes the user to the browser app. -->
- <string name="keyboard_shortcut_group_applications_browser">Browser</string>
- <!-- User visible title for the keyboard shortcut that takes the user to the contacts app. -->
+ <!-- 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 (Chrome as default)</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. -->
- <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. -->
+ <!-- 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 (Gmail as default)</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. -->
+ <!-- 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 YouTube app. -->
- <!-- User visible title for the keyboard shortcut that takes the user to the calendar app. -->
+ <!-- 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>
<!-- SysUI Tuner: Label for screen about do not disturb settings [CHAR LIMIT=60] -->
<string name="volume_and_do_not_disturb">Do Not Disturb</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 4998d68..91b9837 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1388,4 +1388,23 @@
<item name="android:lineHeight">@dimen/magnification_setting_button_line_height</item>
<item name="android:textAlignment">center</item>
</style>
+
+ <style name="ShortCutButton" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/shortcut_button_colored</item>
+ <item name="android:stateListAnimator">@null</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:padding">4dp</item>
+ <item name="android:textColor">?androidprv:attr/textColorSecondary</item>
+ </style>
+
+ <style name="ShortcutHorizontalDivider">
+ <item name="android:layout_width">120dp</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ <item name="android:background">?android:attr/dividerHorizontal</item>
+ </style>
+
+ <style name="ShortcutItemBackground">
+ <item name="android:background">@color/ksh_key_item_new_background</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index d0c5007..fadabb4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -47,6 +47,7 @@
import com.android.systemui.shade.ShadeControllerImpl;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyboardShortcutsModule;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -100,7 +101,8 @@
QSModule.class,
ReferenceScreenshotModule.class,
StartCentralSurfacesModule.class,
- VolumeModule.class
+ VolumeModule.class,
+ KeyboardShortcutsModule.class
})
public abstract class ReferenceSystemUIModule {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
new file mode 100644
index 0000000..01e042b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -0,0 +1,1382 @@
+/*
+ * Copyright (C) 2023 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.statusbar;
+
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.KeyboardShortcutsReceiver;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Contains functionality for handling keyboard shortcuts.
+ */
+public final class KeyboardShortcutListSearch {
+ private static final String TAG = KeyboardShortcutListSearch.class.getSimpleName();
+ private static final Object sLock = new Object();
+ @VisibleForTesting static KeyboardShortcutListSearch sInstance;
+
+ private static int SHORTCUT_SYSTEM_INDEX = 0;
+ private static int SHORTCUT_INPUT_INDEX = 1;
+ private static int SHORTCUT_OPENAPPS_INDEX = 2;
+ private static int SHORTCUT_SPECIFICAPP_INDEX = 3;
+
+ private WindowManager mWindowManager;
+ private EditText mSearchEditText;
+ private String mQueryString;
+ private int mCurrentCategoryIndex = 0;
+ private Map<Integer, Boolean> mKeySearchResultMap = new HashMap<>();
+
+ private List<List<KeyboardShortcutMultiMappingGroup>> mFullShortsGroup = new ArrayList<>();
+ private List<KeyboardShortcutMultiMappingGroup> mSpecificAppGroup = new ArrayList<>();
+ private List<KeyboardShortcutMultiMappingGroup> mSystemGroup = new ArrayList<>();
+ private List<KeyboardShortcutMultiMappingGroup> mInputGroup = new ArrayList<>();
+ private List<KeyboardShortcutMultiMappingGroup> mOpenAppsGroup = new ArrayList<>();
+
+ private ArrayList<Button> mFullButtonList = new ArrayList<>();
+ private Button mButtonSystem;
+ private Button mButtonInput;
+ private Button mButtonOpenApps;
+ private Button mButtonSpecificApp;
+ private ImageView mEditTextCancel;
+ private TextView mNoSearchResults;
+
+ private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
+ private final SparseArray<String> mModifierNames = new SparseArray<>();
+ private final SparseArray<Drawable> mSpecialCharacterDrawables = new SparseArray<>();
+ private final SparseArray<Drawable> mModifierDrawables = new SparseArray<>();
+ // Ordered list of modifiers that are supported. All values in this array must exist in
+ // mModifierNames.
+ private final int[] mModifierList = new int[] {
+ KeyEvent.META_META_ON, KeyEvent.META_CTRL_ON, KeyEvent.META_ALT_ON,
+ KeyEvent.META_SHIFT_ON, KeyEvent.META_SYM_ON, KeyEvent.META_FUNCTION_ON
+ };
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ @VisibleForTesting Context mContext;
+ private final IPackageManager mPackageManager;
+
+ @VisibleForTesting BottomSheetDialog mKeyboardShortcutsBottomSheetDialog;
+ private KeyCharacterMap mKeyCharacterMap;
+ private KeyCharacterMap mBackupKeyCharacterMap;
+
+ @VisibleForTesting
+ KeyboardShortcutListSearch(Context context, WindowManager windowManager) {
+ this.mContext = new ContextThemeWrapper(
+ context, android.R.style.Theme_DeviceDefault_Settings);
+ this.mPackageManager = AppGlobals.getPackageManager();
+ if (windowManager != null) {
+ this.mWindowManager = windowManager;
+ } else {
+ this.mWindowManager = mContext.getSystemService(WindowManager.class);
+ }
+ loadResources(context);
+ createHardcodedShortcuts();
+ }
+
+ private static KeyboardShortcutListSearch getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new KeyboardShortcutListSearch(context, null);
+ }
+ return sInstance;
+ }
+
+ public static void show(Context context, int deviceId) {
+ MetricsLogger.visible(context,
+ MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+ synchronized (sLock) {
+ if (sInstance != null && !sInstance.mContext.equals(context)) {
+ dismiss();
+ }
+ getInstance(context).showKeyboardShortcuts(deviceId);
+ }
+ }
+
+ public static void toggle(Context context, int deviceId) {
+ synchronized (sLock) {
+ if (isShowing()) {
+ dismiss();
+ } else {
+ show(context, deviceId);
+ }
+ }
+ }
+
+ public static void dismiss() {
+ synchronized (sLock) {
+ if (sInstance != null) {
+ MetricsLogger.hidden(sInstance.mContext,
+ MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
+ sInstance.dismissKeyboardShortcuts();
+ sInstance = null;
+ }
+ }
+ }
+
+ private static boolean isShowing() {
+ return sInstance != null && sInstance.mKeyboardShortcutsBottomSheetDialog != null
+ && sInstance.mKeyboardShortcutsBottomSheetDialog.isShowing();
+ }
+
+ private void loadResources(Context context) {
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_BACK, context.getString(R.string.keyboard_key_back));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_UP, context.getString(R.string.keyboard_key_dpad_up));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_DOWN, context.getString(R.string.keyboard_key_dpad_down));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_LEFT, context.getString(R.string.keyboard_key_dpad_left));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_RIGHT, context.getString(R.string.keyboard_key_dpad_right));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DPAD_CENTER, context.getString(R.string.keyboard_key_dpad_center));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_PERIOD, ".");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_TAB, context.getString(R.string.keyboard_key_tab));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_SPACE, context.getString(R.string.keyboard_key_space));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_ENTER, context.getString(R.string.keyboard_key_enter));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_DEL, context.getString(R.string.keyboard_key_backspace));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
+ context.getString(R.string.keyboard_key_media_play_pause));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MEDIA_STOP, context.getString(R.string.keyboard_key_media_stop));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MEDIA_NEXT, context.getString(R.string.keyboard_key_media_next));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+ context.getString(R.string.keyboard_key_media_previous));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_REWIND,
+ context.getString(R.string.keyboard_key_media_rewind));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+ context.getString(R.string.keyboard_key_media_fast_forward));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_PAGE_UP, context.getString(R.string.keyboard_key_page_up));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_PAGE_DOWN, context.getString(R.string.keyboard_key_page_down));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_A,
+ context.getString(R.string.keyboard_key_button_template, "A"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_B,
+ context.getString(R.string.keyboard_key_button_template, "B"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_C,
+ context.getString(R.string.keyboard_key_button_template, "C"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_X,
+ context.getString(R.string.keyboard_key_button_template, "X"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Y,
+ context.getString(R.string.keyboard_key_button_template, "Y"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Z,
+ context.getString(R.string.keyboard_key_button_template, "Z"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L1,
+ context.getString(R.string.keyboard_key_button_template, "L1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R1,
+ context.getString(R.string.keyboard_key_button_template, "R1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L2,
+ context.getString(R.string.keyboard_key_button_template, "L2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R2,
+ context.getString(R.string.keyboard_key_button_template, "R2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_START,
+ context.getString(R.string.keyboard_key_button_template, "Start"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_SELECT,
+ context.getString(R.string.keyboard_key_button_template, "Select"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_MODE,
+ context.getString(R.string.keyboard_key_button_template, "Mode"));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_FORWARD_DEL, context.getString(R.string.keyboard_key_forward_del));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_ESCAPE, "Esc");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_SYSRQ, "SysRq");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_BREAK, "Break");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_SCROLL_LOCK, "Scroll Lock");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MOVE_HOME, context.getString(R.string.keyboard_key_move_home));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_MOVE_END, context.getString(R.string.keyboard_key_move_end));
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_INSERT, context.getString(R.string.keyboard_key_insert));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F1, "F1");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F2, "F2");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F3, "F3");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F4, "F4");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F5, "F5");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F6, "F6");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F7, "F7");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F8, "F8");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F9, "F9");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F10, "F10");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F11, "F11");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_F12, "F12");
+ mSpecialCharacterNames.put(
+ KeyEvent.KEYCODE_NUM_LOCK, context.getString(R.string.keyboard_key_num_lock));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MINUS, "-");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_GRAVE, "~");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_EQUALS, "=");
+
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_0,
+ context.getString(R.string.keyboard_key_numpad_template, "0"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_1,
+ context.getString(R.string.keyboard_key_numpad_template, "1"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_2,
+ context.getString(R.string.keyboard_key_numpad_template, "2"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_3,
+ context.getString(R.string.keyboard_key_numpad_template, "3"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_4,
+ context.getString(R.string.keyboard_key_numpad_template, "4"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_5,
+ context.getString(R.string.keyboard_key_numpad_template, "5"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_6,
+ context.getString(R.string.keyboard_key_numpad_template, "6"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_7,
+ context.getString(R.string.keyboard_key_numpad_template, "7"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_8,
+ context.getString(R.string.keyboard_key_numpad_template, "8"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_9,
+ context.getString(R.string.keyboard_key_numpad_template, "9"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DIVIDE,
+ context.getString(R.string.keyboard_key_numpad_template, "/"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
+ context.getString(R.string.keyboard_key_numpad_template, "*"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
+ context.getString(R.string.keyboard_key_numpad_template, "-"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ADD,
+ context.getString(R.string.keyboard_key_numpad_template, "+"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DOT,
+ context.getString(R.string.keyboard_key_numpad_template, "."));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_COMMA,
+ context.getString(R.string.keyboard_key_numpad_template, ","));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ENTER,
+ context.getString(R.string.keyboard_key_numpad_template,
+ context.getString(R.string.keyboard_key_enter)));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_EQUALS,
+ context.getString(R.string.keyboard_key_numpad_template, "="));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN,
+ context.getString(R.string.keyboard_key_numpad_template, "("));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN,
+ context.getString(R.string.keyboard_key_numpad_template, ")"));
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_ZENKAKU_HANKAKU, "半角/全角");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_EISU, "英数");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
+ mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
+
+ mModifierNames.put(KeyEvent.META_META_ON, "Meta");
+ mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
+ mModifierNames.put(KeyEvent.META_ALT_ON, "Alt");
+ mModifierNames.put(KeyEvent.META_SHIFT_ON, "Shift");
+ mModifierNames.put(KeyEvent.META_SYM_ON, "Sym");
+ mModifierNames.put(KeyEvent.META_FUNCTION_ON, "Fn");
+
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DEL, context.getDrawable(R.drawable.ic_ksh_key_backspace));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_ENTER, context.getDrawable(R.drawable.ic_ksh_key_enter));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_UP, context.getDrawable(R.drawable.ic_ksh_key_up));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_RIGHT, context.getDrawable(R.drawable.ic_ksh_key_right));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_DOWN, context.getDrawable(R.drawable.ic_ksh_key_down));
+ mSpecialCharacterDrawables.put(
+ KeyEvent.KEYCODE_DPAD_LEFT, context.getDrawable(R.drawable.ic_ksh_key_left));
+
+ mModifierDrawables.put(
+ KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
+ }
+
+ private void createHardcodedShortcuts() {
+ // Add system shortcuts
+ mKeySearchResultMap.put(SHORTCUT_SYSTEM_INDEX, true);
+ mSystemGroup.add(getMultiMappingSystemShortcuts(mContext));
+ mSystemGroup.add(getSystemMultitaskingShortcuts(mContext));
+ // Add input shortcuts
+ mKeySearchResultMap.put(SHORTCUT_INPUT_INDEX, true);
+ mInputGroup.add(getMultiMappingInputShortcuts(mContext));
+ // Add open apps shortcuts
+ final List<KeyboardShortcutMultiMappingGroup> appShortcuts =
+ Arrays.asList(getDefaultMultiMappingApplicationShortcuts());
+ if (appShortcuts != null && !appShortcuts.isEmpty()) {
+ mOpenAppsGroup = appShortcuts;
+ mKeySearchResultMap.put(SHORTCUT_OPENAPPS_INDEX, true);
+ } else {
+ mKeySearchResultMap.put(SHORTCUT_OPENAPPS_INDEX, false);
+ }
+ }
+
+ /**
+ * Retrieves a {@link KeyCharacterMap} and assigns it to mKeyCharacterMap. If the given id is an
+ * existing device, that device's map is used. Otherwise, it checks first all available devices
+ * and if there is a full keyboard it uses that map, otherwise falls back to the Virtual
+ * Keyboard with its default map.
+ */
+ private void retrieveKeyCharacterMap(int deviceId) {
+ final InputManager inputManager = InputManager.getInstance();
+ mBackupKeyCharacterMap = inputManager.getInputDevice(-1).getKeyCharacterMap();
+ if (deviceId != -1) {
+ final InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+ if (inputDevice != null) {
+ mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+ return;
+ }
+ }
+ final int[] deviceIds = inputManager.getInputDeviceIds();
+ for (int id : deviceIds) {
+ final InputDevice inputDevice = inputManager.getInputDevice(id);
+ // -1 is the Virtual Keyboard, with the default key map. Use that one only as last
+ // resort.
+ if (inputDevice.getId() != -1 && inputDevice.isFullKeyboard()) {
+ mKeyCharacterMap = inputDevice.getKeyCharacterMap();
+ return;
+ }
+ }
+ // Fall back to -1, the virtual keyboard.
+ mKeyCharacterMap = mBackupKeyCharacterMap;
+ }
+
+ @VisibleForTesting
+ void showKeyboardShortcuts(int deviceId) {
+ retrieveKeyCharacterMap(deviceId);
+ mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() {
+ @Override
+ public void onKeyboardShortcutsReceived(
+ final List<KeyboardShortcutGroup> result) {
+ // Add specific app shortcuts
+ if (result.isEmpty()) {
+ mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false);
+ } else {
+ mSpecificAppGroup = reMapToKeyboardShortcutMultiMappingGroup(result);
+ mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
+ }
+ mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup);
+ mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup);
+ mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup);
+ mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup);
+ showKeyboardShortcutSearchList(mFullShortsGroup);
+ }
+ }, deviceId);
+ }
+
+ // The original data structure is only for 1-to-1 shortcut mapping, so remap the old
+ // data structure to the new data structure for handling the N-to-1 key mapping and other
+ // complex case.
+ private List<KeyboardShortcutMultiMappingGroup> reMapToKeyboardShortcutMultiMappingGroup(
+ List<KeyboardShortcutGroup> keyboardShortcutGroups) {
+ List<KeyboardShortcutMultiMappingGroup> keyboardShortcutMultiMappingGroups =
+ new ArrayList<>();
+ for (KeyboardShortcutGroup group : keyboardShortcutGroups) {
+ CharSequence categoryTitle = group.getLabel();
+ List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
+ for (KeyboardShortcutInfo info : group.getItems()) {
+ shortcutMultiMappingInfos.add(new ShortcutMultiMappingInfo(info));
+ }
+ keyboardShortcutMultiMappingGroups.add(
+ new KeyboardShortcutMultiMappingGroup(
+ categoryTitle, shortcutMultiMappingInfos));
+ }
+ return keyboardShortcutMultiMappingGroups;
+ }
+
+ private void dismissKeyboardShortcuts() {
+ if (mKeyboardShortcutsBottomSheetDialog != null) {
+ mKeyboardShortcutsBottomSheetDialog.dismiss();
+ mKeyboardShortcutsBottomSheetDialog = null;
+ }
+ }
+
+ private KeyboardShortcutMultiMappingGroup getMultiMappingSystemShortcuts(Context context) {
+ KeyboardShortcutMultiMappingGroup systemGroup =
+ new KeyboardShortcutMultiMappingGroup(
+ context.getString(R.string.keyboard_shortcut_group_system),
+ new ArrayList<>());
+ List<ShortcutKeyGroupMultiMappingInfo> infoList = Arrays.asList(
+ /* Access notification shade: Meta + N */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_notification_shade),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_N, KeyEvent.META_META_ON))),
+ /* Take a full screenshot: Meta + Ctrl + S */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_full_screenshot),
+ Arrays.asList(
+ Pair.create(
+ KeyEvent.KEYCODE_S,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))),
+ /* Access list of system / apps shortcuts: Meta + / */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_system_app_shortcuts),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_SLASH, KeyEvent.META_META_ON))),
+ /* Back: go back to previous state (back button) */
+ /* Meta + ~, Meta + backspace, Meta + left arrow */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_go_back),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_GRAVE, KeyEvent.META_META_ON),
+ Pair.create(KeyEvent.KEYCODE_DEL, KeyEvent.META_META_ON),
+ Pair.create(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_META_ON))),
+ /* Access home screen: Meta + H, Meta + Enter */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_home_screen),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_H, KeyEvent.META_META_ON),
+ Pair.create(KeyEvent.KEYCODE_ENTER, KeyEvent.META_META_ON))),
+ /* Overview of open apps: Meta + Tab */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_overview_open_apps),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_TAB, KeyEvent.META_META_ON))),
+ /* Cycle through recent apps (forward): Alt + Tab */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_cycle_forward),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_TAB, KeyEvent.META_ALT_ON))),
+ /* Cycle through recent apps (back): Shift + Alt + Tab */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_cycle_back),
+ Arrays.asList(
+ Pair.create(
+ KeyEvent.KEYCODE_TAB,
+ KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))),
+ /* Access list of all apps and search (i.e. Search/Launcher): Meta */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_all_apps_search),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.META_META_ON))),
+ /* Hide and (re)show taskbar: Meta + T */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_hide_reshow_taskbar),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))),
+ /* Access system settings: Meta + I */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_system_settings),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_I, KeyEvent.META_META_ON))),
+ /* Access Google Assistant: Meta + A */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_access_google_assistant),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))),
+ /* Lock screen: Meta + L */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_lock_screen),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))),
+ /* Pull up Notes app for quick memo: Meta + Ctrl + N */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_quick_memo),
+ Arrays.asList(
+ Pair.create(
+ KeyEvent.KEYCODE_N,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)))
+ );
+ for (ShortcutKeyGroupMultiMappingInfo info : infoList) {
+ systemGroup.addItem(info.getShortcutMultiMappingInfo());
+ }
+ return systemGroup;
+ }
+
+ private static class ShortcutKeyGroupMultiMappingInfo {
+ private String mLabel;
+ private List<Pair<Integer, Integer>> mKeycodeGroupList;
+
+ ShortcutKeyGroupMultiMappingInfo(
+ String label, List<Pair<Integer, Integer>> keycodeGroupList) {
+ mLabel = label;
+ mKeycodeGroupList = keycodeGroupList;
+ }
+
+ ShortcutMultiMappingInfo getShortcutMultiMappingInfo() {
+ List<ShortcutKeyGroup> shortcutKeyGroups = new ArrayList<>();
+ for (Pair<Integer, Integer> keycodeGroup : mKeycodeGroupList) {
+ shortcutKeyGroups.add(new ShortcutKeyGroup(
+ new KeyboardShortcutInfo(
+ mLabel,
+ keycodeGroup.first /* keycode */,
+ keycodeGroup.second /* modifiers*/),
+ null));
+ }
+ ShortcutMultiMappingInfo shortcutMultiMappingInfo =
+ new ShortcutMultiMappingInfo(mLabel, null, shortcutKeyGroups);
+ return shortcutMultiMappingInfo;
+ }
+ }
+
+ private static KeyboardShortcutMultiMappingGroup getSystemMultitaskingShortcuts(
+ Context context) {
+ KeyboardShortcutMultiMappingGroup systemMultitaskingGroup =
+ new KeyboardShortcutMultiMappingGroup(
+ context.getString(R.string.keyboard_shortcut_group_system_multitasking),
+ new ArrayList<>());
+
+ // System multitasking shortcuts:
+ // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow
+ // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow
+ // Switch from Split screen to full screen: Meta + Ctrl + Up arrow
+ // During Split screen: replace an app from one to another: Meta + Ctrl + Down arrow
+ String[] shortcutLabels = {
+ context.getString(R.string.system_multitasking_rhs),
+ context.getString(R.string.system_multitasking_lhs),
+ context.getString(R.string.system_multitasking_full_screen),
+ context.getString(R.string.system_multitasking_replace)
+ };
+ int[] keyCodes = {
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ KeyEvent.KEYCODE_DPAD_UP,
+ KeyEvent.KEYCODE_DPAD_DOWN
+ };
+
+ for (int i = 0; i < shortcutLabels.length; i++) {
+ List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(
+ new KeyboardShortcutInfo(
+ shortcutLabels[i],
+ keyCodes[i],
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON),
+ null));
+ ShortcutMultiMappingInfo shortcutMultiMappingInfo =
+ new ShortcutMultiMappingInfo(
+ shortcutLabels[i],
+ null,
+ shortcutKeyGroups);
+ systemMultitaskingGroup.addItem(shortcutMultiMappingInfo);
+ }
+ return systemMultitaskingGroup;
+ }
+
+ private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts(
+ Context context) {
+ List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList(
+ /* Switch input language (next language): Ctrl + Space or Meta + Space */
+ new ShortcutMultiMappingInfo(
+ context.getString(R.string.input_switch_input_language_next),
+ null,
+ Arrays.asList(
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(
+ R.string.input_switch_input_language_next),
+ KeyEvent.KEYCODE_SPACE, KeyEvent.META_CTRL_ON),
+ null),
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(
+ R.string.input_switch_input_language_next),
+ KeyEvent.KEYCODE_SPACE, KeyEvent.META_META_ON),
+ null))),
+ /* Switch input language (previous language): */
+ /* Ctrl + Shift + Space or Meta + Shift + Space */
+ new ShortcutMultiMappingInfo(
+ context.getString(R.string.input_switch_input_language_previous),
+ null,
+ Arrays.asList(
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(
+ R.string.input_switch_input_language_previous),
+ KeyEvent.KEYCODE_SPACE,
+ KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON),
+ null),
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(
+ R.string.input_switch_input_language_previous),
+ KeyEvent.KEYCODE_SPACE,
+ KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON),
+ null))),
+ /* Access emoji: Meta + . */
+ new ShortcutMultiMappingInfo(
+ context.getString(R.string.input_access_emoji),
+ null,
+ Arrays.asList(
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(R.string.input_access_emoji),
+ KeyEvent.KEYCODE_PERIOD,
+ KeyEvent.META_META_ON),
+ null))),
+ /* Access voice typing: Meta + V */
+ new ShortcutMultiMappingInfo(
+ context.getString(R.string.input_access_voice_typing),
+ null,
+ Arrays.asList(
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(
+ context.getString(R.string.input_access_voice_typing),
+ KeyEvent.KEYCODE_V, KeyEvent.META_META_ON),
+ null)))
+ );
+ return new KeyboardShortcutMultiMappingGroup(
+ context.getString(R.string.keyboard_shortcut_group_input),
+ 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);
+ List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
+
+ 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)
+ };
+ int[] keyCodes = {
+ KeyEvent.KEYCODE_B,
+ KeyEvent.KEYCODE_C,
+ KeyEvent.KEYCODE_E,
+ KeyEvent.KEYCODE_K,
+ KeyEvent.KEYCODE_M,
+ KeyEvent.KEYCODE_P,
+ KeyEvent.KEYCODE_S,
+ KeyEvent.KEYCODE_U,
+ };
+
+ // Assist.
+ if (assistPackageInfo != 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));
+ }
+ }
+
+ Comparator<ShortcutMultiMappingInfo> applicationItemsComparator =
+ new Comparator<ShortcutMultiMappingInfo>() {
+ @Override
+ public int compare(
+ ShortcutMultiMappingInfo ksh1, ShortcutMultiMappingInfo ksh2) {
+ boolean ksh1ShouldBeLast = ksh1.getLabel() == null
+ || ksh1.getLabel().toString().isEmpty();
+ boolean ksh2ShouldBeLast = ksh2.getLabel() == null
+ || ksh2.getLabel().toString().isEmpty();
+ if (ksh1ShouldBeLast && ksh2ShouldBeLast) {
+ return 0;
+ }
+ if (ksh1ShouldBeLast) {
+ return 1;
+ }
+ if (ksh2ShouldBeLast) {
+ return -1;
+ }
+ return (ksh1.getLabel().toString()).compareToIgnoreCase(
+ ksh2.getLabel().toString());
+ }
+ };
+ // Sorts by label, case insensitive with nulls and/or empty labels last.
+ Collections.sort(shortcutMultiMappingInfos, applicationItemsComparator);
+ return new KeyboardShortcutMultiMappingGroup(categoryTitle, shortcutMultiMappingInfos);
+ }
+
+ private Icon getIconForIntentCategory(String intentCategory, int userId) {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(intentCategory);
+
+ final PackageInfo packageInfo = getPackageInfoForIntent(intent, userId);
+ if (packageInfo != null && packageInfo.applicationInfo.icon != 0) {
+ return Icon.createWithResource(
+ packageInfo.applicationInfo.packageName,
+ packageInfo.applicationInfo.icon);
+ }
+ return null;
+ }
+
+ private PackageInfo getPackageInfoForIntent(Intent intent, int userId) {
+ try {
+ ResolveInfo handler;
+ handler = mPackageManager.resolveIntent(
+ intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId);
+ if (handler == null || handler.activityInfo == null) {
+ return null;
+ }
+ return mPackageManager.getPackageInfo(handler.activityInfo.packageName, 0, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManagerService is dead", e);
+ return null;
+ }
+ }
+
+ private void showKeyboardShortcutSearchList(
+ List<List<KeyboardShortcutMultiMappingGroup>> keyboardShortcutMultiMappingGroupList) {
+ // Need to post on the main thread.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleShowKeyboardShortcutSearchList(keyboardShortcutMultiMappingGroupList);
+ }
+ });
+ }
+
+ private void handleShowKeyboardShortcutSearchList(
+ List<List<KeyboardShortcutMultiMappingGroup>> keyboardShortcutMultiMappingGroupList) {
+ mQueryString = null;
+ LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+ mKeyboardShortcutsBottomSheetDialog =
+ new BottomSheetDialog(mContext);
+ final View keyboardShortcutsView = inflater.inflate(
+ R.layout.keyboard_shortcuts_search_view, null);
+ mNoSearchResults = keyboardShortcutsView.findViewById(R.id.shortcut_search_no_result);
+ mKeyboardShortcutsBottomSheetDialog.setContentView(keyboardShortcutsView);
+ setButtonsDefaultStatus(keyboardShortcutsView);
+ populateKeyboardShortcutSearchList(
+ keyboardShortcutsView.findViewById(R.id.keyboard_shortcuts_container));
+
+ // Workaround for solve issue about dialog not full expanded when landscape.
+ FrameLayout bottomSheet = (FrameLayout)
+ mKeyboardShortcutsBottomSheetDialog.findViewById(
+ com.google.android.material.R.id.design_bottom_sheet);
+ if (bottomSheet != null) {
+ bottomSheet.setBackgroundResource(android.R.color.transparent);
+ }
+
+ BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet);
+ behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ behavior.setSkipCollapsed(true);
+
+ mKeyboardShortcutsBottomSheetDialog.setCanceledOnTouchOutside(true);
+ Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow();
+ keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
+ synchronized (sLock) {
+ // show KeyboardShortcutsBottomSheetDialog only if it has not been dismissed already
+ if (sInstance != null) {
+ mKeyboardShortcutsBottomSheetDialog.show();
+ setDialogScreenSize();
+ keyboardShortcutsView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ setDialogScreenSize();
+ }
+ });
+ }
+ }
+ mSearchEditText = keyboardShortcutsView.findViewById(R.id.keyboard_shortcuts_search);
+ mSearchEditText.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ mQueryString = s.toString();
+ populateKeyboardShortcutSearchList(
+ keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container));
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing.
+ }
+ });
+ mEditTextCancel = keyboardShortcutsView.findViewById(R.id.keyboard_shortcuts_search_cancel);
+ mEditTextCancel.setOnClickListener(v -> mSearchEditText.setText(null));
+ }
+
+ private void populateKeyboardShortcutSearchList(LinearLayout keyboardShortcutsLayout) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ TextView shortcutsKeyView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_view, keyboardShortcutsLayout, false);
+ shortcutsKeyView.measure(
+ View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ final int shortcutKeyTextItemMinWidth = shortcutsKeyView.getMeasuredHeight();
+ // Needed to be able to scale the image items to the same height as the text items.
+ final int shortcutKeyIconItemHeightWidth = shortcutsKeyView.getMeasuredHeight()
+ - shortcutsKeyView.getPaddingTop()
+ - shortcutsKeyView.getPaddingBottom();
+ keyboardShortcutsLayout.removeAllViews();
+
+ // Search if user's input is contained in any shortcut groups.
+ if (mQueryString != null) {
+ for (int i = 0; i < mFullShortsGroup.size(); i++) {
+ mKeySearchResultMap.put(i, false);
+ for (KeyboardShortcutMultiMappingGroup group : mFullShortsGroup.get(i)) {
+ for (ShortcutMultiMappingInfo info : group.getItems()) {
+ String itemLabel = info.getLabel().toString();
+ if (itemLabel.toUpperCase(Locale.getDefault()).contains(
+ mQueryString.toUpperCase(Locale.getDefault()))) {
+ mKeySearchResultMap.put(i, true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Set default color for the non-focus categories.
+ for (int i = 0; i < mKeySearchResultMap.size(); i++) {
+ if (mKeySearchResultMap.get(i)) {
+ mFullButtonList.get(i).setVisibility(View.VISIBLE);
+ setButtonFocusColor(i, false);
+ } else {
+ mFullButtonList.get(i).setVisibility(View.GONE);
+ }
+ }
+
+ // Move the focus to the suitable category.
+ if (mFullButtonList.get(mCurrentCategoryIndex).getVisibility() == View.GONE) {
+ for (int i = 0; i < mKeySearchResultMap.size(); i++) {
+ if (mKeySearchResultMap.get(i)) {
+ setCurrentCategoryIndex(i);
+ break;
+ }
+ }
+ }
+
+ // Set color for the current focus category
+ setButtonFocusColor(mCurrentCategoryIndex, true);
+
+ // Load shortcuts for current focus category.
+ List<KeyboardShortcutMultiMappingGroup> keyboardShortcutMultiMappingGroups =
+ mFullShortsGroup.get(mCurrentCategoryIndex);
+
+ int keyboardShortcutGroupsSize = keyboardShortcutMultiMappingGroups.size();
+ List<Boolean> groupSearchResult = new ArrayList<>();
+ for (int i = 0; i < keyboardShortcutGroupsSize; i++) {
+ View separator = inflater.inflate(
+ R.layout.keyboard_shortcuts_category_short_separator,
+ keyboardShortcutsLayout,
+ false);
+
+ // If there are more than one category, add separators among categories.
+ if (i > 0) {
+ keyboardShortcutsLayout.addView(separator);
+ }
+
+ List<Boolean> itemSearchResult = new ArrayList<>();
+ KeyboardShortcutMultiMappingGroup categoryGroup =
+ keyboardShortcutMultiMappingGroups.get(i);
+ TextView categoryTitle = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_category_title, keyboardShortcutsLayout, false);
+ categoryTitle.setText(categoryGroup.getCategory());
+ keyboardShortcutsLayout.addView(categoryTitle);
+ LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
+ R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
+ final int itemsSize = categoryGroup.getItems().size();
+ for (int j = 0; j < itemsSize; j++) {
+ ShortcutMultiMappingInfo keyGroupInfo = categoryGroup.getItems().get(j);
+
+ if (mQueryString != null) {
+ String shortcutLabel =
+ keyGroupInfo.getLabel().toString().toUpperCase(Locale.getDefault());
+ String queryString = mQueryString.toUpperCase(Locale.getDefault());
+ if (!shortcutLabel.contains(queryString)) {
+ itemSearchResult.add(j, false);
+ continue;
+ } else {
+ itemSearchResult.add(j, true);
+ }
+ }
+
+ View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
+ shortcutContainer, false);
+ TextView shortcutKeyword =
+ shortcutView.findViewById(R.id.keyboard_shortcuts_keyword);
+ shortcutKeyword.setText(keyGroupInfo.getLabel());
+
+ if (keyGroupInfo.getIcon() != null) {
+ ImageView shortcutIcon =
+ shortcutView.findViewById(R.id.keyboard_shortcuts_icon);
+ shortcutIcon.setImageIcon(keyGroupInfo.getIcon());
+ shortcutIcon.setVisibility(View.VISIBLE);
+ RelativeLayout.LayoutParams lp =
+ (RelativeLayout.LayoutParams) shortcutKeyword.getLayoutParams();
+ lp.removeRule(RelativeLayout.ALIGN_PARENT_START);
+ shortcutKeyword.setLayoutParams(lp);
+ }
+
+ ViewGroup shortcutItemsContainer =
+ shortcutView.findViewById(R.id.keyboard_shortcuts_item_container);
+ final int keyGroupItemsSize = keyGroupInfo.getShortcutKeyGroups().size();
+ for (int p = 0; p < keyGroupItemsSize; p++) {
+ KeyboardShortcutInfo keyboardShortcutInfo =
+ keyGroupInfo.getShortcutKeyGroups().get(p).getKeyboardShortcutInfo();
+ String complexCommand =
+ keyGroupInfo.getShortcutKeyGroups().get(p).getComplexCommand();
+
+ if (complexCommand == null) {
+ List<StringDrawableContainer> shortcutKeys =
+ getHumanReadableShortcutKeys(keyboardShortcutInfo);
+ if (shortcutKeys == null) {
+ // Ignore shortcuts we can't display keys for.
+ Log.w(TAG, "Keyboard Shortcut contains unsupported keys, skipping.");
+ continue;
+ }
+ final int shortcutKeysSize = shortcutKeys.size();
+ for (int k = 0; k < shortcutKeysSize; k++) {
+ StringDrawableContainer shortcutRepresentation = shortcutKeys.get(k);
+ if (shortcutRepresentation.mDrawable != null) {
+ ImageView shortcutKeyIconView = (ImageView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_new_icon_view,
+ shortcutItemsContainer,
+ false);
+ Bitmap bitmap = Bitmap.createBitmap(shortcutKeyIconItemHeightWidth,
+ shortcutKeyIconItemHeightWidth, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ shortcutRepresentation.mDrawable.setBounds(0, 0, canvas.getWidth(),
+ canvas.getHeight());
+ shortcutRepresentation.mDrawable.draw(canvas);
+ shortcutKeyIconView.setImageBitmap(bitmap);
+ shortcutKeyIconView.setImportantForAccessibility(
+ IMPORTANT_FOR_ACCESSIBILITY_YES);
+ shortcutKeyIconView.setAccessibilityDelegate(
+ new ShortcutKeyAccessibilityDelegate(
+ shortcutRepresentation.mString));
+ shortcutItemsContainer.addView(shortcutKeyIconView);
+ } else if (shortcutRepresentation.mString != null) {
+ TextView shortcutKeyTextView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_new_view,
+ shortcutItemsContainer,
+ false);
+ shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
+ shortcutKeyTextView.setText(shortcutRepresentation.mString);
+ shortcutKeyTextView.setAccessibilityDelegate(
+ new ShortcutKeyAccessibilityDelegate(
+ shortcutRepresentation.mString));
+ shortcutItemsContainer.addView(shortcutKeyTextView);
+ }
+
+ if (k < shortcutKeysSize - 1) {
+ TextView shortcutKeyTextView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_plus_view,
+ shortcutItemsContainer,
+ false);
+ shortcutItemsContainer.addView(shortcutKeyTextView);
+ }
+ }
+ } else {
+ TextView shortcutKeyTextView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_new_view,
+ shortcutItemsContainer,
+ false);
+ shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
+ shortcutKeyTextView.setText(complexCommand);
+ shortcutKeyTextView.setAccessibilityDelegate(
+ new ShortcutKeyAccessibilityDelegate(complexCommand));
+ shortcutItemsContainer.addView(shortcutKeyTextView);
+ }
+
+ if (p < keyGroupItemsSize - 1) {
+ TextView shortcutKeyTextView = (TextView) inflater.inflate(
+ R.layout.keyboard_shortcuts_key_vertical_bar_view,
+ shortcutItemsContainer,
+ false);
+ shortcutItemsContainer.addView(shortcutKeyTextView);
+ }
+ }
+ shortcutContainer.addView(shortcutView);
+ }
+
+ if (!groupSearchResult.isEmpty() && !groupSearchResult.get(i - 1)) {
+ keyboardShortcutsLayout.removeView(separator);
+ }
+
+ if (!itemSearchResult.isEmpty() && !itemSearchResult.contains(true)) {
+ // No results, so remove the category title and separator
+ keyboardShortcutsLayout.removeView(categoryTitle);
+ keyboardShortcutsLayout.removeView(separator);
+ groupSearchResult.add(false);
+ if (i == keyboardShortcutGroupsSize - 1 && !groupSearchResult.contains(true)) {
+ // show "No shortcut found"
+ mNoSearchResults.setVisibility(View.VISIBLE);
+ }
+ continue;
+ }
+ groupSearchResult.add(true);
+ mNoSearchResults.setVisibility(View.GONE);
+ keyboardShortcutsLayout.addView(shortcutContainer);
+ }
+ }
+
+ private List<StringDrawableContainer> getHumanReadableShortcutKeys(KeyboardShortcutInfo info) {
+ List<StringDrawableContainer> shortcutKeys = getHumanReadableModifiers(info);
+ if (shortcutKeys == null) {
+ return null;
+ }
+ String shortcutKeyString = null;
+ Drawable shortcutKeyDrawable = null;
+ if (info.getBaseCharacter() > Character.MIN_VALUE) {
+ shortcutKeyString = String.valueOf(info.getBaseCharacter());
+ } else if (mSpecialCharacterDrawables.get(info.getKeycode()) != null) {
+ shortcutKeyDrawable = mSpecialCharacterDrawables.get(info.getKeycode());
+ shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+ } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
+ shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
+ } else {
+ // Special case for shortcuts with no base key or keycode.
+ if (info.getKeycode() == KeyEvent.KEYCODE_UNKNOWN) {
+ return shortcutKeys;
+ }
+ char displayLabel = mKeyCharacterMap.getDisplayLabel(info.getKeycode());
+ if (displayLabel != 0) {
+ shortcutKeyString = String.valueOf(displayLabel);
+ } else {
+ displayLabel = mBackupKeyCharacterMap.getDisplayLabel(info.getKeycode());
+ if (displayLabel != 0) {
+ shortcutKeyString = String.valueOf(displayLabel);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ if (shortcutKeyString != null) {
+ shortcutKeys.add(new StringDrawableContainer(shortcutKeyString, shortcutKeyDrawable));
+ } else {
+ Log.w(TAG, "Keyboard Shortcut does not have a text representation, skipping.");
+ }
+
+ return shortcutKeys;
+ }
+
+ private List<StringDrawableContainer> getHumanReadableModifiers(KeyboardShortcutInfo info) {
+ final List<StringDrawableContainer> shortcutKeys = new ArrayList<>();
+ int modifiers = info.getModifiers();
+ if (modifiers == 0) {
+ return shortcutKeys;
+ }
+ for (int supportedModifier : mModifierList) {
+ if ((modifiers & supportedModifier) != 0) {
+ shortcutKeys.add(new StringDrawableContainer(
+ mModifierNames.get(supportedModifier),
+ mModifierDrawables.get(supportedModifier)));
+ modifiers &= ~supportedModifier;
+ }
+ }
+ if (modifiers != 0) {
+ // Remaining unsupported modifiers, don't show anything.
+ return null;
+ }
+ return shortcutKeys;
+ }
+
+ private final class ShortcutKeyAccessibilityDelegate extends AccessibilityDelegate {
+ private String mContentDescription;
+
+ ShortcutKeyAccessibilityDelegate(String contentDescription) {
+ mContentDescription = contentDescription;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (mContentDescription != null) {
+ info.setContentDescription(mContentDescription.toLowerCase(Locale.getDefault()));
+ }
+ }
+ }
+
+ private static final class StringDrawableContainer {
+ @NonNull
+ public String mString;
+ @Nullable
+ public Drawable mDrawable;
+
+ StringDrawableContainer(String string, Drawable drawable) {
+ mString = string;
+ mDrawable = drawable;
+ }
+ }
+
+ private void setDialogScreenSize() {
+ Window window = mKeyboardShortcutsBottomSheetDialog.getWindow();
+ Display display = mWindowManager.getDefaultDisplay();
+ WindowManager.LayoutParams lp =
+ mKeyboardShortcutsBottomSheetDialog.getWindow().getAttributes();
+ if (mContext.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_PORTRAIT) {
+ lp.width = (int) (display.getWidth() * 0.8);
+ lp.height = (int) (display.getHeight() * 0.7);
+ } else {
+ lp.width = (int) (display.getWidth() * 0.7);
+ lp.height = (int) (display.getHeight() * 0.8);
+ }
+ window.setGravity(Gravity.BOTTOM);
+ window.setAttributes(lp);
+ }
+
+ private void setCurrentCategoryIndex(int index) {
+ mCurrentCategoryIndex = index;
+ }
+
+ private void setButtonsDefaultStatus(View keyboardShortcutsView) {
+ mButtonSystem = keyboardShortcutsView.findViewById(R.id.shortcut_system);
+ mButtonInput = keyboardShortcutsView.findViewById(R.id.shortcut_input);
+ mButtonOpenApps = keyboardShortcutsView.findViewById(R.id.shortcut_open_apps);
+ mButtonSpecificApp = keyboardShortcutsView.findViewById(R.id.shortcut_specific_app);
+
+ mButtonSystem.setOnClickListener(v -> {
+ setCurrentCategoryIndex(SHORTCUT_SYSTEM_INDEX);
+ populateKeyboardShortcutSearchList(keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container));
+ });
+
+ mButtonInput.setOnClickListener(v -> {
+ setCurrentCategoryIndex(SHORTCUT_INPUT_INDEX);
+ populateKeyboardShortcutSearchList(keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container));
+ });
+
+ mButtonOpenApps.setOnClickListener(v -> {
+ setCurrentCategoryIndex(SHORTCUT_OPENAPPS_INDEX);
+ populateKeyboardShortcutSearchList(keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container));
+ });
+
+ mButtonSpecificApp.setOnClickListener(v -> {
+ setCurrentCategoryIndex(SHORTCUT_SPECIFICAPP_INDEX);
+ populateKeyboardShortcutSearchList(keyboardShortcutsView.findViewById(
+ R.id.keyboard_shortcuts_container));
+ });
+
+ mFullButtonList.add(mButtonSystem);
+ mFullButtonList.add(mButtonInput);
+ mFullButtonList.add(mButtonOpenApps);
+ mFullButtonList.add(mButtonSpecificApp);
+ }
+
+ private void setButtonFocusColor(int i, boolean isFocused) {
+ if (isFocused) {
+ mFullButtonList.get(i).setTextColor(getColorOfTextColorOnAccent());
+ mFullButtonList.get(i).setBackground(
+ mContext.getDrawable(R.drawable.shortcut_button_focus_colored));
+ } else {
+ // Default color
+ mFullButtonList.get(i).setTextColor(getColorOfTextColorSecondary());
+ mFullButtonList.get(i).setBackground(
+ mContext.getDrawable(R.drawable.shortcut_button_colored));
+ }
+ }
+
+ private int getColorOfTextColorOnAccent() {
+ return Utils.getColorAttrDefaultColor(
+ mContext, com.android.internal.R.attr.textColorOnAccent);
+ }
+
+ private int getColorOfTextColorSecondary() {
+ return Utils.getColorAttrDefaultColor(
+ mContext, com.android.internal.R.attr.textColorSecondary);
+ }
+
+ // Create the new data structure for handling the N-to-1 key mapping and other complex case.
+ private static class KeyboardShortcutMultiMappingGroup {
+ private final CharSequence mCategory;
+ private List<ShortcutMultiMappingInfo> mItems;
+
+ KeyboardShortcutMultiMappingGroup(
+ CharSequence category, List<ShortcutMultiMappingInfo> items) {
+ mCategory = category;
+ mItems = items;
+ }
+
+ void addItem(ShortcutMultiMappingInfo item) {
+ mItems.add(item);
+ }
+
+ CharSequence getCategory() {
+ return mCategory;
+ }
+
+ List<ShortcutMultiMappingInfo> getItems() {
+ return mItems;
+ }
+ }
+
+ private static class ShortcutMultiMappingInfo {
+ private final CharSequence mLabel;
+ private final Icon mIcon;
+ private List<ShortcutKeyGroup> mShortcutKeyGroups;
+
+ ShortcutMultiMappingInfo(
+ CharSequence label, Icon icon, List<ShortcutKeyGroup> shortcutKeyGroups) {
+ mLabel = label;
+ mIcon = icon;
+ mShortcutKeyGroups = shortcutKeyGroups;
+ }
+
+ ShortcutMultiMappingInfo(KeyboardShortcutInfo info) {
+ mLabel = info.getLabel();
+ mIcon = info.getIcon();
+ mShortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(info, null));
+ }
+
+ CharSequence getLabel() {
+ return mLabel;
+ }
+
+ Icon getIcon() {
+ return mIcon;
+ }
+
+ List<ShortcutKeyGroup> getShortcutKeyGroups() {
+ return mShortcutKeyGroups;
+ }
+ }
+
+ private static class ShortcutKeyGroup {
+ private final KeyboardShortcutInfo mKeyboardShortcutInfo;
+ private final String mComplexCommand;
+
+ ShortcutKeyGroup(KeyboardShortcutInfo keyboardShortcutInfo, String complexCommand) {
+ mKeyboardShortcutInfo = keyboardShortcutInfo;
+ mComplexCommand = complexCommand;
+ }
+
+ // To be compatible with the original functions, keep KeyboardShortcutInfo in here.
+ KeyboardShortcutInfo getKeyboardShortcutInfo() {
+ return mKeyboardShortcutInfo;
+ }
+
+ // In some case, the shortcut is a complex description not a N-to-1 key mapping.
+ String getComplexCommand() {
+ return mComplexCommand;
+ }
+ }
+
+ private static PackageInfo getAssistPackageInfo(
+ Context context, IPackageManager packageManager, int userId) {
+ AssistUtils assistUtils = new AssistUtils(context);
+ ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
+ // Not all devices have an assist component.
+ PackageInfo assistPackageInfo = null;
+ if (assistComponent != null) {
+ try {
+ assistPackageInfo = packageManager.getPackageInfo(
+ assistComponent.getPackageName(), 0, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManagerService is dead");
+ }
+ }
+ return assistPackageInfo;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 7e6ddcf..f20f929 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -63,6 +63,7 @@
import android.widget.RelativeLayout;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AssistUtils;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
@@ -80,7 +81,8 @@
public final class KeyboardShortcuts {
private static final String TAG = KeyboardShortcuts.class.getSimpleName();
private static final Object sLock = new Object();
- private static KeyboardShortcuts sInstance;
+ @VisibleForTesting static KeyboardShortcuts sInstance;
+ private WindowManager mWindowManager;
private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
private final SparseArray<String> mModifierNames = new SparseArray<>();
@@ -94,7 +96,7 @@
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
- private final Context mContext;
+ @VisibleForTesting Context mContext;
private final IPackageManager mPackageManager;
private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
@@ -123,20 +125,26 @@
}
};
- private Dialog mKeyboardShortcutsDialog;
+ @VisibleForTesting Dialog mKeyboardShortcutsDialog;
private KeyCharacterMap mKeyCharacterMap;
private KeyCharacterMap mBackupKeyCharacterMap;
- private KeyboardShortcuts(Context context) {
+ @VisibleForTesting
+ KeyboardShortcuts(Context context, WindowManager windowManager) {
this.mContext = new ContextThemeWrapper(
context, android.R.style.Theme_DeviceDefault_Settings);
this.mPackageManager = AppGlobals.getPackageManager();
+ if (windowManager != null) {
+ this.mWindowManager = windowManager;
+ } else {
+ this.mWindowManager = mContext.getSystemService(WindowManager.class);
+ }
loadResources(context);
}
private static KeyboardShortcuts getInstance(Context context) {
if (sInstance == null) {
- sInstance = new KeyboardShortcuts(context);
+ sInstance = new KeyboardShortcuts(context, null);
}
return sInstance;
}
@@ -371,10 +379,10 @@
mKeyCharacterMap = mBackupKeyCharacterMap;
}
- private void showKeyboardShortcuts(int deviceId) {
+ @VisibleForTesting
+ void showKeyboardShortcuts(int deviceId) {
retrieveKeyCharacterMap(deviceId);
- WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
- wm.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() {
+ mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() {
@Override
public void onKeyboardShortcutsReceived(
final List<KeyboardShortcutGroup> result) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
index 8a5bece..e9fac28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
@@ -19,16 +19,38 @@
import android.content.Context;
import android.content.Intent;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.shared.recents.utilities.Utilities;
+
+import javax.inject.Inject;
+
/**
* Receiver for the Keyboard Shortcuts Helper.
*/
public class KeyboardShortcutsReceiver extends BroadcastReceiver {
+
+ private boolean mIsShortcutListSearchEnabled;
+
+ @Inject
+ public KeyboardShortcutsReceiver(FeatureFlags featureFlags) {
+ mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
+ }
+
@Override
public void onReceive(Context context, Intent intent) {
- if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
- KeyboardShortcuts.show(context, -1 /* deviceId unknown */);
- } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
- KeyboardShortcuts.dismiss();
+ if (mIsShortcutListSearchEnabled && Utilities.isTablet(context)) {
+ if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcutListSearch.show(context, -1 /* deviceId unknown */);
+ } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcutListSearch.dismiss();
+ }
+ } else {
+ if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcuts.show(context, -1 /* deviceId unknown */);
+ } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
+ KeyboardShortcuts.dismiss();
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1966a66..6a52a33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -186,11 +186,13 @@
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.BackDropView;
import com.android.systemui.statusbar.CircleReveal;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.GestureRecorder;
+import com.android.systemui.statusbar.KeyboardShortcutListSearch;
import com.android.systemui.statusbar.KeyboardShortcuts;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.LiftReveal;
@@ -299,6 +301,7 @@
private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
private float mTransitionToFullShadeProgress = 0f;
private NotificationListContainer mNotifListContainer;
+ private boolean mIsShortcutListSearchEnabled;
private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
new KeyguardStateController.Callback() {
@@ -833,6 +836,7 @@
mCameraLauncherLazy = cameraLauncherLazy;
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUserTracker = userTracker;
+ mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
mStartingSurfaceOptional = startingSurfaceOptional;
@@ -2546,7 +2550,11 @@
String action = intent.getAction();
String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
- KeyboardShortcuts.dismiss();
+ if (mIsShortcutListSearchEnabled && Utilities.isTablet(mContext)) {
+ KeyboardShortcutListSearch.dismiss();
+ } else {
+ KeyboardShortcuts.dismiss();
+ }
mRemoteInputManager.closeRemoteInputs();
if (mLockscreenUserManager.isCurrentProfile(getSendingUserId())) {
int flags = CommandQueue.FLAG_EXCLUDE_NONE;
@@ -3893,11 +3901,19 @@
}
protected void toggleKeyboardShortcuts(int deviceId) {
- KeyboardShortcuts.toggle(mContext, deviceId);
+ if (mIsShortcutListSearchEnabled && Utilities.isTablet(mContext)) {
+ KeyboardShortcutListSearch.toggle(mContext, deviceId);
+ } else {
+ KeyboardShortcuts.toggle(mContext, deviceId);
+ }
}
protected void dismissKeyboardShortcuts() {
- KeyboardShortcuts.dismiss();
+ if (mIsShortcutListSearchEnabled && Utilities.isTablet(mContext)) {
+ KeyboardShortcutListSearch.dismiss();
+ } else {
+ KeyboardShortcuts.dismiss();
+ }
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
new file mode 100644
index 0000000..109f185
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 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.statusbar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardShortcutListSearchTest extends SysuiTestCase {
+
+ @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
+ private static int DEVICE_ID = 1;
+ private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
+
+ @Mock private BottomSheetDialog mBottomSheetDialog;
+ @Mock WindowManager mWindowManager;
+
+ @Before
+ public void setUp() {
+ mKeyboardShortcutListSearch = new KeyboardShortcutListSearch(mContext, mWindowManager);
+ mKeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+ mKeyboardShortcutListSearch.mKeyboardShortcutsBottomSheetDialog = mBottomSheetDialog;
+ mKeyboardShortcutListSearch.mContext = mContext;
+ }
+
+ @Test
+ public void toggle_isShowingTrue_instanceShouldBeNull() {
+ when(mBottomSheetDialog.isShowing()).thenReturn(true);
+
+ mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID);
+
+ assertThat(mKeyboardShortcutListSearch.sInstance).isNull();
+ }
+
+ @Test
+ public void toggle_isShowingFalse_showKeyboardShortcuts() {
+ when(mBottomSheetDialog.isShowing()).thenReturn(false);
+
+ mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID);
+
+ verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
new file mode 100644
index 0000000..bea2cfb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2023 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.statusbar;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.shared.recents.utilities.Utilities;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardShortcutsReceiverTest extends SysuiTestCase {
+
+ @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
+ private KeyboardShortcutsReceiver mKeyboardShortcutsReceiver;
+ private Intent mIntent;
+ private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+
+ @Mock private KeyboardShortcuts mKeyboardShortcuts;
+ @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
+
+ @Before
+ public void setUp() {
+ mIntent = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+ mKeyboardShortcuts.mContext = mContext;
+ mKeyboardShortcutListSearch.mContext = mContext;
+ KeyboardShortcuts.sInstance = mKeyboardShortcuts;
+ KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+ }
+
+ @Test
+ public void onReceive_whenFlagOffDeviceIsTablet_showKeyboardShortcuts() {
+ MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+ .spyStatic(Utilities.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+ when(Utilities.isTablet(mContext)).thenReturn(true);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
+ verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
+ mockitoSession.finishMocking();
+ }
+
+ @Test
+ public void onReceive_whenFlagOffDeviceIsNotTablet_showKeyboardShortcuts() {
+ MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+ .spyStatic(Utilities.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+ when(Utilities.isTablet(mContext)).thenReturn(false);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
+ verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
+ mockitoSession.finishMocking();
+ }
+
+ @Test
+ public void onReceive_whenFlagOnDeviceIsTablet_showKeyboardShortcutListSearch() {
+ MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+ .spyStatic(Utilities.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+ when(Utilities.isTablet(mContext)).thenReturn(true);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+
+ verify(mKeyboardShortcuts, never()).showKeyboardShortcuts(anyInt());
+ verify(mKeyboardShortcutListSearch).showKeyboardShortcuts(anyInt());
+ mockitoSession.finishMocking();
+ }
+
+ @Test
+ public void onReceive_whenFlagOnDeviceIsNotTablet_showKeyboardShortcuts() {
+ MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+ .spyStatic(Utilities.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+ when(Utilities.isTablet(mContext)).thenReturn(false);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
+ verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
+ mockitoSession.finishMocking();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
new file mode 100644
index 0000000..ea822aa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2023 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.statusbar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Dialog;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardShortcutsTest extends SysuiTestCase {
+
+ @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
+ private static int DEVICE_ID = 1;
+ private KeyboardShortcuts mKeyboardShortcuts;
+
+ @Mock private Dialog mDialog;
+ @Mock WindowManager mWindowManager;
+
+ @Before
+ public void setUp() {
+ mKeyboardShortcuts = new KeyboardShortcuts(mContext, mWindowManager);
+ mKeyboardShortcuts.sInstance = mKeyboardShortcuts;
+ mKeyboardShortcuts.mKeyboardShortcutsDialog = mDialog;
+ mKeyboardShortcuts.mContext = mContext;
+ }
+
+ @Test
+ public void toggle_isShowingTrue_instanceShouldBeNull() {
+ when(mDialog.isShowing()).thenReturn(true);
+
+ mKeyboardShortcuts.toggle(mContext, DEVICE_ID);
+
+ assertThat(mKeyboardShortcuts.sInstance).isNull();
+ }
+
+ @Test
+ public void toggle_isShowingFalse_showKeyboardShortcuts() {
+ when(mDialog.isShowing()).thenReturn(false);
+
+ mKeyboardShortcuts.toggle(mContext, DEVICE_ID);
+
+ verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 0605398..8b0d4ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -327,6 +327,8 @@
// CentralSurfacesImpl's runtime flag check fails if the flag is absent.
// This value is unused, because test manifest is opted in.
mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false);
+ // Set default value to avoid IllegalStateException.
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
IThermalService thermalService = mock(IThermalService.class);
mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,