1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar;
18 
19 import static android.content.Context.LAYOUT_INFLATER_SERVICE;
20 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
21 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.AlertDialog;
26 import android.app.AppGlobals;
27 import android.app.Dialog;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.DialogInterface.OnClickListener;
32 import android.content.Intent;
33 import android.content.pm.IPackageManager;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.ResolveInfo;
36 import android.content.res.ColorStateList;
37 import android.graphics.Bitmap;
38 import android.graphics.Canvas;
39 import android.graphics.drawable.Drawable;
40 import android.graphics.drawable.Icon;
41 import android.hardware.input.InputManager;
42 import android.os.Handler;
43 import android.os.Looper;
44 import android.os.RemoteException;
45 import android.util.Log;
46 import android.util.SparseArray;
47 import android.view.ContextThemeWrapper;
48 import android.view.InputDevice;
49 import android.view.KeyCharacterMap;
50 import android.view.KeyEvent;
51 import android.view.KeyboardShortcutGroup;
52 import android.view.KeyboardShortcutInfo;
53 import android.view.LayoutInflater;
54 import android.view.View;
55 import android.view.View.AccessibilityDelegate;
56 import android.view.ViewGroup;
57 import android.view.Window;
58 import android.view.WindowManager;
59 import android.view.accessibility.AccessibilityNodeInfo;
60 import android.widget.ImageView;
61 import android.widget.LinearLayout;
62 import android.widget.RelativeLayout;
63 import android.widget.TextView;
64 
65 import com.android.internal.annotations.VisibleForTesting;
66 import com.android.internal.app.AssistUtils;
67 import com.android.internal.logging.MetricsLogger;
68 import com.android.internal.logging.nano.MetricsProto;
69 import com.android.settingslib.Utils;
70 import com.android.systemui.R;
71 
72 import java.util.ArrayList;
73 import java.util.Collections;
74 import java.util.Comparator;
75 import java.util.List;
76 
77 /**
78  * Contains functionality for handling keyboard shortcuts.
79  */
80 public final class KeyboardShortcuts {
81     private static final String TAG = KeyboardShortcuts.class.getSimpleName();
82     private static final Object sLock = new Object();
83     @VisibleForTesting static KeyboardShortcuts sInstance;
84     private WindowManager mWindowManager;
85 
86     private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
87     private final SparseArray<String> mModifierNames = new SparseArray<>();
88     private final SparseArray<Drawable> mSpecialCharacterDrawables = new SparseArray<>();
89     private final SparseArray<Drawable> mModifierDrawables = new SparseArray<>();
90     // Ordered list of modifiers that are supported. All values in this array must exist in
91     // mModifierNames.
92     private final int[] mModifierList = new int[] {
93             KeyEvent.META_META_ON, KeyEvent.META_CTRL_ON, KeyEvent.META_ALT_ON,
94             KeyEvent.META_SHIFT_ON, KeyEvent.META_SYM_ON, KeyEvent.META_FUNCTION_ON
95     };
96 
97     private final Handler mHandler = new Handler(Looper.getMainLooper());
98     @VisibleForTesting Context mContext;
99     private final IPackageManager mPackageManager;
100     private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
101         public void onClick(DialogInterface dialog, int id) {
102             dismissKeyboardShortcuts();
103         }
104     };
105     private final Comparator<KeyboardShortcutInfo> mApplicationItemsComparator =
106             new Comparator<KeyboardShortcutInfo>() {
107                 @Override
108                 public int compare(KeyboardShortcutInfo ksh1, KeyboardShortcutInfo ksh2) {
109                     boolean ksh1ShouldBeLast = ksh1.getLabel() == null
110                             || ksh1.getLabel().toString().isEmpty();
111                     boolean ksh2ShouldBeLast = ksh2.getLabel() == null
112                             || ksh2.getLabel().toString().isEmpty();
113                     if (ksh1ShouldBeLast && ksh2ShouldBeLast) {
114                         return 0;
115                     }
116                     if (ksh1ShouldBeLast) {
117                         return 1;
118                     }
119                     if (ksh2ShouldBeLast) {
120                         return -1;
121                     }
122                     return (ksh1.getLabel().toString()).compareToIgnoreCase(
123                             ksh2.getLabel().toString());
124                 }
125             };
126 
127     @VisibleForTesting Dialog mKeyboardShortcutsDialog;
128     private KeyCharacterMap mKeyCharacterMap;
129     private KeyCharacterMap mBackupKeyCharacterMap;
130 
131     @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null;
132     @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null;
133 
134     @VisibleForTesting
KeyboardShortcuts(Context context, WindowManager windowManager)135     KeyboardShortcuts(Context context, WindowManager windowManager) {
136         this.mContext = new ContextThemeWrapper(
137                 context, android.R.style.Theme_DeviceDefault_Settings);
138         this.mPackageManager = AppGlobals.getPackageManager();
139         if (windowManager != null) {
140             this.mWindowManager = windowManager;
141         } else {
142             this.mWindowManager = mContext.getSystemService(WindowManager.class);
143         }
144         loadResources(context);
145     }
146 
getInstance(Context context)147     private static KeyboardShortcuts getInstance(Context context) {
148         if (sInstance == null) {
149             sInstance = new KeyboardShortcuts(context, null);
150         }
151         return sInstance;
152     }
153 
show(Context context, int deviceId)154     public static void show(Context context, int deviceId) {
155         MetricsLogger.visible(context,
156                 MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
157         synchronized (sLock) {
158             if (sInstance != null && !sInstance.mContext.equals(context)) {
159                 dismiss();
160             }
161             getInstance(context).showKeyboardShortcuts(deviceId);
162         }
163     }
164 
toggle(Context context, int deviceId)165     public static void toggle(Context context, int deviceId) {
166         synchronized (sLock) {
167             if (isShowing()) {
168                 dismiss();
169             } else {
170                 show(context, deviceId);
171             }
172         }
173     }
174 
dismiss()175     public static void dismiss() {
176         synchronized (sLock) {
177             if (sInstance != null) {
178                 MetricsLogger.hidden(sInstance.mContext,
179                         MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER);
180                 sInstance.dismissKeyboardShortcuts();
181                 sInstance = null;
182             }
183         }
184     }
185 
isShowing()186     private static boolean isShowing() {
187         return sInstance != null && sInstance.mKeyboardShortcutsDialog != null
188                 && sInstance.mKeyboardShortcutsDialog.isShowing();
189     }
190 
loadResources(Context context)191     private void loadResources(Context context) {
192         mSpecialCharacterNames.put(
193                 KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
194         mSpecialCharacterNames.put(
195                 KeyEvent.KEYCODE_BACK, context.getString(R.string.keyboard_key_back));
196         mSpecialCharacterNames.put(
197                 KeyEvent.KEYCODE_DPAD_UP, context.getString(R.string.keyboard_key_dpad_up));
198         mSpecialCharacterNames.put(
199                 KeyEvent.KEYCODE_DPAD_DOWN, context.getString(R.string.keyboard_key_dpad_down));
200         mSpecialCharacterNames.put(
201                 KeyEvent.KEYCODE_DPAD_LEFT, context.getString(R.string.keyboard_key_dpad_left));
202         mSpecialCharacterNames.put(
203                 KeyEvent.KEYCODE_DPAD_RIGHT, context.getString(R.string.keyboard_key_dpad_right));
204         mSpecialCharacterNames.put(
205                 KeyEvent.KEYCODE_DPAD_CENTER, context.getString(R.string.keyboard_key_dpad_center));
206         mSpecialCharacterNames.put(KeyEvent.KEYCODE_PERIOD, ".");
207         mSpecialCharacterNames.put(
208                 KeyEvent.KEYCODE_TAB, context.getString(R.string.keyboard_key_tab));
209         mSpecialCharacterNames.put(
210                 KeyEvent.KEYCODE_SPACE, context.getString(R.string.keyboard_key_space));
211         mSpecialCharacterNames.put(
212                 KeyEvent.KEYCODE_ENTER, context.getString(R.string.keyboard_key_enter));
213         mSpecialCharacterNames.put(
214                 KeyEvent.KEYCODE_DEL, context.getString(R.string.keyboard_key_backspace));
215         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
216                 context.getString(R.string.keyboard_key_media_play_pause));
217         mSpecialCharacterNames.put(
218                 KeyEvent.KEYCODE_MEDIA_STOP, context.getString(R.string.keyboard_key_media_stop));
219         mSpecialCharacterNames.put(
220                 KeyEvent.KEYCODE_MEDIA_NEXT, context.getString(R.string.keyboard_key_media_next));
221         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS,
222                 context.getString(R.string.keyboard_key_media_previous));
223         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_REWIND,
224                 context.getString(R.string.keyboard_key_media_rewind));
225         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
226                 context.getString(R.string.keyboard_key_media_fast_forward));
227         mSpecialCharacterNames.put(
228                 KeyEvent.KEYCODE_PAGE_UP, context.getString(R.string.keyboard_key_page_up));
229         mSpecialCharacterNames.put(
230                 KeyEvent.KEYCODE_PAGE_DOWN, context.getString(R.string.keyboard_key_page_down));
231         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_A,
232                 context.getString(R.string.keyboard_key_button_template, "A"));
233         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_B,
234                 context.getString(R.string.keyboard_key_button_template, "B"));
235         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_C,
236                 context.getString(R.string.keyboard_key_button_template, "C"));
237         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_X,
238                 context.getString(R.string.keyboard_key_button_template, "X"));
239         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Y,
240                 context.getString(R.string.keyboard_key_button_template, "Y"));
241         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Z,
242                 context.getString(R.string.keyboard_key_button_template, "Z"));
243         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L1,
244                 context.getString(R.string.keyboard_key_button_template, "L1"));
245         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R1,
246                 context.getString(R.string.keyboard_key_button_template, "R1"));
247         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L2,
248                 context.getString(R.string.keyboard_key_button_template, "L2"));
249         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R2,
250                 context.getString(R.string.keyboard_key_button_template, "R2"));
251         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_START,
252                 context.getString(R.string.keyboard_key_button_template, "Start"));
253         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_SELECT,
254                 context.getString(R.string.keyboard_key_button_template, "Select"));
255         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_MODE,
256                 context.getString(R.string.keyboard_key_button_template, "Mode"));
257         mSpecialCharacterNames.put(
258                 KeyEvent.KEYCODE_FORWARD_DEL, context.getString(R.string.keyboard_key_forward_del));
259         mSpecialCharacterNames.put(KeyEvent.KEYCODE_ESCAPE, "Esc");
260         mSpecialCharacterNames.put(KeyEvent.KEYCODE_SYSRQ, "SysRq");
261         mSpecialCharacterNames.put(KeyEvent.KEYCODE_BREAK, "Break");
262         mSpecialCharacterNames.put(KeyEvent.KEYCODE_SCROLL_LOCK, "Scroll Lock");
263         mSpecialCharacterNames.put(
264                 KeyEvent.KEYCODE_MOVE_HOME, context.getString(R.string.keyboard_key_move_home));
265         mSpecialCharacterNames.put(
266                 KeyEvent.KEYCODE_MOVE_END, context.getString(R.string.keyboard_key_move_end));
267         mSpecialCharacterNames.put(
268                 KeyEvent.KEYCODE_INSERT, context.getString(R.string.keyboard_key_insert));
269         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F1, "F1");
270         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F2, "F2");
271         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F3, "F3");
272         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F4, "F4");
273         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F5, "F5");
274         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F6, "F6");
275         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F7, "F7");
276         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F8, "F8");
277         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F9, "F9");
278         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F10, "F10");
279         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F11, "F11");
280         mSpecialCharacterNames.put(KeyEvent.KEYCODE_F12, "F12");
281         mSpecialCharacterNames.put(
282                 KeyEvent.KEYCODE_NUM_LOCK, context.getString(R.string.keyboard_key_num_lock));
283         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_0,
284                 context.getString(R.string.keyboard_key_numpad_template, "0"));
285         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_1,
286                 context.getString(R.string.keyboard_key_numpad_template, "1"));
287         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_2,
288                 context.getString(R.string.keyboard_key_numpad_template, "2"));
289         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_3,
290                 context.getString(R.string.keyboard_key_numpad_template, "3"));
291         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_4,
292                 context.getString(R.string.keyboard_key_numpad_template, "4"));
293         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_5,
294                 context.getString(R.string.keyboard_key_numpad_template, "5"));
295         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_6,
296                 context.getString(R.string.keyboard_key_numpad_template, "6"));
297         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_7,
298                 context.getString(R.string.keyboard_key_numpad_template, "7"));
299         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_8,
300                 context.getString(R.string.keyboard_key_numpad_template, "8"));
301         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_9,
302                 context.getString(R.string.keyboard_key_numpad_template, "9"));
303         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DIVIDE,
304                 context.getString(R.string.keyboard_key_numpad_template, "/"));
305         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
306                 context.getString(R.string.keyboard_key_numpad_template, "*"));
307         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
308                 context.getString(R.string.keyboard_key_numpad_template, "-"));
309         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ADD,
310                 context.getString(R.string.keyboard_key_numpad_template, "+"));
311         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DOT,
312                 context.getString(R.string.keyboard_key_numpad_template, "."));
313         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_COMMA,
314                 context.getString(R.string.keyboard_key_numpad_template, ","));
315         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ENTER,
316                 context.getString(R.string.keyboard_key_numpad_template,
317                         context.getString(R.string.keyboard_key_enter)));
318         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_EQUALS,
319                 context.getString(R.string.keyboard_key_numpad_template, "="));
320         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN,
321                 context.getString(R.string.keyboard_key_numpad_template, "("));
322         mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN,
323                 context.getString(R.string.keyboard_key_numpad_template, ")"));
324         mSpecialCharacterNames.put(KeyEvent.KEYCODE_ZENKAKU_HANKAKU, "半角/全角");
325         mSpecialCharacterNames.put(KeyEvent.KEYCODE_EISU, "英数");
326         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
327         mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
328         mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
329         mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt");
330         mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt");
331         mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl");
332         mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl");
333         mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift");
334         mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift");
335 
336         mModifierNames.put(KeyEvent.META_META_ON, "Meta");
337         mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
338         mModifierNames.put(KeyEvent.META_ALT_ON, "Alt");
339         mModifierNames.put(KeyEvent.META_SHIFT_ON, "Shift");
340         mModifierNames.put(KeyEvent.META_SYM_ON, "Sym");
341         mModifierNames.put(KeyEvent.META_FUNCTION_ON, "Fn");
342 
343         mSpecialCharacterDrawables.put(
344                 KeyEvent.KEYCODE_DEL, context.getDrawable(R.drawable.ic_ksh_key_backspace));
345         mSpecialCharacterDrawables.put(
346                 KeyEvent.KEYCODE_ENTER, context.getDrawable(R.drawable.ic_ksh_key_enter));
347         mSpecialCharacterDrawables.put(
348                 KeyEvent.KEYCODE_DPAD_UP, context.getDrawable(R.drawable.ic_ksh_key_up));
349         mSpecialCharacterDrawables.put(
350                 KeyEvent.KEYCODE_DPAD_RIGHT, context.getDrawable(R.drawable.ic_ksh_key_right));
351         mSpecialCharacterDrawables.put(
352                 KeyEvent.KEYCODE_DPAD_DOWN, context.getDrawable(R.drawable.ic_ksh_key_down));
353         mSpecialCharacterDrawables.put(
354                 KeyEvent.KEYCODE_DPAD_LEFT, context.getDrawable(R.drawable.ic_ksh_key_left));
355 
356         mModifierDrawables.put(
357                 KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
358     }
359 
360     /**
361      * Retrieves a {@link KeyCharacterMap} and assigns it to mKeyCharacterMap. If the given id is an
362      * existing device, that device's map is used. Otherwise, it checks first all available devices
363      * and if there is a full keyboard it uses that map, otherwise falls back to the Virtual
364      * Keyboard with its default map.
365      */
retrieveKeyCharacterMap(int deviceId)366     private void retrieveKeyCharacterMap(int deviceId) {
367         final InputManager inputManager = mContext.getSystemService(InputManager.class);
368         mBackupKeyCharacterMap = inputManager.getInputDevice(-1).getKeyCharacterMap();
369         if (deviceId != -1) {
370             final InputDevice inputDevice = inputManager.getInputDevice(deviceId);
371             if (inputDevice != null) {
372                 mKeyCharacterMap = inputDevice.getKeyCharacterMap();
373                 return;
374             }
375         }
376         final int[] deviceIds = inputManager.getInputDeviceIds();
377         for (int i = 0; i < deviceIds.length; ++i) {
378             final InputDevice inputDevice = inputManager.getInputDevice(deviceIds[i]);
379             // -1 is the Virtual Keyboard, with the default key map. Use that one only as last
380             // resort.
381             if (inputDevice.getId() != -1 && inputDevice.isFullKeyboard()) {
382                 mKeyCharacterMap = inputDevice.getKeyCharacterMap();
383                 return;
384             }
385         }
386         // Fall back to -1, the virtual keyboard.
387         mKeyCharacterMap = mBackupKeyCharacterMap;
388     }
389 
390     @VisibleForTesting
showKeyboardShortcuts(int deviceId)391     void showKeyboardShortcuts(int deviceId) {
392         retrieveKeyCharacterMap(deviceId);
393         mReceivedAppShortcutGroups = null;
394         mReceivedImeShortcutGroups = null;
395         mWindowManager.requestAppKeyboardShortcuts(
396                 result -> {
397                     mReceivedAppShortcutGroups = result;
398                     maybeMergeAndShowKeyboardShortcuts();
399                 }, deviceId);
400         mWindowManager.requestImeKeyboardShortcuts(
401                 result -> {
402                     mReceivedImeShortcutGroups = result;
403                     maybeMergeAndShowKeyboardShortcuts();
404                 }, deviceId);
405     }
406 
maybeMergeAndShowKeyboardShortcuts()407     private void maybeMergeAndShowKeyboardShortcuts() {
408         if (mReceivedAppShortcutGroups == null || mReceivedImeShortcutGroups == null) {
409             return;
410         }
411         List<KeyboardShortcutGroup> shortcutGroups = mReceivedAppShortcutGroups;
412         shortcutGroups.addAll(mReceivedImeShortcutGroups);
413         mReceivedAppShortcutGroups = null;
414         mReceivedImeShortcutGroups = null;
415 
416         final KeyboardShortcutGroup defaultAppShortcuts =
417                 getDefaultApplicationShortcuts();
418         if (defaultAppShortcuts != null) {
419             shortcutGroups.add(defaultAppShortcuts);
420         }
421         shortcutGroups.add(getSystemShortcuts());
422         showKeyboardShortcutsDialog(shortcutGroups);
423     }
424 
dismissKeyboardShortcuts()425     private void dismissKeyboardShortcuts() {
426         if (mKeyboardShortcutsDialog != null) {
427             mKeyboardShortcutsDialog.dismiss();
428             mKeyboardShortcutsDialog = null;
429         }
430     }
431 
getSystemShortcuts()432     private KeyboardShortcutGroup getSystemShortcuts() {
433         final KeyboardShortcutGroup systemGroup = new KeyboardShortcutGroup(
434                 mContext.getString(R.string.keyboard_shortcut_group_system), true);
435         systemGroup.addItem(new KeyboardShortcutInfo(
436                 mContext.getString(R.string.keyboard_shortcut_group_system_home),
437                 KeyEvent.KEYCODE_ENTER,
438                 KeyEvent.META_META_ON));
439         systemGroup.addItem(new KeyboardShortcutInfo(
440                 mContext.getString(R.string.keyboard_shortcut_group_system_back),
441                 KeyEvent.KEYCODE_DEL,
442                 KeyEvent.META_META_ON));
443         systemGroup.addItem(new KeyboardShortcutInfo(
444                 mContext.getString(R.string.keyboard_shortcut_group_system_recents),
445                 KeyEvent.KEYCODE_TAB,
446                 KeyEvent.META_ALT_ON));
447         systemGroup.addItem(new KeyboardShortcutInfo(
448                 mContext.getString(
449                         R.string.keyboard_shortcut_group_system_notifications),
450                 KeyEvent.KEYCODE_N,
451                 KeyEvent.META_META_ON));
452         systemGroup.addItem(new KeyboardShortcutInfo(
453                 mContext.getString(
454                         R.string.keyboard_shortcut_group_system_shortcuts_helper),
455                 KeyEvent.KEYCODE_SLASH,
456                 KeyEvent.META_META_ON));
457         systemGroup.addItem(new KeyboardShortcutInfo(
458                 mContext.getString(
459                         R.string.keyboard_shortcut_group_system_switch_input),
460                 KeyEvent.KEYCODE_SPACE,
461                 KeyEvent.META_META_ON));
462         return systemGroup;
463     }
464 
getDefaultApplicationShortcuts()465     private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
466         final int userId = mContext.getUserId();
467         List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
468 
469         // Assist.
470         final AssistUtils assistUtils = new AssistUtils(mContext);
471         final ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
472         // Not all devices have an assist component.
473         if (assistComponent != null) {
474             PackageInfo assistPackageInfo = null;
475             try {
476                 assistPackageInfo = mPackageManager.getPackageInfo(
477                         assistComponent.getPackageName(), 0, userId);
478             } catch (RemoteException e) {
479                 Log.e(TAG, "PackageManagerService is dead");
480             }
481 
482             if (assistPackageInfo != null) {
483                 final Icon assistIcon = Icon.createWithResource(
484                         assistPackageInfo.applicationInfo.packageName,
485                         assistPackageInfo.applicationInfo.icon);
486 
487                 keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
488                         mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
489                         assistIcon,
490                         KeyEvent.KEYCODE_UNKNOWN,
491                         KeyEvent.META_META_ON));
492             }
493         }
494 
495         // Browser.
496         final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
497         if (browserIcon != null) {
498             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
499                     mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
500                     browserIcon,
501                     KeyEvent.KEYCODE_B,
502                     KeyEvent.META_META_ON));
503         }
504 
505 
506         // Contacts.
507         final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
508         if (contactsIcon != null) {
509             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
510                     mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
511                     contactsIcon,
512                     KeyEvent.KEYCODE_C,
513                     KeyEvent.META_META_ON));
514         }
515 
516         // Email.
517         final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
518         if (emailIcon != null) {
519             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
520                     mContext.getString(R.string.keyboard_shortcut_group_applications_email),
521                     emailIcon,
522                     KeyEvent.KEYCODE_E,
523                     KeyEvent.META_META_ON));
524         }
525 
526         // Messaging.
527         final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
528         if (messagingIcon != null) {
529             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
530                     mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
531                     messagingIcon,
532                     KeyEvent.KEYCODE_S,
533                     KeyEvent.META_META_ON));
534         }
535 
536         // Music.
537         final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
538         if (musicIcon != null) {
539             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
540                     mContext.getString(R.string.keyboard_shortcut_group_applications_music),
541                     musicIcon,
542                     KeyEvent.KEYCODE_P,
543                     KeyEvent.META_META_ON));
544         }
545 
546         // Calendar.
547         final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
548         if (calendarIcon != null) {
549             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
550                     mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
551                     calendarIcon,
552                     KeyEvent.KEYCODE_L,
553                     KeyEvent.META_META_ON));
554         }
555 
556         final int itemsSize = keyboardShortcutInfoAppItems.size();
557         if (itemsSize == 0) {
558             return null;
559         }
560 
561         // Sorts by label, case insensitive with nulls and/or empty labels last.
562         Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
563         return new KeyboardShortcutGroup(
564                 mContext.getString(R.string.keyboard_shortcut_group_applications),
565                 keyboardShortcutInfoAppItems,
566                 true);
567     }
568 
getIconForIntentCategory(String intentCategory, int userId)569     private Icon getIconForIntentCategory(String intentCategory, int userId) {
570         final Intent intent = new Intent(Intent.ACTION_MAIN);
571         intent.addCategory(intentCategory);
572 
573         final PackageInfo packageInfo = getPackageInfoForIntent(intent, userId);
574         if (packageInfo != null && packageInfo.applicationInfo.icon != 0) {
575             return Icon.createWithResource(
576                     packageInfo.applicationInfo.packageName,
577                     packageInfo.applicationInfo.icon);
578         }
579         return null;
580     }
581 
getPackageInfoForIntent(Intent intent, int userId)582     private PackageInfo getPackageInfoForIntent(Intent intent, int userId) {
583         try {
584             ResolveInfo handler;
585             handler = mPackageManager.resolveIntent(
586                     intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId);
587             if (handler == null || handler.activityInfo == null) {
588                 return null;
589             }
590             return mPackageManager.getPackageInfo(handler.activityInfo.packageName, 0, userId);
591         } catch (RemoteException e) {
592             Log.e(TAG, "PackageManagerService is dead", e);
593             return null;
594         }
595     }
596 
showKeyboardShortcutsDialog( final List<KeyboardShortcutGroup> keyboardShortcutGroups)597     private void showKeyboardShortcutsDialog(
598             final List<KeyboardShortcutGroup> keyboardShortcutGroups) {
599         // Need to post on the main thread.
600         mHandler.post(new Runnable() {
601             @Override
602             public void run() {
603                 handleShowKeyboardShortcuts(keyboardShortcutGroups);
604             }
605         });
606     }
607 
handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups)608     private void handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups) {
609         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
610         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
611                 LAYOUT_INFLATER_SERVICE);
612         final View keyboardShortcutsView = inflater.inflate(
613                 R.layout.keyboard_shortcuts_view, null);
614         populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
615                 R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
616         dialogBuilder.setView(keyboardShortcutsView);
617         dialogBuilder.setPositiveButton(R.string.quick_settings_done, mDialogCloseListener);
618         mKeyboardShortcutsDialog = dialogBuilder.create();
619         mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
620         Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
621         keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
622         synchronized (sLock) {
623             // showKeyboardShortcutsDialog only if it has not been dismissed already
624             if (sInstance != null) {
625                 mKeyboardShortcutsDialog.show();
626             }
627         }
628     }
629 
populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout, List<KeyboardShortcutGroup> keyboardShortcutGroups)630     private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
631             List<KeyboardShortcutGroup> keyboardShortcutGroups) {
632         LayoutInflater inflater = LayoutInflater.from(mContext);
633         final int keyboardShortcutGroupsSize = keyboardShortcutGroups.size();
634         TextView shortcutsKeyView = (TextView) inflater.inflate(
635                 R.layout.keyboard_shortcuts_key_view, null, false);
636         shortcutsKeyView.measure(
637                 View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
638         final int shortcutKeyTextItemMinWidth = shortcutsKeyView.getMeasuredHeight();
639         // Needed to be able to scale the image items to the same height as the text items.
640         final int shortcutKeyIconItemHeightWidth = shortcutsKeyView.getMeasuredHeight()
641                 - shortcutsKeyView.getPaddingTop()
642                 - shortcutsKeyView.getPaddingBottom();
643         for (int i = 0; i < keyboardShortcutGroupsSize; i++) {
644             KeyboardShortcutGroup group = keyboardShortcutGroups.get(i);
645             TextView categoryTitle = (TextView) inflater.inflate(
646                     R.layout.keyboard_shortcuts_category_title, keyboardShortcutsLayout, false);
647             categoryTitle.setText(group.getLabel());
648             categoryTitle.setTextColor(group.isSystemGroup() ? Utils.getColorAccent(mContext) :
649                     ColorStateList.valueOf(mContext.getColor(R.color.ksh_application_group_color)));
650             keyboardShortcutsLayout.addView(categoryTitle);
651 
652             LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
653                     R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
654             final int itemsSize = group.getItems().size();
655             for (int j = 0; j < itemsSize; j++) {
656                 KeyboardShortcutInfo info = group.getItems().get(j);
657                 List<StringDrawableContainer> shortcutKeys = getHumanReadableShortcutKeys(info);
658                 if (shortcutKeys == null) {
659                     // Ignore shortcuts we can't display keys for.
660                     Log.w(TAG, "Keyboard Shortcut contains unsupported keys, skipping.");
661                     continue;
662                 }
663                 View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
664                         shortcutContainer, false);
665 
666                 if (info.getIcon() != null) {
667                     ImageView shortcutIcon = (ImageView) shortcutView
668                             .findViewById(R.id.keyboard_shortcuts_icon);
669                     shortcutIcon.setImageIcon(info.getIcon());
670                     shortcutIcon.setVisibility(View.VISIBLE);
671                 }
672 
673                 TextView shortcutKeyword = (TextView) shortcutView
674                         .findViewById(R.id.keyboard_shortcuts_keyword);
675                 shortcutKeyword.setText(info.getLabel());
676                 if (info.getIcon() != null) {
677                     RelativeLayout.LayoutParams lp =
678                             (RelativeLayout.LayoutParams) shortcutKeyword.getLayoutParams();
679                     lp.removeRule(RelativeLayout.ALIGN_PARENT_START);
680                     shortcutKeyword.setLayoutParams(lp);
681                 }
682 
683                 ViewGroup shortcutItemsContainer = (ViewGroup) shortcutView
684                         .findViewById(R.id.keyboard_shortcuts_item_container);
685                 final int shortcutKeysSize = shortcutKeys.size();
686                 for (int k = 0; k < shortcutKeysSize; k++) {
687                     StringDrawableContainer shortcutRepresentation = shortcutKeys.get(k);
688                     if (shortcutRepresentation.mDrawable != null) {
689                         ImageView shortcutKeyIconView = (ImageView) inflater.inflate(
690                                 R.layout.keyboard_shortcuts_key_icon_view, shortcutItemsContainer,
691                                 false);
692                         Bitmap bitmap = Bitmap.createBitmap(shortcutKeyIconItemHeightWidth,
693                                 shortcutKeyIconItemHeightWidth, Bitmap.Config.ARGB_8888);
694                         Canvas canvas = new Canvas(bitmap);
695                         shortcutRepresentation.mDrawable.setBounds(0, 0, canvas.getWidth(),
696                                 canvas.getHeight());
697                         shortcutRepresentation.mDrawable.draw(canvas);
698                         shortcutKeyIconView.setImageBitmap(bitmap);
699                         shortcutKeyIconView.setImportantForAccessibility(
700                                 IMPORTANT_FOR_ACCESSIBILITY_YES);
701                         shortcutKeyIconView.setAccessibilityDelegate(
702                                 new ShortcutKeyAccessibilityDelegate(
703                                         shortcutRepresentation.mString));
704                         shortcutItemsContainer.addView(shortcutKeyIconView);
705                     } else if (shortcutRepresentation.mString != null) {
706                         TextView shortcutKeyTextView = (TextView) inflater.inflate(
707                                 R.layout.keyboard_shortcuts_key_view, shortcutItemsContainer,
708                                 false);
709                         shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
710                         shortcutKeyTextView.setText(shortcutRepresentation.mString);
711                         shortcutKeyTextView.setAccessibilityDelegate(
712                                 new ShortcutKeyAccessibilityDelegate(
713                                         shortcutRepresentation.mString));
714                         shortcutItemsContainer.addView(shortcutKeyTextView);
715                     }
716                 }
717                 shortcutContainer.addView(shortcutView);
718             }
719             keyboardShortcutsLayout.addView(shortcutContainer);
720             if (i < keyboardShortcutGroupsSize - 1) {
721                 View separator = inflater.inflate(
722                         R.layout.keyboard_shortcuts_category_separator, keyboardShortcutsLayout,
723                         false);
724                 keyboardShortcutsLayout.addView(separator);
725             }
726         }
727     }
728 
getHumanReadableShortcutKeys(KeyboardShortcutInfo info)729     private List<StringDrawableContainer> getHumanReadableShortcutKeys(KeyboardShortcutInfo info) {
730         List<StringDrawableContainer> shortcutKeys = getHumanReadableModifiers(info);
731         if (shortcutKeys == null) {
732             return null;
733         }
734         String shortcutKeyString = null;
735         Drawable shortcutKeyDrawable = null;
736         if (info.getBaseCharacter() > Character.MIN_VALUE) {
737             shortcutKeyString = String.valueOf(info.getBaseCharacter());
738         } else if (mSpecialCharacterDrawables.get(info.getKeycode()) != null) {
739             shortcutKeyDrawable = mSpecialCharacterDrawables.get(info.getKeycode());
740             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
741         } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
742             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
743         } else {
744             // Special case for shortcuts with no base key or keycode.
745             if (info.getKeycode() == KeyEvent.KEYCODE_UNKNOWN) {
746                 return shortcutKeys;
747             }
748             char displayLabel = mKeyCharacterMap.getDisplayLabel(info.getKeycode());
749             if (displayLabel != 0) {
750                 shortcutKeyString = String.valueOf(displayLabel);
751             } else {
752                 displayLabel = mBackupKeyCharacterMap.getDisplayLabel(info.getKeycode());
753                 if (displayLabel != 0) {
754                     shortcutKeyString = String.valueOf(displayLabel);
755                 } else {
756                     return null;
757                 }
758             }
759         }
760 
761         if (shortcutKeyString != null) {
762             shortcutKeys.add(new StringDrawableContainer(shortcutKeyString, shortcutKeyDrawable));
763         } else {
764             Log.w(TAG, "Keyboard Shortcut does not have a text representation, skipping.");
765         }
766 
767         return shortcutKeys;
768     }
769 
getHumanReadableModifiers(KeyboardShortcutInfo info)770     private List<StringDrawableContainer> getHumanReadableModifiers(KeyboardShortcutInfo info) {
771         final List<StringDrawableContainer> shortcutKeys = new ArrayList<>();
772         int modifiers = info.getModifiers();
773         if (modifiers == 0) {
774             return shortcutKeys;
775         }
776         for(int i = 0; i < mModifierList.length; ++i) {
777             final int supportedModifier = mModifierList[i];
778             if ((modifiers & supportedModifier) != 0) {
779                 shortcutKeys.add(new StringDrawableContainer(
780                         mModifierNames.get(supportedModifier),
781                         mModifierDrawables.get(supportedModifier)));
782                 modifiers &= ~supportedModifier;
783             }
784         }
785         if (modifiers != 0) {
786             // Remaining unsupported modifiers, don't show anything.
787             return null;
788         }
789         return shortcutKeys;
790     }
791 
792     private final class ShortcutKeyAccessibilityDelegate extends AccessibilityDelegate {
793         private String mContentDescription;
794 
ShortcutKeyAccessibilityDelegate(String contentDescription)795         ShortcutKeyAccessibilityDelegate(String contentDescription) {
796             mContentDescription = contentDescription;
797         }
798 
799         @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)800         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
801             super.onInitializeAccessibilityNodeInfo(host, info);
802             if (mContentDescription != null) {
803                 info.setContentDescription(mContentDescription.toLowerCase());
804             }
805         }
806     }
807 
808     private static final class StringDrawableContainer {
809         @NonNull
810         public String mString;
811         @Nullable
812         public Drawable mDrawable;
813 
StringDrawableContainer(String string, Drawable drawable)814         StringDrawableContainer(String string, Drawable drawable) {
815             mString = string;
816             mDrawable = drawable;
817         }
818     }
819 }
820