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