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