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