ShortcutPackage.java revision be73a8068ff3babe5b0a4f12656731ba8eea6149
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 */ 16package com.android.server.pm; 17 18import android.annotation.NonNull; 19import android.annotation.Nullable; 20import android.annotation.UserIdInt; 21import android.content.ComponentName; 22import android.content.Intent; 23import android.content.pm.ShortcutInfo; 24import android.os.PersistableBundle; 25import android.text.format.Formatter; 26import android.util.ArrayMap; 27import android.util.ArraySet; 28import android.util.Slog; 29 30import com.android.internal.annotations.VisibleForTesting; 31import com.android.internal.util.XmlUtils; 32 33import org.xmlpull.v1.XmlPullParser; 34import org.xmlpull.v1.XmlPullParserException; 35import org.xmlpull.v1.XmlSerializer; 36 37import java.io.File; 38import java.io.IOException; 39import java.io.PrintWriter; 40import java.util.ArrayList; 41import java.util.Arrays; 42import java.util.List; 43import java.util.Set; 44import java.util.function.Predicate; 45 46/** 47 * Package information used by {@link ShortcutService}. 48 */ 49class ShortcutPackage extends ShortcutPackageItem { 50 private static final String TAG = ShortcutService.TAG; 51 52 static final String TAG_ROOT = "package"; 53 private static final String TAG_INTENT_EXTRAS = "intent-extras"; 54 private static final String TAG_EXTRAS = "extras"; 55 private static final String TAG_SHORTCUT = "shortcut"; 56 private static final String TAG_CATEGORIES = "categories"; 57 58 private static final String ATTR_NAME = "name"; 59 private static final String ATTR_DYNAMIC_COUNT = "dynamic-count"; 60 private static final String ATTR_CALL_COUNT = "call-count"; 61 private static final String ATTR_LAST_RESET = "last-reset"; 62 private static final String ATTR_ID = "id"; 63 private static final String ATTR_ACTIVITY = "activity"; 64 private static final String ATTR_TITLE = "title"; 65 private static final String ATTR_TEXT = "text"; 66 private static final String ATTR_INTENT = "intent"; 67 private static final String ATTR_WEIGHT = "weight"; 68 private static final String ATTR_TIMESTAMP = "timestamp"; 69 private static final String ATTR_FLAGS = "flags"; 70 private static final String ATTR_ICON_RES = "icon-res"; 71 private static final String ATTR_BITMAP_PATH = "bitmap-path"; 72 73 private static final String NAME_CATEGORIES = "categories"; 74 75 private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array"; 76 private static final String ATTR_NAME_XMLUTILS = "name"; 77 78 /** 79 * All the shortcuts from the package, keyed on IDs. 80 */ 81 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 82 83 /** 84 * # of dynamic shortcuts. 85 */ 86 private int mDynamicShortcutCount = 0; 87 88 /** 89 * # of times the package has called rate-limited APIs. 90 */ 91 private int mApiCallCount; 92 93 /** 94 * When {@link #mApiCallCount} was reset last time. 95 */ 96 private long mLastResetTime; 97 98 private ShortcutPackage(int packageUserId, String packageName, ShortcutPackageInfo spi) { 99 super(packageUserId, packageName, spi != null ? spi : ShortcutPackageInfo.newEmpty()); 100 } 101 102 public ShortcutPackage(int packageUserId, String packageName) { 103 this(packageUserId, packageName, null); 104 } 105 106 @Override 107 public int getOwnerUserId() { 108 // For packages, always owner user == package user. 109 return getPackageUserId(); 110 } 111 112 @Override 113 protected void onRestoreBlocked(ShortcutService s) { 114 // Can't restore due to version/signature mismatch. Remove all shortcuts. 115 mShortcuts.clear(); 116 } 117 118 @Override 119 protected void onRestored(ShortcutService s) { 120 // Because some launchers may not have been restored (e.g. allowBackup=false), 121 // we need to re-calculate the pinned shortcuts. 122 refreshPinnedFlags(s); 123 } 124 125 /** 126 * Note this does *not* provide a correct view to the calling launcher. 127 */ 128 @Nullable 129 public ShortcutInfo findShortcutById(String id) { 130 return mShortcuts.get(id); 131 } 132 133 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s, 134 @NonNull String id) { 135 final ShortcutInfo shortcut = mShortcuts.remove(id); 136 if (shortcut != null) { 137 s.removeIcon(getPackageUserId(), shortcut); 138 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED); 139 } 140 return shortcut; 141 } 142 143 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) { 144 deleteShortcut(s, newShortcut.getId()); 145 s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut); 146 mShortcuts.put(newShortcut.getId(), newShortcut); 147 } 148 149 /** 150 * Add a shortcut, or update one with the same ID, with taking over existing flags. 151 * 152 * It checks the max number of dynamic shortcuts. 153 */ 154 public void addDynamicShortcut(@NonNull ShortcutService s, 155 @NonNull ShortcutInfo newShortcut) { 156 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 157 158 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 159 160 final boolean wasPinned; 161 final int newDynamicCount; 162 163 if (oldShortcut == null) { 164 wasPinned = false; 165 newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut. 166 } else { 167 wasPinned = oldShortcut.isPinned(); 168 if (oldShortcut.isDynamic()) { 169 newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut. 170 } else { 171 newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut. 172 } 173 } 174 175 // Make sure there's still room. 176 s.enforceMaxDynamicShortcuts(newDynamicCount); 177 178 // Okay, make it dynamic and add. 179 if (wasPinned) { 180 newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); 181 } 182 183 addShortcut(s, newShortcut); 184 mDynamicShortcutCount = newDynamicCount; 185 } 186 187 /** 188 * Remove all shortcuts that aren't pinned nor dynamic. 189 */ 190 private void removeOrphans(@NonNull ShortcutService s) { 191 ArrayList<String> removeList = null; // Lazily initialize. 192 193 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 194 final ShortcutInfo si = mShortcuts.valueAt(i); 195 196 if (si.isPinned() || si.isDynamic()) continue; 197 198 if (removeList == null) { 199 removeList = new ArrayList<>(); 200 } 201 removeList.add(si.getId()); 202 } 203 if (removeList != null) { 204 for (int i = removeList.size() - 1; i >= 0; i--) { 205 deleteShortcut(s, removeList.get(i)); 206 } 207 } 208 } 209 210 /** 211 * Remove all dynamic shortcuts. 212 */ 213 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) { 214 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 215 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC); 216 } 217 removeOrphans(s); 218 mDynamicShortcutCount = 0; 219 } 220 221 /** 222 * Remove a dynamic shortcut by ID. 223 */ 224 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) { 225 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 226 227 if (oldShortcut == null) { 228 return; 229 } 230 if (oldShortcut.isDynamic()) { 231 mDynamicShortcutCount--; 232 } 233 if (oldShortcut.isPinned()) { 234 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 235 } else { 236 deleteShortcut(s, shortcutId); 237 } 238 } 239 240 /** 241 * Called after a launcher updates the pinned set. For each shortcut in this package, 242 * set FLAG_PINNED if any launcher has pinned it. Otherwise, clear it. 243 * 244 * <p>Then remove all shortcuts that are not dynamic and no longer pinned either. 245 */ 246 public void refreshPinnedFlags(@NonNull ShortcutService s) { 247 // First, un-pin all shortcuts 248 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 249 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 250 } 251 252 // Then, for the pinned set for each launcher, set the pin flag one by one. 253 final ArrayMap<ShortcutUser.PackageWithUser, ShortcutLauncher> launchers = 254 s.getUserShortcutsLocked(getPackageUserId()).getAllLaunchers(); 255 256 for (int l = launchers.size() - 1; l >= 0; l--) { 257 // Note even if a launcher that hasn't been installed can still pin shortcuts. 258 259 final ShortcutLauncher launcherShortcuts = launchers.valueAt(l); 260 final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds( 261 getPackageName(), getPackageUserId()); 262 263 if (pinned == null || pinned.size() == 0) { 264 continue; 265 } 266 for (int i = pinned.size() - 1; i >= 0; i--) { 267 final String id = pinned.valueAt(i); 268 final ShortcutInfo si = mShortcuts.get(id); 269 if (si == null) { 270 // This happens if a launcher pinned shortcuts from this package, then backup& 271 // restored, but this package doesn't allow backing up. 272 // In that case the launcher ends up having a dangling pinned shortcuts. 273 // That's fine, when the launcher is restored, we'll fix it. 274 continue; 275 } 276 si.addFlags(ShortcutInfo.FLAG_PINNED); 277 } 278 } 279 280 // Lastly, remove the ones that are no longer pinned nor dynamic. 281 removeOrphans(s); 282 } 283 284 /** 285 * Number of calls that the caller has made, since the last reset. 286 */ 287 public int getApiCallCount(@NonNull ShortcutService s) { 288 final long last = s.getLastResetTimeLocked(); 289 290 final long now = s.injectCurrentTimeMillis(); 291 if (ShortcutService.isClockValid(now) && mLastResetTime > now) { 292 Slog.w(TAG, "Clock rewound"); 293 // Clock rewound. 294 mLastResetTime = now; 295 mApiCallCount = 0; 296 return mApiCallCount; 297 } 298 299 // If not reset yet, then reset. 300 if (mLastResetTime < last) { 301 if (ShortcutService.DEBUG) { 302 Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting", 303 mLastResetTime, now, last)); 304 } 305 mApiCallCount = 0; 306 mLastResetTime = last; 307 } 308 return mApiCallCount; 309 } 310 311 /** 312 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 313 * and return true. Otherwise just return false. 314 */ 315 public boolean tryApiCall(@NonNull ShortcutService s) { 316 if (getApiCallCount(s) >= s.mMaxUpdatesPerInterval) { 317 return false; 318 } 319 mApiCallCount++; 320 return true; 321 } 322 323 public void resetRateLimitingForCommandLine() { 324 mApiCallCount = 0; 325 mLastResetTime = 0; 326 } 327 328 /** 329 * Find all shortcuts that match {@code query}. 330 */ 331 public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result, 332 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 333 findAll(s, result, query, cloneFlag, null, 0); 334 } 335 336 /** 337 * Find all shortcuts that match {@code query}. 338 * 339 * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned 340 * by the calling launcher will not be included in the result, and also "isPinned" will be 341 * adjusted for the caller too. 342 */ 343 public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result, 344 @Nullable Predicate<ShortcutInfo> query, int cloneFlag, 345 @Nullable String callingLauncher, int launcherUserId) { 346 if (getPackageInfo().isShadow()) { 347 // Restored and the app not installed yet, so don't return any. 348 return; 349 } 350 351 // Set of pinned shortcuts by the calling launcher. 352 final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null 353 : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) 354 .getPinnedShortcutIds(getPackageName(), getPackageUserId()); 355 356 for (int i = 0; i < mShortcuts.size(); i++) { 357 final ShortcutInfo si = mShortcuts.valueAt(i); 358 359 // If it's called by non-launcher (i.e. publisher, always include -> true. 360 // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned 361 // it. 362 final boolean isPinnedByCaller = (callingLauncher == null) 363 || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId())); 364 if (!si.isDynamic()) { 365 if (!si.isPinned()) { 366 s.wtf("Shortcut not pinned: package " + getPackageName() 367 + ", user=" + getPackageUserId() + ", id=" + si.getId()); 368 continue; 369 } 370 if (!isPinnedByCaller) { 371 continue; 372 } 373 } 374 final ShortcutInfo clone = si.clone(cloneFlag); 375 // Fix up isPinned for the caller. Note we need to do it before the "test" callback, 376 // since it may check isPinned. 377 if (!isPinnedByCaller) { 378 clone.clearFlags(ShortcutInfo.FLAG_PINNED); 379 } 380 if (query == null || query.test(clone)) { 381 result.add(clone); 382 } 383 } 384 } 385 386 public void resetThrottling() { 387 mApiCallCount = 0; 388 } 389 390 public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) { 391 pw.println(); 392 393 pw.print(prefix); 394 pw.print("Package: "); 395 pw.print(getPackageName()); 396 pw.println(); 397 398 pw.print(prefix); 399 pw.print(" "); 400 pw.print("Calls: "); 401 pw.print(getApiCallCount(s)); 402 pw.println(); 403 404 // This should be after getApiCallCount(), which may update it. 405 pw.print(prefix); 406 pw.print(" "); 407 pw.print("Last reset: ["); 408 pw.print(mLastResetTime); 409 pw.print("] "); 410 pw.print(s.formatTime(mLastResetTime)); 411 pw.println(); 412 413 getPackageInfo().dump(s, pw, prefix + " "); 414 pw.println(); 415 416 pw.println(" Shortcuts:"); 417 long totalBitmapSize = 0; 418 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 419 final int size = shortcuts.size(); 420 for (int i = 0; i < size; i++) { 421 final ShortcutInfo si = shortcuts.valueAt(i); 422 pw.print(" "); 423 pw.println(si.toInsecureString()); 424 if (si.getBitmapPath() != null) { 425 final long len = new File(si.getBitmapPath()).length(); 426 pw.print(" "); 427 pw.print("bitmap size="); 428 pw.println(len); 429 430 totalBitmapSize += len; 431 } 432 } 433 pw.print(prefix); 434 pw.print(" "); 435 pw.print("Total bitmap size: "); 436 pw.print(totalBitmapSize); 437 pw.print(" ("); 438 pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize)); 439 pw.println(")"); 440 } 441 442 @Override 443 public void saveToXml(@NonNull XmlSerializer out, boolean forBackup) 444 throws IOException, XmlPullParserException { 445 final int size = mShortcuts.size(); 446 447 if (size == 0 && mApiCallCount == 0) { 448 return; // nothing to write. 449 } 450 451 out.startTag(null, TAG_ROOT); 452 453 ShortcutService.writeAttr(out, ATTR_NAME, getPackageName()); 454 ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount); 455 ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount); 456 ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime); 457 getPackageInfo().saveToXml(out); 458 459 for (int j = 0; j < size; j++) { 460 saveShortcut(out, mShortcuts.valueAt(j), forBackup); 461 } 462 463 out.endTag(null, TAG_ROOT); 464 } 465 466 private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup) 467 throws IOException, XmlPullParserException { 468 if (forBackup) { 469 if (!si.isPinned()) { 470 return; // Backup only pinned icons. 471 } 472 } 473 out.startTag(null, TAG_SHORTCUT); 474 ShortcutService.writeAttr(out, ATTR_ID, si.getId()); 475 // writeAttr(out, "package", si.getPackageName()); // not needed 476 ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent()); 477 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 478 ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle()); 479 ShortcutService.writeAttr(out, ATTR_TEXT, si.getText()); 480 ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); 481 ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight()); 482 ShortcutService.writeAttr(out, ATTR_TIMESTAMP, 483 si.getLastChangedTimestamp()); 484 if (forBackup) { 485 // Don't write icon information. Also drop the dynamic flag. 486 ShortcutService.writeAttr(out, ATTR_FLAGS, 487 si.getFlags() & 488 ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES 489 | ShortcutInfo.FLAG_DYNAMIC)); 490 } else { 491 ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); 492 ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId()); 493 ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath()); 494 } 495 496 { 497 final Set<String> cat = si.getCategories(); 498 if (cat != null && cat.size() > 0) { 499 out.startTag(null, TAG_CATEGORIES); 500 XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]), 501 NAME_CATEGORIES, out); 502 out.endTag(null, TAG_CATEGORIES); 503 } 504 } 505 506 ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS, 507 si.getIntentPersistableExtras()); 508 ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras()); 509 510 out.endTag(null, TAG_SHORTCUT); 511 } 512 513 public static ShortcutPackage loadFromXml(ShortcutService s, XmlPullParser parser, 514 int ownerUserId, boolean fromBackup) 515 throws IOException, XmlPullParserException { 516 517 final String packageName = ShortcutService.parseStringAttribute(parser, 518 ATTR_NAME); 519 520 final ShortcutPackage ret = new ShortcutPackage(ownerUserId, packageName); 521 522 ret.mDynamicShortcutCount = 523 ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT); 524 ret.mApiCallCount = 525 ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT); 526 ret.mLastResetTime = 527 ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET); 528 529 final int outerDepth = parser.getDepth(); 530 int type; 531 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 532 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 533 if (type != XmlPullParser.START_TAG) { 534 continue; 535 } 536 final int depth = parser.getDepth(); 537 final String tag = parser.getName(); 538 if (depth == outerDepth + 1) { 539 switch (tag) { 540 case ShortcutPackageInfo.TAG_ROOT: 541 ret.getPackageInfo().loadFromXml(parser, fromBackup); 542 continue; 543 case TAG_SHORTCUT: 544 final ShortcutInfo si = parseShortcut(parser, packageName, ownerUserId); 545 546 // Don't use addShortcut(), we don't need to save the icon. 547 ret.mShortcuts.put(si.getId(), si); 548 continue; 549 } 550 } 551 ShortcutService.warnForInvalidTag(depth, tag); 552 } 553 return ret; 554 } 555 556 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName, 557 @UserIdInt int userId) throws IOException, XmlPullParserException { 558 String id; 559 ComponentName activityComponent; 560 // Icon icon; 561 String title; 562 String text; 563 Intent intent; 564 PersistableBundle intentPersistableExtras = null; 565 int weight; 566 PersistableBundle extras = null; 567 long lastChangedTimestamp; 568 int flags; 569 int iconRes; 570 String bitmapPath; 571 ArraySet<String> categories = null; 572 573 id = ShortcutService.parseStringAttribute(parser, ATTR_ID); 574 activityComponent = ShortcutService.parseComponentNameAttribute(parser, 575 ATTR_ACTIVITY); 576 title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE); 577 text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT); 578 intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT); 579 weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT); 580 lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP); 581 flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS); 582 iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES); 583 bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH); 584 585 final int outerDepth = parser.getDepth(); 586 int type; 587 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 588 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 589 if (type != XmlPullParser.START_TAG) { 590 continue; 591 } 592 final int depth = parser.getDepth(); 593 final String tag = parser.getName(); 594 if (ShortcutService.DEBUG_LOAD) { 595 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 596 depth, type, tag)); 597 } 598 switch (tag) { 599 case TAG_INTENT_EXTRAS: 600 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 601 continue; 602 case TAG_EXTRAS: 603 extras = PersistableBundle.restoreFromXml(parser); 604 continue; 605 case TAG_CATEGORIES: 606 // This just contains string-array. 607 continue; 608 case TAG_STRING_ARRAY_XMLUTILS: 609 if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser, 610 ATTR_NAME_XMLUTILS))) { 611 final String[] ar = XmlUtils.readThisStringArrayXml( 612 parser, TAG_STRING_ARRAY_XMLUTILS, null); 613 categories = new ArraySet<>(ar.length); 614 for (int i = 0; i < ar.length; i++) { 615 categories.add(ar[i]); 616 } 617 } 618 continue; 619 } 620 throw ShortcutService.throwForInvalidTag(depth, tag); 621 } 622 623 return new ShortcutInfo( 624 userId, id, packageName, activityComponent, /* icon =*/ null, title, text, 625 categories, intent, 626 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 627 iconRes, bitmapPath); 628 } 629 630 @VisibleForTesting 631 List<ShortcutInfo> getAllShortcutsForTest() { 632 return new ArrayList<>(mShortcuts.values()); 633 } 634} 635