ShortcutPackage.java revision df6da040e00cba255cad64d2d231aae62928607a
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.PackageInfo; 24import android.content.pm.ShortcutInfo; 25import android.content.res.Resources; 26import android.os.PersistableBundle; 27import android.text.format.Formatter; 28import android.util.ArrayMap; 29import android.util.ArraySet; 30import android.util.Log; 31import android.util.Slog; 32 33import com.android.internal.annotations.VisibleForTesting; 34import com.android.internal.util.Preconditions; 35import com.android.internal.util.XmlUtils; 36import com.android.server.pm.ShortcutService.ShortcutOperation; 37 38import org.xmlpull.v1.XmlPullParser; 39import org.xmlpull.v1.XmlPullParserException; 40import org.xmlpull.v1.XmlSerializer; 41 42import java.io.File; 43import java.io.IOException; 44import java.io.PrintWriter; 45import java.util.ArrayList; 46import java.util.Collections; 47import java.util.Comparator; 48import java.util.List; 49import java.util.Set; 50import java.util.function.Predicate; 51 52/** 53 * Package information used by {@link ShortcutService}. 54 * User information used by {@link ShortcutService}. 55 * 56 * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. 57 * 58 * TODO Max dynamic shortcuts cap should be per activity. 59 */ 60class ShortcutPackage extends ShortcutPackageItem { 61 private static final String TAG = ShortcutService.TAG; 62 private static final String TAG_VERIFY = ShortcutService.TAG + ".verify"; 63 64 static final String TAG_ROOT = "package"; 65 private static final String TAG_INTENT_EXTRAS = "intent-extras"; 66 private static final String TAG_EXTRAS = "extras"; 67 private static final String TAG_SHORTCUT = "shortcut"; 68 private static final String TAG_CATEGORIES = "categories"; 69 70 private static final String ATTR_NAME = "name"; 71 private static final String ATTR_CALL_COUNT = "call-count"; 72 private static final String ATTR_LAST_RESET = "last-reset"; 73 private static final String ATTR_ID = "id"; 74 private static final String ATTR_ACTIVITY = "activity"; 75 private static final String ATTR_TITLE = "title"; 76 private static final String ATTR_TITLE_RES_ID = "titleid"; 77 private static final String ATTR_TITLE_RES_NAME = "titlename"; 78 private static final String ATTR_TEXT = "text"; 79 private static final String ATTR_TEXT_RES_ID = "textid"; 80 private static final String ATTR_TEXT_RES_NAME = "textname"; 81 private static final String ATTR_DISABLED_MESSAGE = "dmessage"; 82 private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid"; 83 private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename"; 84 private static final String ATTR_INTENT = "intent"; 85 private static final String ATTR_RANK = "rank"; 86 private static final String ATTR_TIMESTAMP = "timestamp"; 87 private static final String ATTR_FLAGS = "flags"; 88 private static final String ATTR_ICON_RES_ID = "icon-res"; 89 private static final String ATTR_ICON_RES_NAME = "icon-resname"; 90 private static final String ATTR_BITMAP_PATH = "bitmap-path"; 91 92 private static final String NAME_CATEGORIES = "categories"; 93 94 private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array"; 95 private static final String ATTR_NAME_XMLUTILS = "name"; 96 97 /** 98 * All the shortcuts from the package, keyed on IDs. 99 */ 100 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 101 102 /** 103 * # of times the package has called rate-limited APIs. 104 */ 105 private int mApiCallCount; 106 107 /** 108 * When {@link #mApiCallCount} was reset last time. 109 */ 110 private long mLastResetTime; 111 112 private final int mPackageUid; 113 114 private long mLastKnownForegroundElapsedTime; 115 116 private ShortcutPackage(ShortcutUser shortcutUser, 117 int packageUserId, String packageName, ShortcutPackageInfo spi) { 118 super(shortcutUser, packageUserId, packageName, 119 spi != null ? spi : ShortcutPackageInfo.newEmpty()); 120 121 mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId); 122 } 123 124 public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) { 125 this(shortcutUser, packageUserId, packageName, null); 126 } 127 128 @Override 129 public int getOwnerUserId() { 130 // For packages, always owner user == package user. 131 return getPackageUserId(); 132 } 133 134 public int getPackageUid() { 135 return mPackageUid; 136 } 137 138 /** 139 * Called when a shortcut is about to be published. At this point we know the publisher 140 * package 141 * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so 142 * we do some initialization for the package. 143 */ 144 private void ensurePackageVersionInfo() { 145 // Make sure we have the version code for the app. We need the version code in 146 // handlePackageUpdated(). 147 if (getPackageInfo().getVersionCode() < 0) { 148 final ShortcutService s = mShortcutUser.mService; 149 150 final PackageInfo pi = s.getPackageInfo(getPackageName(), getOwnerUserId()); 151 if (pi != null) { 152 if (ShortcutService.DEBUG) { 153 Slog.d(TAG, String.format("Package %s version = %d", getPackageName(), 154 pi.versionCode)); 155 } 156 getPackageInfo().updateVersionInfo(pi); 157 s.scheduleSaveUser(getOwnerUserId()); 158 } 159 } 160 } 161 162 @Nullable 163 public Resources getPackageResources() { 164 return mShortcutUser.mService.injectGetResourcesForApplicationAsUser( 165 getPackageName(), getPackageUserId()); 166 } 167 168 @Override 169 protected void onRestoreBlocked() { 170 // Can't restore due to version/signature mismatch. Remove all shortcuts. 171 mShortcuts.clear(); 172 } 173 174 @Override 175 protected void onRestored() { 176 // Because some launchers may not have been restored (e.g. allowBackup=false), 177 // we need to re-calculate the pinned shortcuts. 178 refreshPinnedFlags(); 179 } 180 181 /** 182 * Note this does *not* provide a correct view to the calling launcher. 183 */ 184 @Nullable 185 public ShortcutInfo findShortcutById(String id) { 186 return mShortcuts.get(id); 187 } 188 189 private void ensureNotImmutable(@Nullable ShortcutInfo shortcut) { 190 if (shortcut != null && shortcut.isImmutable()) { 191 throw new IllegalArgumentException( 192 "Manifest shortcut ID=" + shortcut.getId() 193 + " may not be manipulated via APIs"); 194 } 195 } 196 197 private void ensureNotImmutable(@NonNull String id) { 198 ensureNotImmutable(mShortcuts.get(id)); 199 } 200 201 public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds) { 202 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 203 ensureNotImmutable(shortcutIds.get(i)); 204 } 205 } 206 207 public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts) { 208 for (int i = shortcuts.size() - 1; i >= 0; i--) { 209 ensureNotImmutable(shortcuts.get(i).getId()); 210 } 211 } 212 213 private ShortcutInfo deleteShortcutInner(@NonNull String id) { 214 final ShortcutInfo shortcut = mShortcuts.remove(id); 215 if (shortcut != null) { 216 mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut); 217 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED 218 | ShortcutInfo.FLAG_MANIFEST); 219 } 220 return shortcut; 221 } 222 223 private void addShortcutInner(@NonNull ShortcutInfo newShortcut) { 224 final ShortcutService s = mShortcutUser.mService; 225 226 deleteShortcutInner(newShortcut.getId()); 227 228 // Extract Icon and update the icon res ID and the bitmap path. 229 s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut); 230 s.fixUpShortcutResourceNamesAndValues(newShortcut); 231 mShortcuts.put(newShortcut.getId(), newShortcut); 232 } 233 234 /** 235 * Add a shortcut, or update one with the same ID, with taking over existing flags. 236 * 237 * It checks the max number of dynamic shortcuts. 238 */ 239 public void addOrUpdateDynamicShortcut(@NonNull ShortcutInfo newShortcut) { 240 241 Preconditions.checkArgument(newShortcut.isEnabled(), 242 "add/setDynamicShortcuts() cannot publish disabled shortcuts"); 243 244 ensurePackageVersionInfo(); 245 246 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 247 248 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 249 250 final boolean wasPinned; 251 252 if (oldShortcut == null) { 253 wasPinned = false; 254 } else { 255 // It's an update case. 256 // Make sure the target is updatable. (i.e. should be mutable.) 257 oldShortcut.ensureUpdatableWith(newShortcut); 258 259 wasPinned = oldShortcut.isPinned(); 260 if (!oldShortcut.isEnabled()) { 261 newShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); 262 } 263 } 264 265 // TODO Check max dynamic count. 266 // mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount); 267 268 // If it was originally pinned, the new one should be pinned too. 269 if (wasPinned) { 270 newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); 271 } 272 273 addShortcutInner(newShortcut); 274 } 275 276 /** 277 * Remove all shortcuts that aren't pinned nor dynamic. 278 */ 279 private void removeOrphans() { 280 ArrayList<String> removeList = null; // Lazily initialize. 281 282 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 283 final ShortcutInfo si = mShortcuts.valueAt(i); 284 285 if (si.isAlive()) continue; 286 287 if (removeList == null) { 288 removeList = new ArrayList<>(); 289 } 290 removeList.add(si.getId()); 291 } 292 if (removeList != null) { 293 for (int i = removeList.size() - 1; i >= 0; i--) { 294 deleteShortcutInner(removeList.get(i)); 295 } 296 } 297 } 298 299 /** 300 * Remove all dynamic shortcuts. 301 */ 302 public void deleteAllDynamicShortcuts() { 303 final long now = mShortcutUser.mService.injectCurrentTimeMillis(); 304 305 boolean changed = false; 306 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 307 final ShortcutInfo si = mShortcuts.valueAt(i); 308 if (si.isDynamic()) { 309 changed = true; 310 311 si.setTimestamp(now); 312 si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 313 si.setRank(0); // It may still be pinned, so clear the rank. 314 } 315 } 316 if (changed) { 317 removeOrphans(); 318 } 319 } 320 321 /** 322 * Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut 323 * is pinned, it'll remain as a pinned shortcut, and is still enabled. 324 */ 325 public void deleteDynamicWithId(@NonNull String shortcutId) { 326 deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false); 327 } 328 329 /** 330 * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut 331 * is pinned, it'll remain as a pinned shortcut but will be disabled. 332 */ 333 public void disableWithId(@NonNull String shortcutId, String disabledMessage, 334 int disabledMessageResId, boolean overrideImmutable) { 335 final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true, 336 overrideImmutable); 337 338 if (disabled != null) { 339 if (disabledMessage != null) { 340 disabled.setDisabledMessage(disabledMessage); 341 } else if (disabledMessageResId != 0) { 342 disabled.setDisabledMessageResId(disabledMessageResId); 343 344 mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled); 345 } 346 } 347 } 348 349 @Nullable 350 private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable, 351 boolean overrideImmutable) { 352 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 353 354 if (oldShortcut == null || !oldShortcut.isEnabled()) { 355 return null; // Doesn't exist or already disabled. 356 } 357 if (!overrideImmutable) { 358 ensureNotImmutable(oldShortcut); 359 } 360 if (oldShortcut.isPinned()) { 361 362 oldShortcut.setRank(0); 363 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); 364 if (disable) { 365 oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); 366 } 367 oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis()); 368 369 return oldShortcut; 370 } else { 371 deleteShortcutInner(shortcutId); 372 return null; 373 } 374 } 375 376 public void enableWithId(@NonNull String shortcutId) { 377 final ShortcutInfo shortcut = mShortcuts.get(shortcutId); 378 if (shortcut != null) { 379 ensureNotImmutable(shortcut); 380 shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED); 381 } 382 } 383 384 /** 385 * Called after a launcher updates the pinned set. For each shortcut in this package, 386 * set FLAG_PINNED if any launcher has pinned it. Otherwise, clear it. 387 * 388 * <p>Then remove all shortcuts that are not dynamic and no longer pinned either. 389 */ 390 public void refreshPinnedFlags() { 391 // First, un-pin all shortcuts 392 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 393 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 394 } 395 396 // Then, for the pinned set for each launcher, set the pin flag one by one. 397 mShortcutUser.mService.getUserShortcutsLocked(getPackageUserId()) 398 .forAllLaunchers(launcherShortcuts -> { 399 final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds( 400 getPackageName(), getPackageUserId()); 401 402 if (pinned == null || pinned.size() == 0) { 403 return; 404 } 405 for (int i = pinned.size() - 1; i >= 0; i--) { 406 final String id = pinned.valueAt(i); 407 final ShortcutInfo si = mShortcuts.get(id); 408 if (si == null) { 409 // This happens if a launcher pinned shortcuts from this package, then backup& 410 // restored, but this package doesn't allow backing up. 411 // In that case the launcher ends up having a dangling pinned shortcuts. 412 // That's fine, when the launcher is restored, we'll fix it. 413 continue; 414 } 415 si.addFlags(ShortcutInfo.FLAG_PINNED); 416 } 417 }); 418 419 // Lastly, remove the ones that are no longer pinned nor dynamic. 420 removeOrphans(); 421 } 422 423 /** 424 * Number of calls that the caller has made, since the last reset. 425 * 426 * <p>This takes care of the resetting the counter for foreground apps as well as after 427 * locale changes. 428 */ 429 public int getApiCallCount() { 430 mShortcutUser.resetThrottlingIfNeeded(); 431 432 final ShortcutService s = mShortcutUser.mService; 433 434 // Reset the counter if: 435 // - the package is in foreground now. 436 // - the package is *not* in foreground now, but was in foreground at some point 437 // since the previous time it had been. 438 if (s.isUidForegroundLocked(mPackageUid) 439 || mLastKnownForegroundElapsedTime 440 < s.getUidLastForegroundElapsedTimeLocked(mPackageUid)) { 441 mLastKnownForegroundElapsedTime = s.injectElapsedRealtime(); 442 resetRateLimiting(); 443 } 444 445 // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount, 446 // but we just can't return 0 at this point, because we may have to update 447 // mLastResetTime. 448 449 final long last = s.getLastResetTimeLocked(); 450 451 final long now = s.injectCurrentTimeMillis(); 452 if (ShortcutService.isClockValid(now) && mLastResetTime > now) { 453 Slog.w(TAG, "Clock rewound"); 454 // Clock rewound. 455 mLastResetTime = now; 456 mApiCallCount = 0; 457 return mApiCallCount; 458 } 459 460 // If not reset yet, then reset. 461 if (mLastResetTime < last) { 462 if (ShortcutService.DEBUG) { 463 Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting", 464 getPackageName(), mLastResetTime, now, last)); 465 } 466 mApiCallCount = 0; 467 mLastResetTime = last; 468 } 469 return mApiCallCount; 470 } 471 472 /** 473 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 474 * and return true. Otherwise just return false. 475 * 476 * <p>This takes care of the resetting the counter for foreground apps as well as after 477 * locale changes, which is done internally by {@link #getApiCallCount}. 478 */ 479 public boolean tryApiCall() { 480 final ShortcutService s = mShortcutUser.mService; 481 482 if (getApiCallCount() >= s.mMaxUpdatesPerInterval) { 483 return false; 484 } 485 mApiCallCount++; 486 s.scheduleSaveUser(getOwnerUserId()); 487 return true; 488 } 489 490 public void resetRateLimiting() { 491 if (ShortcutService.DEBUG) { 492 Slog.d(TAG, "resetRateLimiting: " + getPackageName()); 493 } 494 if (mApiCallCount > 0) { 495 mApiCallCount = 0; 496 mShortcutUser.mService.scheduleSaveUser(getOwnerUserId()); 497 } 498 } 499 500 public void resetRateLimitingForCommandLineNoSaving() { 501 mApiCallCount = 0; 502 mLastResetTime = 0; 503 } 504 505 /** 506 * Find all shortcuts that match {@code query}. 507 */ 508 public void findAll(@NonNull List<ShortcutInfo> result, 509 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 510 findAll(result, query, cloneFlag, null, 0); 511 } 512 513 /** 514 * Find all shortcuts that match {@code query}. 515 * 516 * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned 517 * by the calling launcher will not be included in the result, and also "isPinned" will be 518 * adjusted for the caller too. 519 */ 520 public void findAll(@NonNull List<ShortcutInfo> result, 521 @Nullable Predicate<ShortcutInfo> query, int cloneFlag, 522 @Nullable String callingLauncher, int launcherUserId) { 523 if (getPackageInfo().isShadow()) { 524 // Restored and the app not installed yet, so don't return any. 525 return; 526 } 527 528 final ShortcutService s = mShortcutUser.mService; 529 530 // Set of pinned shortcuts by the calling launcher. 531 final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null 532 : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) 533 .getPinnedShortcutIds(getPackageName(), getPackageUserId()); 534 535 for (int i = 0; i < mShortcuts.size(); i++) { 536 final ShortcutInfo si = mShortcuts.valueAt(i); 537 538 // Need to adjust PINNED flag depending on the caller. 539 // Basically if the caller is a launcher (callingLauncher != null) and the launcher 540 // isn't pinning it, then we need to clear PINNED for this caller. 541 final boolean isPinnedByCaller = (callingLauncher == null) 542 || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId())); 543 544 if (si.isFloating()) { 545 if (!isPinnedByCaller) { 546 continue; 547 } 548 } 549 final ShortcutInfo clone = si.clone(cloneFlag); 550 551 // Fix up isPinned for the caller. Note we need to do it before the "test" callback, 552 // since it may check isPinned. 553 if (!isPinnedByCaller) { 554 clone.clearFlags(ShortcutInfo.FLAG_PINNED); 555 } 556 if (query == null || query.test(clone)) { 557 result.add(clone); 558 } 559 } 560 } 561 562 public void resetThrottling() { 563 mApiCallCount = 0; 564 } 565 566 /** 567 * Return the filenames (excluding path names) of icon bitmap files from this package. 568 */ 569 public ArraySet<String> getUsedBitmapFiles() { 570 final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size()); 571 572 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 573 final ShortcutInfo si = mShortcuts.valueAt(i); 574 if (si.getBitmapPath() != null) { 575 usedFiles.add(getFileName(si.getBitmapPath())); 576 } 577 } 578 return usedFiles; 579 } 580 581 private static String getFileName(@NonNull String path) { 582 final int sep = path.lastIndexOf(File.separatorChar); 583 if (sep == -1) { 584 return path; 585 } else { 586 return path.substring(sep + 1); 587 } 588 } 589 590 /** 591 * Called when the package is updated or added. 592 * 593 * Add case: 594 * - Publish manifest shortcuts. 595 * 596 * Update case: 597 * - Re-publish manifest shortcuts. 598 * - If there are shortcuts with resources (icons or strings), update their timestamps. 599 * 600 * @return TRUE if any shortcuts have been changed. 601 */ 602 public boolean handlePackageAddedOrUpdated(boolean isNewApp) { 603 final PackageInfo pi = mShortcutUser.mService.getPackageInfo( 604 getPackageName(), getPackageUserId()); 605 if (pi == null) { 606 return false; // Shouldn't happen. 607 } 608 609 if (!isNewApp) { 610 // Make sure the version code or last update time has changed. 611 // Otherwise, nothing to do. 612 if (getPackageInfo().getVersionCode() >= pi.versionCode 613 && getPackageInfo().getLastUpdateTime() >= pi.lastUpdateTime) { 614 return false; 615 } 616 } 617 618 // Now prepare to publish manifest shortcuts. 619 List<ShortcutInfo> newManifestShortcutList = null; 620 try { 621 newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService, 622 getPackageName(), getPackageUserId()); 623 } catch (IOException|XmlPullParserException e) { 624 Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e); 625 } 626 final int manifestShortcutSize = newManifestShortcutList == null ? 0 627 : newManifestShortcutList.size(); 628 if (ShortcutService.DEBUG) { 629 Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)", 630 getPackageName(), manifestShortcutSize)); 631 } 632 if (isNewApp && (manifestShortcutSize == 0)) { 633 // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do. 634 635 // If it's an update, then it may already have manifest shortcuts, which need to be 636 // disabled. 637 return false; 638 } 639 if (ShortcutService.DEBUG) { 640 Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(), 641 (isNewApp ? "added" : "updated"), 642 getPackageInfo().getVersionCode(), pi.versionCode)); 643 } 644 645 getPackageInfo().updateVersionInfo(pi); 646 647 final ShortcutService s = mShortcutUser.mService; 648 649 boolean changed = false; 650 651 // For existing shortcuts, update timestamps if they have any resources. 652 if (!isNewApp) { 653 Resources publisherRes = null; 654 655 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 656 final ShortcutInfo si = mShortcuts.valueAt(i); 657 658 if (si.hasAnyResources()) { 659 if (!si.isOriginallyFromManifest()) { 660 if (publisherRes == null) { 661 publisherRes = getPackageResources(); 662 if (publisherRes == null) { 663 break; // Resources couldn't be loaded. 664 } 665 } 666 667 // If this shortcut is not from a manifest, then update all resource IDs 668 // from resource names. (We don't allow resource strings for 669 // non-manifest at the moment, but icons can still be resources.) 670 si.lookupAndFillInResourceIds(publisherRes); 671 } 672 changed = true; 673 si.setTimestamp(s.injectCurrentTimeMillis()); 674 } 675 } 676 } 677 678 // (Re-)publish manifest shortcut. 679 changed |= publishManifestShortcuts(newManifestShortcutList); 680 681 if (newManifestShortcutList != null) { 682 changed |= pushOutExcessShortcuts(); 683 } 684 685 s.verifyStates(); 686 687 if (changed) { 688 // This will send a notification to the launcher, and also save . 689 s.packageShortcutsChanged(getPackageName(), getPackageUserId()); 690 } else { 691 // Still save the version code. 692 s.scheduleSaveUser(getPackageUserId()); 693 } 694 return changed; 695 } 696 697 private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) { 698 if (ShortcutService.DEBUG) { 699 Slog.d(TAG, String.format( 700 "Package %s: publishing manifest shortcuts", getPackageName())); 701 } 702 boolean changed = false; 703 704 // Keep the previous IDs. 705 ArraySet<String> toDisableList = null; 706 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 707 final ShortcutInfo si = mShortcuts.valueAt(i); 708 709 if (si.isManifestShortcut()) { 710 if (toDisableList == null) { 711 toDisableList = new ArraySet<>(); 712 } 713 toDisableList.add(si.getId()); 714 } 715 } 716 717 // Publish new ones. 718 if (newManifestShortcutList != null) { 719 final int newListSize = newManifestShortcutList.size(); 720 721 for (int i = 0; i < newListSize; i++) { 722 changed = true; 723 724 final ShortcutInfo newShortcut = newManifestShortcutList.get(i); 725 final boolean newDisabled = !newShortcut.isEnabled(); 726 727 final String id = newShortcut.getId(); 728 final ShortcutInfo oldShortcut = mShortcuts.get(id); 729 730 boolean wasPinned = false; 731 732 if (oldShortcut != null) { 733 if (!oldShortcut.isOriginallyFromManifest()) { 734 Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId() 735 + " exists but is not from AndroidManifest.xml, not updating."); 736 continue; 737 } 738 // Take over the pinned flag. 739 if (oldShortcut.isPinned()) { 740 wasPinned = true; 741 newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); 742 } 743 } 744 if (newDisabled && !wasPinned) { 745 // If the shortcut is disabled, and it was *not* pinned, then this 746 // just doesn't have to be published. 747 // Just keep it in toDisableList, so the previous one would be removed. 748 continue; 749 } 750 751 // Note even if enabled=false, we still need to update all fields, so do it 752 // regardless. 753 addShortcutInner(newShortcut); // This will clean up the old one too. 754 755 if (!newDisabled && toDisableList != null) { 756 // Still alive, don't remove. 757 toDisableList.remove(id); 758 } 759 } 760 } 761 762 // Disable the previous manifest shortcuts that are no longer in the manifest. 763 if (toDisableList != null) { 764 if (ShortcutService.DEBUG) { 765 Slog.d(TAG, String.format( 766 "Package %s: disabling %d stale shortcuts", getPackageName(), 767 toDisableList.size())); 768 } 769 for (int i = toDisableList.size() - 1; i >= 0; i--) { 770 changed = true; 771 772 final String id = toDisableList.valueAt(i); 773 774 disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0, 775 /* overrideImmutable=*/ true); 776 } 777 removeOrphans(); 778 } 779 adjustRanks(); 780 return changed; 781 } 782 783 /** 784 * For each target activity, make sure # of dynamic + manifest shortcuts <= max. 785 * If too many, we'll remove the dynamic with the lowest ranks. 786 */ 787 private boolean pushOutExcessShortcuts() { 788 final ShortcutService service = mShortcutUser.mService; 789 final int maxShortcuts = service.getMaxActivityShortcuts(); 790 791 boolean changed = false; 792 793 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 794 sortShortcutsToActivities(); 795 for (int outer = all.size() - 1; outer >= 0; outer--) { 796 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 797 if (list.size() <= maxShortcuts) { 798 continue; 799 } 800 // Sort by isManifestShortcut() and getRank(). 801 Collections.sort(list, mShortcutTypeAndRankComparator); 802 803 // Keep [0 .. max), and remove (as dynamic) [max .. size) 804 for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) { 805 final ShortcutInfo shortcut = list.get(inner); 806 807 if (shortcut.isManifestShortcut()) { 808 // This shouldn't happen -- excess shortcuts should all be non-manifest. 809 // But just in case. 810 service.wtf("Found manifest shortcuts in excess list."); 811 continue; 812 } 813 deleteDynamicWithId(shortcut.getId()); 814 } 815 } 816 817 return changed; 818 } 819 820 /** 821 * To sort by isManifestShortcut() and getRank(). i.e. manifest shortcuts come before 822 * non-manifest shortcuts, then sort by rank. 823 * 824 * This is used to decide which dynamic shortcuts to remove when an upgraded version has more 825 * manifest shortcuts than before and as a result we need to remove some of the dynamic 826 * shortcuts. We sort manifest + dynamic shortcuts by this order, and remove the ones with 827 * the last ones. 828 * 829 * (Note the number of manifest shortcuts is always <= the max number, because if there are 830 * more, ShortcutParser would ignore the rest.) 831 */ 832 final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a, 833 ShortcutInfo b) -> { 834 if (a.isManifestShortcut() && !b.isManifestShortcut()) { 835 return -1; 836 } 837 if (!a.isManifestShortcut() && b.isManifestShortcut()) { 838 return 1; 839 } 840 return Integer.compare(a.getRank(), b.getRank()); 841 }; 842 843 /** 844 * Build a list of shortcuts for each target activity and return as a map. The result won't 845 * contain "floating" shortcuts because they don't belong on any activities. 846 */ 847 private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() { 848 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts 849 = new ArrayMap<>(); 850 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 851 final ShortcutInfo si = mShortcuts.valueAt(i); 852 if (si.isFloating()) { 853 continue; // Ignore floating shortcuts, which are not tied to any activities. 854 } 855 856 final ComponentName activity = si.getActivity(); 857 858 ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity); 859 if (list == null) { 860 list = new ArrayList<>(); 861 activitiesToShortcuts.put(activity, list); 862 } 863 list.add(si); 864 } 865 return activitiesToShortcuts; 866 } 867 868 /** Used by {@link #enforceShortcutCountsBeforeOperation} */ 869 private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts, 870 ComponentName cn, int increment) { 871 Integer oldValue = counts.get(cn); 872 if (oldValue == null) { 873 oldValue = 0; 874 } 875 876 counts.put(cn, oldValue + increment); 877 } 878 879 /** 880 * Called by 881 * {@link android.content.pm.ShortcutManager#setDynamicShortcuts}, 882 * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and 883 * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing 884 * the operation to make sure the operation wouldn't result in the target activities having 885 * more than the allowed number of dynamic/manifest shortcuts. 886 * 887 * @param newList shortcut list passed to set, add or updateShortcuts(). 888 * @param operation add, set or update. 889 * @throws IllegalArgumentException if the operation would result in going over the max 890 * shortcut count for any activity. 891 */ 892 public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList, 893 @ShortcutOperation int operation) { 894 final ShortcutService service = mShortcutUser.mService; 895 896 // Current # of dynamic / manifest shortcuts for each activity. 897 // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced 898 // anyway.) 899 final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4); 900 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 901 final ShortcutInfo shortcut = mShortcuts.valueAt(i); 902 903 if (shortcut.isManifestShortcut()) { 904 incrementCountForActivity(counts, shortcut.getActivity(), 1); 905 } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) { 906 incrementCountForActivity(counts, shortcut.getActivity(), 1); 907 } 908 } 909 910 for (int i = newList.size() - 1; i >= 0; i--) { 911 final ShortcutInfo newShortcut = newList.get(i); 912 final ComponentName newActivity = newShortcut.getActivity(); 913 if (newActivity == null) { 914 if (operation != ShortcutService.OPERATION_UPDATE) { 915 // This method may be called before validating shortcuts, so this may happen, 916 // and is a caller side error. 917 throw new NullPointerException("Activity must be provided"); 918 } 919 continue; // Activity can be null for update. 920 } 921 922 final ShortcutInfo original = mShortcuts.get(newShortcut.getId()); 923 if (original == null) { 924 if (operation == ShortcutService.OPERATION_UPDATE) { 925 continue; // When updating, ignore if there's no target. 926 } 927 // Add() or set(), and there's no existing shortcut with the same ID. We're 928 // simply publishing (as opposed to updating) this shortcut, so just +1. 929 incrementCountForActivity(counts, newActivity, 1); 930 continue; 931 } 932 if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) { 933 // Updating floating shortcuts doesn't affect the count, so ignore. 934 continue; 935 } 936 937 // If it's add() or update(), then need to decrement for the previous activity. 938 // Skip it for set() since it's already been taken care of by not counting the original 939 // dynamic shortcuts in the first loop. 940 if (operation != ShortcutService.OPERATION_SET) { 941 final ComponentName oldActivity = original.getActivity(); 942 if (!original.isFloating()) { 943 incrementCountForActivity(counts, oldActivity, -1); 944 } 945 } 946 incrementCountForActivity(counts, newActivity, 1); 947 } 948 949 // Then make sure none of the activities have more than the max number of shortcuts. 950 for (int i = counts.size() - 1; i >= 0; i--) { 951 service.enforceMaxActivityShortcuts(counts.valueAt(i)); 952 } 953 } 954 955 /** 956 * For all the text fields, refresh the string values if they're from resources. 957 */ 958 public void resolveResourceStrings() { 959 final ShortcutService s = mShortcutUser.mService; 960 boolean changed = false; 961 962 Resources publisherRes = null; 963 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 964 final ShortcutInfo si = mShortcuts.valueAt(i); 965 966 if (si.hasStringResources()) { 967 changed = true; 968 969 if (publisherRes == null) { 970 publisherRes = getPackageResources(); 971 if (publisherRes == null) { 972 break; // Resources couldn't be loaded. 973 } 974 } 975 976 si.resolveResourceStrings(publisherRes); 977 si.setTimestamp(s.injectCurrentTimeMillis()); 978 } 979 } 980 if (changed) { 981 s.scheduleSaveUser(getPackageUserId()); 982 } 983 } 984 985 /** Clears the implicit ranks for all shortcuts. */ 986 public void clearAllImplicitRanks() { 987 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 988 final ShortcutInfo si = mShortcuts.valueAt(i); 989 si.clearImplicitRankAndRankChangedFlag(); 990 } 991 } 992 993 /** 994 * Used to sort shortcuts for rank auto-adjusting. 995 */ 996 final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> { 997 // First, sort by rank. 998 int ret = Integer.compare(a.getRank(), b.getRank()); 999 if (ret != 0) { 1000 return ret; 1001 } 1002 // When ranks are tie, then prioritize the ones that have just been assigned new ranks. 1003 // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively, 1004 // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because 1005 // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set. 1006 // Similarly, updating s3's rank to 1 will insert it between s1 and s2. 1007 if (a.isRankChanged() != b.isRankChanged()) { 1008 return a.isRankChanged() ? -1 : 1; 1009 } 1010 // If they're still tie, sort by implicit rank -- i.e. preserve the order in which 1011 // they're passed to the API. 1012 ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank()); 1013 if (ret != 0) { 1014 return ret; 1015 } 1016 // If they're stil tie, just sort by their IDs. 1017 // This may happen with updateShortcuts() -- see 1018 // the testUpdateShortcuts_noManifestShortcuts() test. 1019 return a.getId().compareTo(b.getId()); 1020 }; 1021 1022 /** 1023 * Re-calculate the ranks for all shortcuts. 1024 */ 1025 public void adjustRanks() { 1026 final ShortcutService s = mShortcutUser.mService; 1027 final long now = s.injectCurrentTimeMillis(); 1028 1029 // First, clear ranks for floating shortcuts. 1030 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1031 final ShortcutInfo si = mShortcuts.valueAt(i); 1032 if (si.isFloating()) { 1033 if (si.getRank() != 0) { 1034 si.setTimestamp(now); 1035 si.setRank(0); 1036 } 1037 } 1038 } 1039 1040 // Then adjust ranks. Ranks are unique for each activity, so we first need to sort 1041 // shortcuts to each activity. 1042 // Then sort the shortcuts within each activity with mShortcutRankComparator, and 1043 // assign ranks from 0. 1044 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 1045 sortShortcutsToActivities(); 1046 for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity. 1047 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 1048 1049 // Sort by ranks and other signals. 1050 Collections.sort(list, mShortcutRankComparator); 1051 1052 int rank = 0; 1053 1054 final int size = list.size(); 1055 for (int i = 0; i < size; i++) { 1056 final ShortcutInfo si = list.get(i); 1057 if (si.isManifestShortcut()) { 1058 // Don't adjust ranks for manifest shortcuts. 1059 continue; 1060 } 1061 // At this point, it must be dynamic. 1062 if (!si.isDynamic()) { 1063 s.wtf("Non-dynamic shortcut found."); 1064 continue; 1065 } 1066 final int thisRank = rank++; 1067 if (si.getRank() != thisRank) { 1068 si.setTimestamp(now); 1069 si.setRank(thisRank); 1070 } 1071 } 1072 } 1073 } 1074 1075 public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { 1076 pw.println(); 1077 1078 pw.print(prefix); 1079 pw.print("Package: "); 1080 pw.print(getPackageName()); 1081 pw.print(" UID: "); 1082 pw.print(mPackageUid); 1083 pw.println(); 1084 1085 pw.print(prefix); 1086 pw.print(" "); 1087 pw.print("Calls: "); 1088 pw.print(getApiCallCount()); 1089 pw.println(); 1090 1091 // getApiCallCount() may have updated mLastKnownForegroundElapsedTime. 1092 pw.print(prefix); 1093 pw.print(" "); 1094 pw.print("Last known FG: "); 1095 pw.print(mLastKnownForegroundElapsedTime); 1096 pw.println(); 1097 1098 // This should be after getApiCallCount(), which may update it. 1099 pw.print(prefix); 1100 pw.print(" "); 1101 pw.print("Last reset: ["); 1102 pw.print(mLastResetTime); 1103 pw.print("] "); 1104 pw.print(ShortcutService.formatTime(mLastResetTime)); 1105 pw.println(); 1106 1107 getPackageInfo().dump(pw, prefix + " "); 1108 pw.println(); 1109 1110 pw.print(prefix); 1111 pw.println(" Shortcuts:"); 1112 long totalBitmapSize = 0; 1113 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 1114 final int size = shortcuts.size(); 1115 for (int i = 0; i < size; i++) { 1116 final ShortcutInfo si = shortcuts.valueAt(i); 1117 pw.print(prefix); 1118 pw.print(" "); 1119 pw.println(si.toInsecureString()); 1120 if (si.getBitmapPath() != null) { 1121 final long len = new File(si.getBitmapPath()).length(); 1122 pw.print(prefix); 1123 pw.print(" "); 1124 pw.print("bitmap size="); 1125 pw.println(len); 1126 1127 totalBitmapSize += len; 1128 } 1129 } 1130 pw.print(prefix); 1131 pw.print(" "); 1132 pw.print("Total bitmap size: "); 1133 pw.print(totalBitmapSize); 1134 pw.print(" ("); 1135 pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize)); 1136 pw.println(")"); 1137 } 1138 1139 @Override 1140 public void saveToXml(@NonNull XmlSerializer out, boolean forBackup) 1141 throws IOException, XmlPullParserException { 1142 final int size = mShortcuts.size(); 1143 1144 if (size == 0 && mApiCallCount == 0) { 1145 return; // nothing to write. 1146 } 1147 1148 out.startTag(null, TAG_ROOT); 1149 1150 ShortcutService.writeAttr(out, ATTR_NAME, getPackageName()); 1151 ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount); 1152 ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime); 1153 getPackageInfo().saveToXml(out); 1154 1155 for (int j = 0; j < size; j++) { 1156 saveShortcut(out, mShortcuts.valueAt(j), forBackup); 1157 } 1158 1159 out.endTag(null, TAG_ROOT); 1160 } 1161 1162 private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup) 1163 throws IOException, XmlPullParserException { 1164 if (forBackup) { 1165 if (!si.isPinned()) { 1166 return; // Backup only pinned icons. 1167 } 1168 } 1169 out.startTag(null, TAG_SHORTCUT); 1170 ShortcutService.writeAttr(out, ATTR_ID, si.getId()); 1171 // writeAttr(out, "package", si.getPackageName()); // not needed 1172 ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity()); 1173 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 1174 ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle()); 1175 ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId()); 1176 ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName()); 1177 ShortcutService.writeAttr(out, ATTR_TEXT, si.getText()); 1178 ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId()); 1179 ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName()); 1180 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage()); 1181 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID, 1182 si.getDisabledMessageResourceId()); 1183 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME, 1184 si.getDisabledMessageResName()); 1185 ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); 1186 ShortcutService.writeAttr(out, ATTR_TIMESTAMP, 1187 si.getLastChangedTimestamp()); 1188 if (forBackup) { 1189 // Don't write icon information. Also drop the dynamic flag. 1190 ShortcutService.writeAttr(out, ATTR_FLAGS, 1191 si.getFlags() & 1192 ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES 1193 | ShortcutInfo.FLAG_DYNAMIC)); 1194 } else { 1195 // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored 1196 // as dynamic. 1197 ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); 1198 1199 ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); 1200 ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId()); 1201 ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName()); 1202 ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath()); 1203 } 1204 1205 { 1206 final Set<String> cat = si.getCategories(); 1207 if (cat != null && cat.size() > 0) { 1208 out.startTag(null, TAG_CATEGORIES); 1209 XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]), 1210 NAME_CATEGORIES, out); 1211 out.endTag(null, TAG_CATEGORIES); 1212 } 1213 } 1214 1215 ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS, 1216 si.getIntentPersistableExtras()); 1217 ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras()); 1218 1219 out.endTag(null, TAG_SHORTCUT); 1220 } 1221 1222 public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser, 1223 XmlPullParser parser, boolean fromBackup) 1224 throws IOException, XmlPullParserException { 1225 1226 final String packageName = ShortcutService.parseStringAttribute(parser, 1227 ATTR_NAME); 1228 1229 final ShortcutPackage ret = new ShortcutPackage(shortcutUser, 1230 shortcutUser.getUserId(), packageName); 1231 1232 ret.mApiCallCount = 1233 ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT); 1234 ret.mLastResetTime = 1235 ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET); 1236 1237 final int outerDepth = parser.getDepth(); 1238 int type; 1239 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 1240 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 1241 if (type != XmlPullParser.START_TAG) { 1242 continue; 1243 } 1244 final int depth = parser.getDepth(); 1245 final String tag = parser.getName(); 1246 if (depth == outerDepth + 1) { 1247 switch (tag) { 1248 case ShortcutPackageInfo.TAG_ROOT: 1249 ret.getPackageInfo().loadFromXml(parser, fromBackup); 1250 continue; 1251 case TAG_SHORTCUT: 1252 final ShortcutInfo si = parseShortcut(parser, packageName, 1253 shortcutUser.getUserId()); 1254 1255 // Don't use addShortcut(), we don't need to save the icon. 1256 ret.mShortcuts.put(si.getId(), si); 1257 continue; 1258 } 1259 } 1260 ShortcutService.warnForInvalidTag(depth, tag); 1261 } 1262 return ret; 1263 } 1264 1265 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName, 1266 @UserIdInt int userId) throws IOException, XmlPullParserException { 1267 String id; 1268 ComponentName activityComponent; 1269 // Icon icon; 1270 String title; 1271 int titleResId; 1272 String titleResName; 1273 String text; 1274 int textResId; 1275 String textResName; 1276 String disabledMessage; 1277 int disabledMessageResId; 1278 String disabledMessageResName; 1279 Intent intent; 1280 PersistableBundle intentPersistableExtras = null; 1281 int rank; 1282 PersistableBundle extras = null; 1283 long lastChangedTimestamp; 1284 int flags; 1285 int iconResId; 1286 String iconResName; 1287 String bitmapPath; 1288 ArraySet<String> categories = null; 1289 1290 id = ShortcutService.parseStringAttribute(parser, ATTR_ID); 1291 activityComponent = ShortcutService.parseComponentNameAttribute(parser, 1292 ATTR_ACTIVITY); 1293 title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE); 1294 titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID); 1295 titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME); 1296 text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT); 1297 textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID); 1298 textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME); 1299 disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE); 1300 disabledMessageResId = ShortcutService.parseIntAttribute(parser, 1301 ATTR_DISABLED_MESSAGE_RES_ID); 1302 disabledMessageResName = ShortcutService.parseStringAttribute(parser, 1303 ATTR_DISABLED_MESSAGE_RES_NAME); 1304 intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT); 1305 rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK); 1306 lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP); 1307 flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS); 1308 iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID); 1309 iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME); 1310 bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH); 1311 1312 final int outerDepth = parser.getDepth(); 1313 int type; 1314 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 1315 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 1316 if (type != XmlPullParser.START_TAG) { 1317 continue; 1318 } 1319 final int depth = parser.getDepth(); 1320 final String tag = parser.getName(); 1321 if (ShortcutService.DEBUG_LOAD) { 1322 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 1323 depth, type, tag)); 1324 } 1325 switch (tag) { 1326 case TAG_INTENT_EXTRAS: 1327 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 1328 continue; 1329 case TAG_EXTRAS: 1330 extras = PersistableBundle.restoreFromXml(parser); 1331 continue; 1332 case TAG_CATEGORIES: 1333 // This just contains string-array. 1334 continue; 1335 case TAG_STRING_ARRAY_XMLUTILS: 1336 if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser, 1337 ATTR_NAME_XMLUTILS))) { 1338 final String[] ar = XmlUtils.readThisStringArrayXml( 1339 parser, TAG_STRING_ARRAY_XMLUTILS, null); 1340 categories = new ArraySet<>(ar.length); 1341 for (int i = 0; i < ar.length; i++) { 1342 categories.add(ar[i]); 1343 } 1344 } 1345 continue; 1346 } 1347 throw ShortcutService.throwForInvalidTag(depth, tag); 1348 } 1349 1350 return new ShortcutInfo( 1351 userId, id, packageName, activityComponent, /* icon =*/ null, 1352 title, titleResId, titleResName, text, textResId, textResName, 1353 disabledMessage, disabledMessageResId, disabledMessageResName, 1354 categories, intent, 1355 intentPersistableExtras, rank, extras, lastChangedTimestamp, flags, 1356 iconResId, iconResName, bitmapPath); 1357 } 1358 1359 @VisibleForTesting 1360 List<ShortcutInfo> getAllShortcutsForTest() { 1361 return new ArrayList<>(mShortcuts.values()); 1362 } 1363 1364 @Override 1365 public void verifyStates() { 1366 super.verifyStates(); 1367 1368 boolean failed = false; 1369 1370 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 1371 sortShortcutsToActivities(); 1372 1373 // Make sure each activity won't have more than max shortcuts. 1374 for (int outer = all.size() - 1; outer >= 0; outer--) { 1375 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 1376 if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) { 1377 failed = true; 1378 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer) 1379 + " has " + all.valueAt(outer).size() + " shortcuts."); 1380 } 1381 1382 // Sort by rank. 1383 Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank())); 1384 1385 // Split into two arrays for each kind. 1386 final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list); 1387 dynamicList.removeIf((si) -> !si.isDynamic()); 1388 1389 final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list); 1390 dynamicList.removeIf((si) -> !si.isManifestShortcut()); 1391 1392 verifyRanksSequential(dynamicList); 1393 verifyRanksSequential(manifestList); 1394 } 1395 1396 // Verify each shortcut's status. 1397 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1398 final ShortcutInfo si = mShortcuts.valueAt(i); 1399 if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) { 1400 failed = true; 1401 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1402 + " is not manifest, dynamic or pinned."); 1403 } 1404 if (si.isManifestShortcut() && si.isDynamic()) { 1405 failed = true; 1406 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1407 + " is both dynamic and manifest at the same time."); 1408 } 1409 if (si.getActivity() == null) { 1410 failed = true; 1411 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1412 + " has null activity."); 1413 } 1414 if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) { 1415 failed = true; 1416 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1417 + " is not floating, but is disabled."); 1418 } 1419 if (si.isFloating() && si.getRank() != 0) { 1420 failed = true; 1421 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1422 + " is floating, but has rank=" + si.getRank()); 1423 } 1424 } 1425 1426 if (failed) { 1427 throw new IllegalStateException("See logcat for errors"); 1428 } 1429 } 1430 1431 private boolean verifyRanksSequential(List<ShortcutInfo> list) { 1432 boolean failed = false; 1433 1434 for (int i = 0; i < list.size(); i++) { 1435 final ShortcutInfo si = list.get(i); 1436 if (si.getRank() != i) { 1437 failed = true; 1438 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1439 + " rank=" + si.getRank() + " but expected to be "+ i); 1440 } 1441 } 1442 return failed; 1443 } 1444} 1445