1 /* 2 * Copyright (C) 2021 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.server.policy; 18 19 import android.annotation.SuppressLint; 20 import android.app.role.RoleManager; 21 import android.content.ActivityNotFoundException; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.res.XmlResourceParser; 27 import android.hardware.input.InputManager; 28 import android.os.Handler; 29 import android.os.RemoteException; 30 import android.os.UserHandle; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.LongSparseArray; 34 import android.util.Slog; 35 import android.util.SparseArray; 36 import android.view.InputDevice; 37 import android.view.KeyCharacterMap; 38 import android.view.KeyEvent; 39 40 import com.android.internal.policy.IShortcutService; 41 import com.android.internal.util.XmlUtils; 42 import com.android.server.input.KeyboardMetricsCollector; 43 import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; 44 45 import org.xmlpull.v1.XmlPullParser; 46 import org.xmlpull.v1.XmlPullParserException; 47 48 import java.io.IOException; 49 import java.util.HashMap; 50 import java.util.Map; 51 52 /** 53 * Manages quick launch shortcuts by: 54 * <li> Keeping the local copy in sync with the database (this is an observer) 55 * <li> Returning a shortcut-matching intent to clients 56 * <li> Returning particular kind of application intent by special key. 57 */ 58 public class ModifierShortcutManager { 59 private static final String TAG = "ModifierShortcutManager"; 60 61 private static final String TAG_BOOKMARKS = "bookmarks"; 62 private static final String TAG_BOOKMARK = "bookmark"; 63 64 private static final String ATTRIBUTE_PACKAGE = "package"; 65 private static final String ATTRIBUTE_CLASS = "class"; 66 private static final String ATTRIBUTE_SHORTCUT = "shortcut"; 67 private static final String ATTRIBUTE_CATEGORY = "category"; 68 private static final String ATTRIBUTE_SHIFT = "shift"; 69 private static final String ATTRIBUTE_ROLE = "role"; 70 71 private final SparseArray<Intent> mIntentShortcuts = new SparseArray<>(); 72 private final SparseArray<Intent> mShiftShortcuts = new SparseArray<>(); 73 private final SparseArray<String> mRoleShortcuts = new SparseArray<String>(); 74 private final SparseArray<String> mShiftRoleShortcuts = new SparseArray<String>(); 75 private final Map<String, Intent> mRoleIntents = new HashMap<String, Intent>(); 76 77 private LongSparseArray<IShortcutService> mShortcutKeyServices = new LongSparseArray<>(); 78 79 /* Table of Application Launch keys. Maps from key codes to intent categories. 80 * 81 * These are special keys that are used to launch particular kinds of applications, 82 * such as a web browser. HID defines nearly a hundred of them in the Consumer (0x0C) 83 * usage page. We don't support quite that many yet... 84 */ 85 static SparseArray<String> sApplicationLaunchKeyCategories; 86 static SparseArray<String> sApplicationLaunchKeyRoles; 87 static { 88 sApplicationLaunchKeyRoles = new SparseArray<String>(); 89 sApplicationLaunchKeyCategories = new SparseArray<String>(); sApplicationLaunchKeyRoles.append( KeyEvent.KEYCODE_EXPLORER, RoleManager.ROLE_BROWSER)90 sApplicationLaunchKeyRoles.append( 91 KeyEvent.KEYCODE_EXPLORER, RoleManager.ROLE_BROWSER); sApplicationLaunchKeyCategories.append( KeyEvent.KEYCODE_ENVELOPE, Intent.CATEGORY_APP_EMAIL)92 sApplicationLaunchKeyCategories.append( 93 KeyEvent.KEYCODE_ENVELOPE, Intent.CATEGORY_APP_EMAIL); sApplicationLaunchKeyCategories.append( KeyEvent.KEYCODE_CONTACTS, Intent.CATEGORY_APP_CONTACTS)94 sApplicationLaunchKeyCategories.append( 95 KeyEvent.KEYCODE_CONTACTS, Intent.CATEGORY_APP_CONTACTS); sApplicationLaunchKeyCategories.append( KeyEvent.KEYCODE_CALENDAR, Intent.CATEGORY_APP_CALENDAR)96 sApplicationLaunchKeyCategories.append( 97 KeyEvent.KEYCODE_CALENDAR, Intent.CATEGORY_APP_CALENDAR); sApplicationLaunchKeyCategories.append( KeyEvent.KEYCODE_MUSIC, Intent.CATEGORY_APP_MUSIC)98 sApplicationLaunchKeyCategories.append( 99 KeyEvent.KEYCODE_MUSIC, Intent.CATEGORY_APP_MUSIC); sApplicationLaunchKeyCategories.append( KeyEvent.KEYCODE_CALCULATOR, Intent.CATEGORY_APP_CALCULATOR)100 sApplicationLaunchKeyCategories.append( 101 KeyEvent.KEYCODE_CALCULATOR, Intent.CATEGORY_APP_CALCULATOR); 102 } 103 104 public static final String EXTRA_ROLE = 105 "com.android.server.policy.ModifierShortcutManager.EXTRA_ROLE"; 106 107 private final Context mContext; 108 private final Handler mHandler; 109 private final RoleManager mRoleManager; 110 private final PackageManager mPackageManager; 111 private boolean mSearchKeyShortcutPending = false; 112 private boolean mConsumeSearchKeyUp = true; 113 ModifierShortcutManager(Context context, Handler handler)114 ModifierShortcutManager(Context context, Handler handler) { 115 mContext = context; 116 mHandler = handler; 117 mPackageManager = mContext.getPackageManager(); 118 mRoleManager = mContext.getSystemService(RoleManager.class); 119 mRoleManager.addOnRoleHoldersChangedListenerAsUser(mContext.getMainExecutor(), 120 (String roleName, UserHandle user) -> { 121 mRoleIntents.remove(roleName); 122 }, UserHandle.ALL); 123 loadShortcuts(); 124 } 125 126 /** 127 * Gets the shortcut intent for a given keycode+modifier. Make sure you 128 * strip whatever modifier is used for invoking shortcuts (for example, 129 * if 'Sym+A' should invoke a shortcut on 'A', you should strip the 130 * 'Sym' bit from the modifiers before calling this method. 131 * <p> 132 * This will first try an exact match (with modifiers), and then try a 133 * match without modifiers (primary character on a key). 134 * 135 * @param kcm The key character map of the device on which the key was pressed. 136 * @param keyCode The key code. 137 * @param metaState The meta state, omitting any modifiers that were used 138 * to invoke the shortcut. 139 * @return The intent that matches the shortcut, or null if not found. 140 */ getIntent(KeyCharacterMap kcm, int keyCode, int metaState)141 private Intent getIntent(KeyCharacterMap kcm, int keyCode, int metaState) { 142 // If a modifier key other than shift is also pressed, skip it. 143 final boolean isShiftOn = KeyEvent.metaStateHasModifiers(metaState, KeyEvent.META_SHIFT_ON); 144 if (!isShiftOn && !KeyEvent.metaStateHasNoModifiers(metaState)) { 145 return null; 146 } 147 148 Intent shortcutIntent = null; 149 150 // If the Shift key is pressed, then search for the shift shortcuts. 151 SparseArray<Intent> shortcutMap = isShiftOn ? mShiftShortcuts : mIntentShortcuts; 152 153 // First try the exact keycode (with modifiers). 154 int shortcutChar = kcm.get(keyCode, metaState); 155 if (shortcutChar != 0) { 156 shortcutIntent = shortcutMap.get(shortcutChar); 157 } 158 159 // Next try the primary character on that key. 160 if (shortcutIntent == null) { 161 shortcutChar = Character.toLowerCase(kcm.getDisplayLabel(keyCode)); 162 if (shortcutChar != 0) { 163 shortcutIntent = shortcutMap.get(shortcutChar); 164 165 if (shortcutIntent == null) { 166 // Check for role based shortcut 167 String role = isShiftOn ? mShiftRoleShortcuts.get(shortcutChar) 168 : mRoleShortcuts.get(shortcutChar); 169 if (role != null) { 170 shortcutIntent = getRoleLaunchIntent(role); 171 } 172 } 173 } 174 } 175 176 return shortcutIntent; 177 } 178 getRoleLaunchIntent(String role)179 private Intent getRoleLaunchIntent(String role) { 180 Intent intent = mRoleIntents.get(role); 181 if (intent == null) { 182 if (mRoleManager.isRoleAvailable(role)) { 183 String rolePackage = mRoleManager.getDefaultApplication(role); 184 if (rolePackage != null) { 185 intent = mPackageManager.getLaunchIntentForPackage(rolePackage); 186 intent.putExtra(EXTRA_ROLE, role); 187 mRoleIntents.put(role, intent); 188 } else { 189 Log.w(TAG, "No default application for role " + role); 190 } 191 } else { 192 Log.w(TAG, "Role " + role + " is not available."); 193 } 194 } 195 return intent; 196 } 197 loadShortcuts()198 private void loadShortcuts() { 199 200 try { 201 XmlResourceParser parser = mContext.getResources().getXml( 202 com.android.internal.R.xml.bookmarks); 203 XmlUtils.beginDocument(parser, TAG_BOOKMARKS); 204 205 while (true) { 206 XmlUtils.nextElement(parser); 207 208 if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { 209 break; 210 } 211 212 if (!TAG_BOOKMARK.equals(parser.getName())) { 213 break; 214 } 215 216 String packageName = parser.getAttributeValue(null, ATTRIBUTE_PACKAGE); 217 String className = parser.getAttributeValue(null, ATTRIBUTE_CLASS); 218 String shortcutName = parser.getAttributeValue(null, ATTRIBUTE_SHORTCUT); 219 String categoryName = parser.getAttributeValue(null, ATTRIBUTE_CATEGORY); 220 String shiftName = parser.getAttributeValue(null, ATTRIBUTE_SHIFT); 221 String roleName = parser.getAttributeValue(null, ATTRIBUTE_ROLE); 222 223 if (TextUtils.isEmpty(shortcutName)) { 224 Log.w(TAG, "Shortcut required for bookmark with category=" + categoryName 225 + " packageName=" + packageName + " className=" + className 226 + " role=" + roleName + "shiftName=" + shiftName); 227 continue; 228 } 229 230 final int shortcutChar = shortcutName.charAt(0); 231 final boolean isShiftShortcut = (shiftName != null && shiftName.equals("true")); 232 final Intent intent; 233 if (packageName != null && className != null) { 234 if (roleName != null || categoryName != null) { 235 Log.w(TAG, "Cannot specify role or category when package and class" 236 + " are present for bookmark packageName=" + packageName 237 + " className=" + className + " shortcutChar=" + shortcutChar); 238 continue; 239 } 240 ComponentName componentName = new ComponentName(packageName, className); 241 try { 242 mPackageManager.getActivityInfo(componentName, 243 PackageManager.MATCH_DIRECT_BOOT_AWARE 244 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 245 | PackageManager.MATCH_UNINSTALLED_PACKAGES); 246 } catch (PackageManager.NameNotFoundException e) { 247 String[] packages = mPackageManager.canonicalToCurrentPackageNames( 248 new String[] { packageName }); 249 componentName = new ComponentName(packages[0], className); 250 try { 251 mPackageManager.getActivityInfo(componentName, 252 PackageManager.MATCH_DIRECT_BOOT_AWARE 253 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 254 | PackageManager.MATCH_UNINSTALLED_PACKAGES); 255 } catch (PackageManager.NameNotFoundException e1) { 256 Log.w(TAG, "Unable to add bookmark: " + packageName 257 + "/" + className + " not found."); 258 continue; 259 } 260 } 261 262 intent = new Intent(Intent.ACTION_MAIN); 263 intent.addCategory(Intent.CATEGORY_LAUNCHER); 264 intent.setComponent(componentName); 265 } else if (categoryName != null) { 266 if (roleName != null) { 267 Log.w(TAG, "Cannot specify role bookmark when category is present for" 268 + " bookmark shortcutChar=" + shortcutChar 269 + " category= " + categoryName); 270 continue; 271 } 272 intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, categoryName); 273 } else if (roleName != null) { 274 // We can't resolve the role at the time of this file being parsed as the 275 // device hasn't finished booting, so we will look it up lazily. 276 if (isShiftShortcut) { 277 mShiftRoleShortcuts.put(shortcutChar, roleName); 278 } else { 279 mRoleShortcuts.put(shortcutChar, roleName); 280 } 281 continue; 282 } else { 283 Log.w(TAG, "Unable to add bookmark for shortcut " + shortcutName 284 + ": missing package/class, category or role attributes"); 285 continue; 286 } 287 288 if (isShiftShortcut) { 289 mShiftShortcuts.put(shortcutChar, intent); 290 } else { 291 mIntentShortcuts.put(shortcutChar, intent); 292 } 293 } 294 } catch (XmlPullParserException | IOException e) { 295 Log.e(TAG, "Got exception parsing bookmarks.", e); 296 } 297 } 298 registerShortcutKey(long shortcutCode, IShortcutService shortcutService)299 void registerShortcutKey(long shortcutCode, IShortcutService shortcutService) 300 throws RemoteException { 301 IShortcutService service = mShortcutKeyServices.get(shortcutCode); 302 if (service != null && service.asBinder().pingBinder()) { 303 throw new RemoteException("Key already exists."); 304 } 305 306 mShortcutKeyServices.put(shortcutCode, shortcutService); 307 } 308 309 /** 310 * Handle the shortcut to {@link IShortcutService} 311 * @param keyCode The key code of the event. 312 * @param metaState The meta key modifier state. 313 * @return True if invoked the shortcut, otherwise false. 314 */ handleShortcutService(int keyCode, int metaState)315 private boolean handleShortcutService(int keyCode, int metaState) { 316 long shortcutCode = keyCode; 317 if ((metaState & KeyEvent.META_CTRL_ON) != 0) { 318 shortcutCode |= ((long) KeyEvent.META_CTRL_ON) << Integer.SIZE; 319 } 320 321 if ((metaState & KeyEvent.META_ALT_ON) != 0) { 322 shortcutCode |= ((long) KeyEvent.META_ALT_ON) << Integer.SIZE; 323 } 324 325 if ((metaState & KeyEvent.META_SHIFT_ON) != 0) { 326 shortcutCode |= ((long) KeyEvent.META_SHIFT_ON) << Integer.SIZE; 327 } 328 329 if ((metaState & KeyEvent.META_META_ON) != 0) { 330 shortcutCode |= ((long) KeyEvent.META_META_ON) << Integer.SIZE; 331 } 332 333 IShortcutService shortcutService = mShortcutKeyServices.get(shortcutCode); 334 if (shortcutService != null) { 335 try { 336 shortcutService.notifyShortcutKeyPressed(shortcutCode); 337 } catch (RemoteException e) { 338 mShortcutKeyServices.delete(shortcutCode); 339 } 340 return true; 341 } 342 return false; 343 } 344 345 /** 346 * Handle the shortcut to {@link Intent} 347 * 348 * @param kcm the {@link KeyCharacterMap} associated with the keyboard device. 349 * @param keyEvent The key event. 350 * @param metaState The meta key modifier state. 351 * @return True if invoked the shortcut, otherwise false. 352 */ 353 @SuppressLint("MissingPermission") handleIntentShortcut(KeyCharacterMap kcm, KeyEvent keyEvent, int metaState)354 private boolean handleIntentShortcut(KeyCharacterMap kcm, KeyEvent keyEvent, int metaState) { 355 final int keyCode = keyEvent.getKeyCode(); 356 // Shortcuts are invoked through Search+key, so intercept those here 357 // Any printing key that is chorded with Search should be consumed 358 // even if no shortcut was invoked. This prevents text from being 359 // inadvertently inserted when using a keyboard that has built-in macro 360 // shortcut keys (that emit Search+x) and some of them are not registered. 361 if (mSearchKeyShortcutPending) { 362 if (kcm.isPrintingKey(keyCode)) { 363 mConsumeSearchKeyUp = true; 364 mSearchKeyShortcutPending = false; 365 } else { 366 return false; 367 } 368 } else if ((metaState & KeyEvent.META_META_MASK) != 0) { 369 // Invoke shortcuts using Meta. 370 metaState &= ~KeyEvent.META_META_MASK; 371 } else { 372 Intent intent = null; 373 // Handle application launch keys. 374 String role = sApplicationLaunchKeyRoles.get(keyCode); 375 String category = sApplicationLaunchKeyCategories.get(keyCode); 376 if (role != null) { 377 intent = getRoleLaunchIntent(role); 378 } else if (category != null) { 379 intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, category); 380 } 381 382 if (intent != null) { 383 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 384 try { 385 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 386 } catch (ActivityNotFoundException ex) { 387 Slog.w(TAG, "Dropping application launch key because " 388 + "the activity to which it is registered was not found: " 389 + "keyCode=" + KeyEvent.keyCodeToString(keyCode) + "," 390 + " category=" + category + " role=" + role); 391 } 392 logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(intent)); 393 return true; 394 } else { 395 return false; 396 } 397 } 398 399 final Intent shortcutIntent = getIntent(kcm, keyCode, metaState); 400 if (shortcutIntent != null) { 401 shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 402 try { 403 mContext.startActivityAsUser(shortcutIntent, UserHandle.CURRENT); 404 } catch (ActivityNotFoundException ex) { 405 Slog.w(TAG, "Dropping shortcut key combination because " 406 + "the activity to which it is registered was not found: " 407 + "META+ or SEARCH" + KeyEvent.keyCodeToString(keyCode)); 408 } 409 logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(shortcutIntent)); 410 return true; 411 } 412 return false; 413 } 414 logKeyboardShortcut(KeyEvent event, KeyboardLogEvent logEvent)415 private void logKeyboardShortcut(KeyEvent event, KeyboardLogEvent logEvent) { 416 mHandler.post(() -> handleKeyboardLogging(event, logEvent)); 417 } 418 handleKeyboardLogging(KeyEvent event, KeyboardLogEvent logEvent)419 private void handleKeyboardLogging(KeyEvent event, KeyboardLogEvent logEvent) { 420 final InputManager inputManager = mContext.getSystemService(InputManager.class); 421 final InputDevice inputDevice = inputManager != null 422 ? inputManager.getInputDevice(event.getDeviceId()) : null; 423 KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice, 424 logEvent, event.getMetaState(), event.getKeyCode()); 425 } 426 427 /** 428 * Handle the shortcut from {@link KeyEvent} 429 * 430 * @param event Description of the key event. 431 * @return True if invoked the shortcut, otherwise false. 432 */ interceptKey(KeyEvent event)433 boolean interceptKey(KeyEvent event) { 434 if (event.getRepeatCount() != 0) { 435 return false; 436 } 437 438 final int metaState = event.getModifiers(); 439 final int keyCode = event.getKeyCode(); 440 if (keyCode == KeyEvent.KEYCODE_SEARCH) { 441 if (event.getAction() == KeyEvent.ACTION_DOWN) { 442 mSearchKeyShortcutPending = true; 443 mConsumeSearchKeyUp = false; 444 } else { 445 mSearchKeyShortcutPending = false; 446 if (mConsumeSearchKeyUp) { 447 mConsumeSearchKeyUp = false; 448 return true; 449 } 450 } 451 return false; 452 } 453 454 if (event.getAction() != KeyEvent.ACTION_DOWN) { 455 return false; 456 } 457 458 final KeyCharacterMap kcm = event.getKeyCharacterMap(); 459 if (handleIntentShortcut(kcm, event, metaState)) { 460 return true; 461 } 462 463 if (handleShortcutService(keyCode, metaState)) { 464 return true; 465 } 466 467 return false; 468 } 469 } 470