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