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