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