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 */ 16 package com.android.server.pm; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.annotation.UserIdInt; 21 import android.app.Person; 22 import android.content.ComponentName; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.LocusId; 26 import android.content.pm.PackageInfo; 27 import android.content.pm.ShortcutInfo; 28 import android.content.pm.ShortcutManager; 29 import android.content.res.Resources; 30 import android.os.PersistableBundle; 31 import android.text.format.Formatter; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import android.util.Log; 35 import android.util.Slog; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.util.ArrayUtils; 39 import com.android.internal.util.Preconditions; 40 import com.android.internal.util.XmlUtils; 41 import com.android.server.pm.ShortcutService.DumpFilter; 42 import com.android.server.pm.ShortcutService.ShortcutOperation; 43 import com.android.server.pm.ShortcutService.Stats; 44 45 import org.json.JSONException; 46 import org.json.JSONObject; 47 import org.xmlpull.v1.XmlPullParser; 48 import org.xmlpull.v1.XmlPullParserException; 49 import org.xmlpull.v1.XmlSerializer; 50 51 import java.io.File; 52 import java.io.IOException; 53 import java.io.PrintWriter; 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.Comparator; 57 import java.util.List; 58 import java.util.Set; 59 import java.util.function.Predicate; 60 61 /** 62 * Package information used by {@link ShortcutService}. 63 * User information used by {@link ShortcutService}. 64 * 65 * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. 66 */ 67 class ShortcutPackage extends ShortcutPackageItem { 68 private static final String TAG = ShortcutService.TAG; 69 private static final String TAG_VERIFY = ShortcutService.TAG + ".verify"; 70 71 static final String TAG_ROOT = "package"; 72 private static final String TAG_INTENT_EXTRAS_LEGACY = "intent-extras"; 73 private static final String TAG_INTENT = "intent"; 74 private static final String TAG_EXTRAS = "extras"; 75 private static final String TAG_SHORTCUT = "shortcut"; 76 private static final String TAG_SHARE_TARGET = "share-target"; 77 private static final String TAG_CATEGORIES = "categories"; 78 private static final String TAG_PERSON = "person"; 79 80 private static final String ATTR_NAME = "name"; 81 private static final String ATTR_CALL_COUNT = "call-count"; 82 private static final String ATTR_LAST_RESET = "last-reset"; 83 private static final String ATTR_ID = "id"; 84 private static final String ATTR_ACTIVITY = "activity"; 85 private static final String ATTR_TITLE = "title"; 86 private static final String ATTR_TITLE_RES_ID = "titleid"; 87 private static final String ATTR_TITLE_RES_NAME = "titlename"; 88 private static final String ATTR_TEXT = "text"; 89 private static final String ATTR_TEXT_RES_ID = "textid"; 90 private static final String ATTR_TEXT_RES_NAME = "textname"; 91 private static final String ATTR_DISABLED_MESSAGE = "dmessage"; 92 private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid"; 93 private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename"; 94 private static final String ATTR_DISABLED_REASON = "disabled-reason"; 95 private static final String ATTR_INTENT_LEGACY = "intent"; 96 private static final String ATTR_INTENT_NO_EXTRA = "intent-base"; 97 private static final String ATTR_RANK = "rank"; 98 private static final String ATTR_TIMESTAMP = "timestamp"; 99 private static final String ATTR_FLAGS = "flags"; 100 private static final String ATTR_ICON_RES_ID = "icon-res"; 101 private static final String ATTR_ICON_RES_NAME = "icon-resname"; 102 private static final String ATTR_BITMAP_PATH = "bitmap-path"; 103 private static final String ATTR_LOCUS_ID = "locus-id"; 104 105 private static final String ATTR_PERSON_NAME = "name"; 106 private static final String ATTR_PERSON_URI = "uri"; 107 private static final String ATTR_PERSON_KEY = "key"; 108 private static final String ATTR_PERSON_IS_BOT = "is-bot"; 109 private static final String ATTR_PERSON_IS_IMPORTANT = "is-important"; 110 111 private static final String NAME_CATEGORIES = "categories"; 112 113 private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array"; 114 private static final String ATTR_NAME_XMLUTILS = "name"; 115 116 private static final String KEY_DYNAMIC = "dynamic"; 117 private static final String KEY_MANIFEST = "manifest"; 118 private static final String KEY_PINNED = "pinned"; 119 private static final String KEY_BITMAPS = "bitmaps"; 120 private static final String KEY_BITMAP_BYTES = "bitmapBytes"; 121 122 /** 123 * All the shortcuts from the package, keyed on IDs. 124 */ 125 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 126 127 /** 128 * All the share targets from the package 129 */ 130 private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0); 131 132 /** 133 * # of times the package has called rate-limited APIs. 134 */ 135 private int mApiCallCount; 136 137 /** 138 * When {@link #mApiCallCount} was reset last time. 139 */ 140 private long mLastResetTime; 141 142 private final int mPackageUid; 143 144 private long mLastKnownForegroundElapsedTime; 145 ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi)146 private ShortcutPackage(ShortcutUser shortcutUser, 147 int packageUserId, String packageName, ShortcutPackageInfo spi) { 148 super(shortcutUser, packageUserId, packageName, 149 spi != null ? spi : ShortcutPackageInfo.newEmpty()); 150 151 mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId); 152 } 153 ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName)154 public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) { 155 this(shortcutUser, packageUserId, packageName, null); 156 } 157 158 @Override getOwnerUserId()159 public int getOwnerUserId() { 160 // For packages, always owner user == package user. 161 return getPackageUserId(); 162 } 163 getPackageUid()164 public int getPackageUid() { 165 return mPackageUid; 166 } 167 168 @Nullable getPackageResources()169 public Resources getPackageResources() { 170 return mShortcutUser.mService.injectGetResourcesForApplicationAsUser( 171 getPackageName(), getPackageUserId()); 172 } 173 getShortcutCount()174 public int getShortcutCount() { 175 return mShortcuts.size(); 176 } 177 178 @Override canRestoreAnyVersion()179 protected boolean canRestoreAnyVersion() { 180 return false; 181 } 182 183 @Override onRestored(int restoreBlockReason)184 protected void onRestored(int restoreBlockReason) { 185 // Shortcuts have been restored. 186 // - Unshadow all shortcuts. 187 // - Set disabled reason. 188 // - Disable if needed. 189 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 190 ShortcutInfo si = mShortcuts.valueAt(i); 191 si.clearFlags(ShortcutInfo.FLAG_SHADOW); 192 193 si.setDisabledReason(restoreBlockReason); 194 if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) { 195 si.addFlags(ShortcutInfo.FLAG_DISABLED); 196 } 197 } 198 // Because some launchers may not have been restored (e.g. allowBackup=false), 199 // we need to re-calculate the pinned shortcuts. 200 refreshPinnedFlags(); 201 } 202 203 /** 204 * Note this does *not* provide a correct view to the calling launcher. 205 */ 206 @Nullable findShortcutById(String id)207 public ShortcutInfo findShortcutById(String id) { 208 return mShortcuts.get(id); 209 } 210 isShortcutExistsAndInvisibleToPublisher(String id)211 public boolean isShortcutExistsAndInvisibleToPublisher(String id) { 212 ShortcutInfo si = findShortcutById(id); 213 return si != null && !si.isVisibleToPublisher(); 214 } 215 isShortcutExistsAndVisibleToPublisher(String id)216 public boolean isShortcutExistsAndVisibleToPublisher(String id) { 217 ShortcutInfo si = findShortcutById(id); 218 return si != null && si.isVisibleToPublisher(); 219 } 220 ensureNotImmutable(@ullable ShortcutInfo shortcut, boolean ignoreInvisible)221 private void ensureNotImmutable(@Nullable ShortcutInfo shortcut, boolean ignoreInvisible) { 222 if (shortcut != null && shortcut.isImmutable() 223 && (!ignoreInvisible || shortcut.isVisibleToPublisher())) { 224 throw new IllegalArgumentException( 225 "Manifest shortcut ID=" + shortcut.getId() 226 + " may not be manipulated via APIs"); 227 } 228 } 229 ensureNotImmutable(@onNull String id, boolean ignoreInvisible)230 public void ensureNotImmutable(@NonNull String id, boolean ignoreInvisible) { 231 ensureNotImmutable(mShortcuts.get(id), ignoreInvisible); 232 } 233 ensureImmutableShortcutsNotIncludedWithIds(@onNull List<String> shortcutIds, boolean ignoreInvisible)234 public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds, 235 boolean ignoreInvisible) { 236 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 237 ensureNotImmutable(shortcutIds.get(i), ignoreInvisible); 238 } 239 } 240 ensureImmutableShortcutsNotIncluded(@onNull List<ShortcutInfo> shortcuts, boolean ignoreInvisible)241 public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts, 242 boolean ignoreInvisible) { 243 for (int i = shortcuts.size() - 1; i >= 0; i--) { 244 ensureNotImmutable(shortcuts.get(i).getId(), ignoreInvisible); 245 } 246 } 247 248 /** 249 * Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible. 250 */ forceDeleteShortcutInner(@onNull String id)251 private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) { 252 final ShortcutInfo shortcut = mShortcuts.remove(id); 253 if (shortcut != null) { 254 mShortcutUser.mService.removeIconLocked(shortcut); 255 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED 256 | ShortcutInfo.FLAG_MANIFEST); 257 } 258 return shortcut; 259 } 260 261 /** 262 * Force replace a shortcut. If there's already a shortcut with the same ID, it'll be removed, 263 * even if it's invisible. 264 */ forceReplaceShortcutInner(@onNull ShortcutInfo newShortcut)265 private void forceReplaceShortcutInner(@NonNull ShortcutInfo newShortcut) { 266 final ShortcutService s = mShortcutUser.mService; 267 268 forceDeleteShortcutInner(newShortcut.getId()); 269 270 // Extract Icon and update the icon res ID and the bitmap path. 271 s.saveIconAndFixUpShortcutLocked(newShortcut); 272 s.fixUpShortcutResourceNamesAndValues(newShortcut); 273 mShortcuts.put(newShortcut.getId(), newShortcut); 274 } 275 276 /** 277 * Add a shortcut. If there's already a one with the same ID, it'll be removed, even if it's 278 * invisible. 279 * 280 * It checks the max number of dynamic shortcuts. 281 */ addOrReplaceDynamicShortcut(@onNull ShortcutInfo newShortcut)282 public void addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) { 283 284 Preconditions.checkArgument(newShortcut.isEnabled(), 285 "add/setDynamicShortcuts() cannot publish disabled shortcuts"); 286 287 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 288 289 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 290 291 final boolean wasPinned; 292 293 if (oldShortcut == null) { 294 wasPinned = false; 295 } else { 296 // It's an update case. 297 // Make sure the target is updatable. (i.e. should be mutable.) 298 oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false); 299 300 wasPinned = oldShortcut.isPinned(); 301 } 302 303 // If it was originally pinned, the new one should be pinned too. 304 if (wasPinned) { 305 newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); 306 } 307 308 forceReplaceShortcutInner(newShortcut); 309 } 310 311 /** 312 * Remove all shortcuts that aren't pinned nor dynamic. 313 */ removeOrphans()314 private void removeOrphans() { 315 ArrayList<String> removeList = null; // Lazily initialize. 316 317 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 318 final ShortcutInfo si = mShortcuts.valueAt(i); 319 320 if (si.isAlive()) continue; 321 322 if (removeList == null) { 323 removeList = new ArrayList<>(); 324 } 325 removeList.add(si.getId()); 326 } 327 if (removeList != null) { 328 for (int i = removeList.size() - 1; i >= 0; i--) { 329 forceDeleteShortcutInner(removeList.get(i)); 330 } 331 } 332 } 333 334 /** 335 * Remove all dynamic shortcuts. 336 */ deleteAllDynamicShortcuts(boolean ignoreInvisible)337 public void deleteAllDynamicShortcuts(boolean ignoreInvisible) { 338 final long now = mShortcutUser.mService.injectCurrentTimeMillis(); 339 340 boolean changed = false; 341 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 342 final ShortcutInfo si = mShortcuts.valueAt(i); 343 if (si.isDynamic() && (!ignoreInvisible || si.isVisibleToPublisher())) { 344 changed = true; 345 346 si.setTimestamp(now); 347 si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 348 si.setRank(0); // It may still be pinned, so clear the rank. 349 } 350 } 351 if (changed) { 352 removeOrphans(); 353 } 354 } 355 356 /** 357 * Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut 358 * is pinned, it'll remain as a pinned shortcut, and is still enabled. 359 * 360 * @return true if it's actually removed because it wasn't pinned, or false if it's still 361 * pinned. 362 */ deleteDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible)363 public boolean deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) { 364 final ShortcutInfo removed = deleteOrDisableWithId( 365 shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible, 366 ShortcutInfo.DISABLED_REASON_NOT_DISABLED); 367 return removed == null; 368 } 369 370 /** 371 * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut 372 * is pinned, it'll remain as a pinned shortcut, but will be disabled. 373 * 374 * @return true if it's actually removed because it wasn't pinned, or false if it's still 375 * pinned. 376 */ disableDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible, int disabledReason)377 private boolean disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible, 378 int disabledReason) { 379 final ShortcutInfo disabled = deleteOrDisableWithId( 380 shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, ignoreInvisible, 381 disabledReason); 382 return disabled == null; 383 } 384 385 /** 386 * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut 387 * is pinned, it'll remain as a pinned shortcut but will be disabled. 388 */ disableWithId(@onNull String shortcutId, String disabledMessage, int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)389 public void disableWithId(@NonNull String shortcutId, String disabledMessage, 390 int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, 391 int disabledReason) { 392 final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true, 393 overrideImmutable, ignoreInvisible, disabledReason); 394 395 if (disabled != null) { 396 if (disabledMessage != null) { 397 disabled.setDisabledMessage(disabledMessage); 398 } else if (disabledMessageResId != 0) { 399 disabled.setDisabledMessageResId(disabledMessageResId); 400 401 mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled); 402 } 403 } 404 } 405 406 @Nullable deleteOrDisableWithId(@onNull String shortcutId, boolean disable, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)407 private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable, 408 boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) { 409 Preconditions.checkState( 410 (disable == (disabledReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED)), 411 "disable and disabledReason disagree: " + disable + " vs " + disabledReason); 412 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 413 414 if (oldShortcut == null || !oldShortcut.isEnabled() 415 && (ignoreInvisible && !oldShortcut.isVisibleToPublisher())) { 416 return null; // Doesn't exist or already disabled. 417 } 418 if (!overrideImmutable) { 419 ensureNotImmutable(oldShortcut, /*ignoreInvisible=*/ true); 420 } 421 if (oldShortcut.isPinned()) { 422 423 oldShortcut.setRank(0); 424 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); 425 if (disable) { 426 oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); 427 // Do not overwrite the disabled reason if one is alreay set. 428 if (oldShortcut.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED) { 429 oldShortcut.setDisabledReason(disabledReason); 430 } 431 } 432 oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis()); 433 434 // See ShortcutRequestPinProcessor.directPinShortcut(). 435 if (mShortcutUser.mService.isDummyMainActivity(oldShortcut.getActivity())) { 436 oldShortcut.setActivity(null); 437 } 438 439 return oldShortcut; 440 } else { 441 forceDeleteShortcutInner(shortcutId); 442 return null; 443 } 444 } 445 enableWithId(@onNull String shortcutId)446 public void enableWithId(@NonNull String shortcutId) { 447 final ShortcutInfo shortcut = mShortcuts.get(shortcutId); 448 if (shortcut != null) { 449 ensureNotImmutable(shortcut, /*ignoreInvisible=*/ true); 450 shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED); 451 shortcut.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED); 452 } 453 } 454 updateInvisibleShortcutForPinRequestWith(@onNull ShortcutInfo shortcut)455 public void updateInvisibleShortcutForPinRequestWith(@NonNull ShortcutInfo shortcut) { 456 final ShortcutInfo source = mShortcuts.get(shortcut.getId()); 457 Preconditions.checkNotNull(source); 458 459 mShortcutUser.mService.validateShortcutForPinRequest(shortcut); 460 461 shortcut.addFlags(ShortcutInfo.FLAG_PINNED); 462 463 forceReplaceShortcutInner(shortcut); 464 465 adjustRanks(); 466 } 467 468 /** 469 * Called after a launcher updates the pinned set. For each shortcut in this package, 470 * set FLAG_PINNED if any launcher has pinned it. Otherwise, clear it. 471 * 472 * <p>Then remove all shortcuts that are not dynamic and no longer pinned either. 473 */ refreshPinnedFlags()474 public void refreshPinnedFlags() { 475 // First, un-pin all shortcuts 476 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 477 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 478 } 479 480 // Then, for the pinned set for each launcher, set the pin flag one by one. 481 mShortcutUser.forAllLaunchers(launcherShortcuts -> { 482 final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds( 483 getPackageName(), getPackageUserId()); 484 485 if (pinned == null || pinned.size() == 0) { 486 return; 487 } 488 for (int i = pinned.size() - 1; i >= 0; i--) { 489 final String id = pinned.valueAt(i); 490 final ShortcutInfo si = mShortcuts.get(id); 491 if (si == null) { 492 // This happens if a launcher pinned shortcuts from this package, then backup& 493 // restored, but this package doesn't allow backing up. 494 // In that case the launcher ends up having a dangling pinned shortcuts. 495 // That's fine, when the launcher is restored, we'll fix it. 496 continue; 497 } 498 si.addFlags(ShortcutInfo.FLAG_PINNED); 499 } 500 }); 501 502 // Lastly, remove the ones that are no longer pinned nor dynamic. 503 removeOrphans(); 504 } 505 506 /** 507 * Number of calls that the caller has made, since the last reset. 508 * 509 * <p>This takes care of the resetting the counter for foreground apps as well as after 510 * locale changes. 511 */ getApiCallCount(boolean unlimited)512 public int getApiCallCount(boolean unlimited) { 513 final ShortcutService s = mShortcutUser.mService; 514 515 // Reset the counter if: 516 // - the package is in foreground now. 517 // - the package is *not* in foreground now, but was in foreground at some point 518 // since the previous time it had been. 519 if (s.isUidForegroundLocked(mPackageUid) 520 || (mLastKnownForegroundElapsedTime 521 < s.getUidLastForegroundElapsedTimeLocked(mPackageUid)) 522 || unlimited) { 523 mLastKnownForegroundElapsedTime = s.injectElapsedRealtime(); 524 resetRateLimiting(); 525 } 526 527 // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount, 528 // but we just can't return 0 at this point, because we may have to update 529 // mLastResetTime. 530 531 final long last = s.getLastResetTimeLocked(); 532 533 final long now = s.injectCurrentTimeMillis(); 534 if (ShortcutService.isClockValid(now) && mLastResetTime > now) { 535 Slog.w(TAG, "Clock rewound"); 536 // Clock rewound. 537 mLastResetTime = now; 538 mApiCallCount = 0; 539 return mApiCallCount; 540 } 541 542 // If not reset yet, then reset. 543 if (mLastResetTime < last) { 544 if (ShortcutService.DEBUG) { 545 Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting", 546 getPackageName(), mLastResetTime, now, last)); 547 } 548 mApiCallCount = 0; 549 mLastResetTime = last; 550 } 551 return mApiCallCount; 552 } 553 554 /** 555 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 556 * and return true. Otherwise just return false. 557 * 558 * <p>This takes care of the resetting the counter for foreground apps as well as after 559 * locale changes, which is done internally by {@link #getApiCallCount}. 560 */ tryApiCall(boolean unlimited)561 public boolean tryApiCall(boolean unlimited) { 562 final ShortcutService s = mShortcutUser.mService; 563 564 if (getApiCallCount(unlimited) >= s.mMaxUpdatesPerInterval) { 565 return false; 566 } 567 mApiCallCount++; 568 s.scheduleSaveUser(getOwnerUserId()); 569 return true; 570 } 571 resetRateLimiting()572 public void resetRateLimiting() { 573 if (ShortcutService.DEBUG) { 574 Slog.d(TAG, "resetRateLimiting: " + getPackageName()); 575 } 576 if (mApiCallCount > 0) { 577 mApiCallCount = 0; 578 mShortcutUser.mService.scheduleSaveUser(getOwnerUserId()); 579 } 580 } 581 resetRateLimitingForCommandLineNoSaving()582 public void resetRateLimitingForCommandLineNoSaving() { 583 mApiCallCount = 0; 584 mLastResetTime = 0; 585 } 586 587 /** 588 * Find all shortcuts that match {@code query}. 589 */ findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> query, int cloneFlag)590 public void findAll(@NonNull List<ShortcutInfo> result, 591 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 592 findAll(result, query, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false); 593 } 594 595 /** 596 * Find all shortcuts that match {@code query}. 597 * 598 * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned 599 * by the calling launcher will not be included in the result, and also "isPinned" will be 600 * adjusted for the caller too. 601 */ findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> query, int cloneFlag, @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher)602 public void findAll(@NonNull List<ShortcutInfo> result, 603 @Nullable Predicate<ShortcutInfo> query, int cloneFlag, 604 @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) { 605 if (getPackageInfo().isShadow()) { 606 // Restored and the app not installed yet, so don't return any. 607 return; 608 } 609 610 final ShortcutService s = mShortcutUser.mService; 611 612 // Set of pinned shortcuts by the calling launcher. 613 final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null 614 : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) 615 .getPinnedShortcutIds(getPackageName(), getPackageUserId()); 616 617 for (int i = 0; i < mShortcuts.size(); i++) { 618 final ShortcutInfo si = mShortcuts.valueAt(i); 619 620 // Need to adjust PINNED flag depending on the caller. 621 // Basically if the caller is a launcher (callingLauncher != null) and the launcher 622 // isn't pinning it, then we need to clear PINNED for this caller. 623 final boolean isPinnedByCaller = (callingLauncher == null) 624 || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId())); 625 626 if (!getPinnedByAnyLauncher) { 627 if (si.isFloating()) { 628 if (!isPinnedByCaller) { 629 continue; 630 } 631 } 632 } 633 final ShortcutInfo clone = si.clone(cloneFlag); 634 635 // Fix up isPinned for the caller. Note we need to do it before the "test" callback, 636 // since it may check isPinned. 637 // However, if getPinnedByAnyLauncher is set, we do it after the test. 638 if (!getPinnedByAnyLauncher) { 639 if (!isPinnedByCaller) { 640 clone.clearFlags(ShortcutInfo.FLAG_PINNED); 641 } 642 } 643 if (query == null || query.test(clone)) { 644 if (!isPinnedByCaller) { 645 clone.clearFlags(ShortcutInfo.FLAG_PINNED); 646 } 647 result.add(clone); 648 } 649 } 650 } 651 resetThrottling()652 public void resetThrottling() { 653 mApiCallCount = 0; 654 } 655 656 /** 657 * Returns a list of ShortcutInfos that match the given intent filter and the category of 658 * available ShareTarget definitions in this package. 659 */ getMatchingShareTargets( @onNull IntentFilter filter)660 public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets( 661 @NonNull IntentFilter filter) { 662 final List<ShareTargetInfo> matchedTargets = new ArrayList<>(); 663 for (int i = 0; i < mShareTargets.size(); i++) { 664 final ShareTargetInfo target = mShareTargets.get(i); 665 for (ShareTargetInfo.TargetData data : target.mTargetData) { 666 if (filter.hasDataType(data.mMimeType)) { 667 // Matched at least with one data type 668 matchedTargets.add(target); 669 break; 670 } 671 } 672 } 673 674 if (matchedTargets.isEmpty()) { 675 return new ArrayList<>(); 676 } 677 678 // Get the list of all dynamic shortcuts in this package. 679 final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 680 findAll(shortcuts, ShortcutInfo::isDynamicVisible, 681 ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION); 682 683 final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>(); 684 for (int i = 0; i < shortcuts.size(); i++) { 685 final Set<String> categories = shortcuts.get(i).getCategories(); 686 if (categories == null || categories.isEmpty()) { 687 continue; 688 } 689 for (int j = 0; j < matchedTargets.size(); j++) { 690 // Shortcut must have all of share target categories 691 boolean hasAllCategories = true; 692 final ShareTargetInfo target = matchedTargets.get(j); 693 for (int q = 0; q < target.mCategories.length; q++) { 694 if (!categories.contains(target.mCategories[q])) { 695 hasAllCategories = false; 696 break; 697 } 698 } 699 if (hasAllCategories) { 700 result.add(new ShortcutManager.ShareShortcutInfo(shortcuts.get(i), 701 new ComponentName(getPackageName(), target.mTargetClass))); 702 break; 703 } 704 } 705 } 706 return result; 707 } 708 hasShareTargets()709 public boolean hasShareTargets() { 710 return !mShareTargets.isEmpty(); 711 } 712 713 /** 714 * Returns the number of shortcuts that can be used as a share target in the ShareSheet. Such 715 * shortcuts must have a matching category with at least one of the defined ShareTargets from 716 * the app's Xml resource. 717 */ getSharingShortcutCount()718 int getSharingShortcutCount() { 719 if (mShortcuts.isEmpty() || mShareTargets.isEmpty()) { 720 return 0; 721 } 722 723 // Get the list of all dynamic shortcuts in this package 724 final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); 725 findAll(shortcuts, ShortcutInfo::isDynamicVisible, ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 726 727 int sharingShortcutCount = 0; 728 for (int i = 0; i < shortcuts.size(); i++) { 729 final Set<String> categories = shortcuts.get(i).getCategories(); 730 if (categories == null || categories.isEmpty()) { 731 continue; 732 } 733 for (int j = 0; j < mShareTargets.size(); j++) { 734 // A SharingShortcut must have all of share target categories 735 boolean hasAllCategories = true; 736 final ShareTargetInfo target = mShareTargets.get(j); 737 for (int q = 0; q < target.mCategories.length; q++) { 738 if (!categories.contains(target.mCategories[q])) { 739 hasAllCategories = false; 740 break; 741 } 742 } 743 if (hasAllCategories) { 744 sharingShortcutCount++; 745 break; 746 } 747 } 748 } 749 return sharingShortcutCount; 750 } 751 752 /** 753 * Return the filenames (excluding path names) of icon bitmap files from this package. 754 */ getUsedBitmapFiles()755 public ArraySet<String> getUsedBitmapFiles() { 756 final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size()); 757 758 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 759 final ShortcutInfo si = mShortcuts.valueAt(i); 760 if (si.getBitmapPath() != null) { 761 usedFiles.add(getFileName(si.getBitmapPath())); 762 } 763 } 764 return usedFiles; 765 } 766 getFileName(@onNull String path)767 private static String getFileName(@NonNull String path) { 768 final int sep = path.lastIndexOf(File.separatorChar); 769 if (sep == -1) { 770 return path; 771 } else { 772 return path.substring(sep + 1); 773 } 774 } 775 776 /** 777 * @return false if any of the target activities are no longer enabled. 778 */ areAllActivitiesStillEnabled()779 private boolean areAllActivitiesStillEnabled() { 780 if (mShortcuts.size() == 0) { 781 return true; 782 } 783 final ShortcutService s = mShortcutUser.mService; 784 785 // Normally the number of target activities is 1 or so, so no need to use a complex 786 // structure like a set. 787 final ArrayList<ComponentName> checked = new ArrayList<>(4); 788 789 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 790 final ShortcutInfo si = mShortcuts.valueAt(i); 791 final ComponentName activity = si.getActivity(); 792 793 if (checked.contains(activity)) { 794 continue; // Already checked. 795 } 796 checked.add(activity); 797 798 if ((activity != null) 799 && !s.injectIsActivityEnabledAndExported(activity, getOwnerUserId())) { 800 return false; 801 } 802 } 803 return true; 804 } 805 806 /** 807 * Called when the package may be added or updated, or its activities may be disabled, and 808 * if so, rescan the package and do the necessary stuff. 809 * 810 * Add case: 811 * - Publish manifest shortcuts. 812 * 813 * Update case: 814 * - Re-publish manifest shortcuts. 815 * - If there are shortcuts with resources (icons or strings), update their timestamps. 816 * - Disable shortcuts whose target activities are disabled. 817 * 818 * @return TRUE if any shortcuts have been changed. 819 */ rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan)820 public boolean rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan) { 821 final ShortcutService s = mShortcutUser.mService; 822 final long start = s.getStatStartTime(); 823 824 final PackageInfo pi; 825 try { 826 pi = mShortcutUser.mService.getPackageInfo( 827 getPackageName(), getPackageUserId()); 828 if (pi == null) { 829 return false; // Shouldn't happen. 830 } 831 832 if (!isNewApp && !forceRescan) { 833 // Return if the package hasn't changed, ie: 834 // - version code hasn't change 835 // - lastUpdateTime hasn't change 836 // - all target activities are still enabled. 837 838 // Note, system apps timestamps do *not* change after OTAs. (But they do 839 // after an adb sync or a local flash.) 840 // This means if a system app's version code doesn't change on an OTA, 841 // we don't notice it's updated. But that's fine since their version code *should* 842 // really change on OTAs. 843 if ((getPackageInfo().getVersionCode() == pi.getLongVersionCode()) 844 && (getPackageInfo().getLastUpdateTime() == pi.lastUpdateTime) 845 && areAllActivitiesStillEnabled()) { 846 return false; 847 } 848 } 849 } finally { 850 s.logDurationStat(Stats.PACKAGE_UPDATE_CHECK, start); 851 } 852 853 // Now prepare to publish manifest shortcuts. 854 List<ShortcutInfo> newManifestShortcutList = null; 855 try { 856 newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService, 857 getPackageName(), getPackageUserId(), mShareTargets); 858 } catch (IOException|XmlPullParserException e) { 859 Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e); 860 } 861 final int manifestShortcutSize = newManifestShortcutList == null ? 0 862 : newManifestShortcutList.size(); 863 if (ShortcutService.DEBUG) { 864 Slog.d(TAG, 865 String.format("Package %s has %d manifest shortcut(s), and %d share target(s)", 866 getPackageName(), manifestShortcutSize, mShareTargets.size())); 867 } 868 if (isNewApp && (manifestShortcutSize == 0)) { 869 // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do. 870 871 // If it's an update, then it may already have manifest shortcuts, which need to be 872 // disabled. 873 return false; 874 } 875 if (ShortcutService.DEBUG) { 876 Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(), 877 (isNewApp ? "added" : "updated"), 878 getPackageInfo().getVersionCode(), pi.getLongVersionCode())); 879 } 880 881 getPackageInfo().updateFromPackageInfo(pi); 882 final long newVersionCode = getPackageInfo().getVersionCode(); 883 884 // See if there are any shortcuts that were prevented restoring because the app was of a 885 // lower version, and re-enable them. 886 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 887 final ShortcutInfo si = mShortcuts.valueAt(i); 888 if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { 889 continue; 890 } 891 if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) { 892 if (ShortcutService.DEBUG) { 893 Slog.d(TAG, String.format("Shortcut %s require version %s, still not restored.", 894 si.getId(), getPackageInfo().getBackupSourceVersionCode())); 895 } 896 continue; 897 } 898 Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId())); 899 si.clearFlags(ShortcutInfo.FLAG_DISABLED); 900 si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED); 901 } 902 903 // For existing shortcuts, update timestamps if they have any resources. 904 // Also check if shortcuts' activities are still main activities. Otherwise, disable them. 905 if (!isNewApp) { 906 Resources publisherRes = null; 907 908 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 909 final ShortcutInfo si = mShortcuts.valueAt(i); 910 911 // Disable dynamic shortcuts whose target activity is gone. 912 if (si.isDynamic()) { 913 if (si.getActivity() == null) { 914 // Note if it's dynamic, it must have a target activity, but b/36228253. 915 s.wtf("null activity detected."); 916 // TODO Maybe remove it? 917 } else if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) { 918 Slog.w(TAG, String.format( 919 "%s is no longer main activity. Disabling shorcut %s.", 920 getPackageName(), si.getId())); 921 if (disableDynamicWithId(si.getId(), /*ignoreInvisible*/ false, 922 ShortcutInfo.DISABLED_REASON_APP_CHANGED)) { 923 continue; // Actually removed. 924 } 925 // Still pinned, so fall-through and possibly update the resources. 926 } 927 } 928 929 if (si.hasAnyResources()) { 930 if (!si.isOriginallyFromManifest()) { 931 if (publisherRes == null) { 932 publisherRes = getPackageResources(); 933 if (publisherRes == null) { 934 break; // Resources couldn't be loaded. 935 } 936 } 937 938 // If this shortcut is not from a manifest, then update all resource IDs 939 // from resource names. (We don't allow resource strings for 940 // non-manifest at the moment, but icons can still be resources.) 941 si.lookupAndFillInResourceIds(publisherRes); 942 } 943 si.setTimestamp(s.injectCurrentTimeMillis()); 944 } 945 } 946 } 947 948 // (Re-)publish manifest shortcut. 949 publishManifestShortcuts(newManifestShortcutList); 950 951 if (newManifestShortcutList != null) { 952 pushOutExcessShortcuts(); 953 } 954 955 s.verifyStates(); 956 957 // This will send a notification to the launcher, and also save . 958 s.packageShortcutsChanged(getPackageName(), getPackageUserId()); 959 return true; // true means changed. 960 } 961 publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList)962 private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) { 963 if (ShortcutService.DEBUG) { 964 Slog.d(TAG, String.format( 965 "Package %s: publishing manifest shortcuts", getPackageName())); 966 } 967 boolean changed = false; 968 969 // Keep the previous IDs. 970 ArraySet<String> toDisableList = null; 971 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 972 final ShortcutInfo si = mShortcuts.valueAt(i); 973 974 if (si.isManifestShortcut()) { 975 if (toDisableList == null) { 976 toDisableList = new ArraySet<>(); 977 } 978 toDisableList.add(si.getId()); 979 } 980 } 981 982 // Publish new ones. 983 if (newManifestShortcutList != null) { 984 final int newListSize = newManifestShortcutList.size(); 985 986 for (int i = 0; i < newListSize; i++) { 987 changed = true; 988 989 final ShortcutInfo newShortcut = newManifestShortcutList.get(i); 990 final boolean newDisabled = !newShortcut.isEnabled(); 991 992 final String id = newShortcut.getId(); 993 final ShortcutInfo oldShortcut = mShortcuts.get(id); 994 995 boolean wasPinned = false; 996 997 if (oldShortcut != null) { 998 if (!oldShortcut.isOriginallyFromManifest()) { 999 Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId() 1000 + " exists but is not from AndroidManifest.xml, not updating."); 1001 continue; 1002 } 1003 // Take over the pinned flag. 1004 if (oldShortcut.isPinned()) { 1005 wasPinned = true; 1006 newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); 1007 } 1008 } 1009 if (newDisabled && !wasPinned) { 1010 // If the shortcut is disabled, and it was *not* pinned, then this 1011 // just doesn't have to be published. 1012 // Just keep it in toDisableList, so the previous one would be removed. 1013 continue; 1014 } 1015 1016 // Note even if enabled=false, we still need to update all fields, so do it 1017 // regardless. 1018 forceReplaceShortcutInner(newShortcut); // This will clean up the old one too. 1019 1020 if (!newDisabled && toDisableList != null) { 1021 // Still alive, don't remove. 1022 toDisableList.remove(id); 1023 } 1024 } 1025 } 1026 1027 // Disable the previous manifest shortcuts that are no longer in the manifest. 1028 if (toDisableList != null) { 1029 if (ShortcutService.DEBUG) { 1030 Slog.d(TAG, String.format( 1031 "Package %s: disabling %d stale shortcuts", getPackageName(), 1032 toDisableList.size())); 1033 } 1034 for (int i = toDisableList.size() - 1; i >= 0; i--) { 1035 changed = true; 1036 1037 final String id = toDisableList.valueAt(i); 1038 1039 disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0, 1040 /* overrideImmutable=*/ true, /*ignoreInvisible=*/ false, 1041 ShortcutInfo.DISABLED_REASON_APP_CHANGED); 1042 } 1043 removeOrphans(); 1044 } 1045 adjustRanks(); 1046 return changed; 1047 } 1048 1049 /** 1050 * For each target activity, make sure # of dynamic + manifest shortcuts <= max. 1051 * If too many, we'll remove the dynamic with the lowest ranks. 1052 */ pushOutExcessShortcuts()1053 private boolean pushOutExcessShortcuts() { 1054 final ShortcutService service = mShortcutUser.mService; 1055 final int maxShortcuts = service.getMaxActivityShortcuts(); 1056 1057 boolean changed = false; 1058 1059 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 1060 sortShortcutsToActivities(); 1061 for (int outer = all.size() - 1; outer >= 0; outer--) { 1062 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 1063 if (list.size() <= maxShortcuts) { 1064 continue; 1065 } 1066 // Sort by isManifestShortcut() and getRank(). 1067 Collections.sort(list, mShortcutTypeAndRankComparator); 1068 1069 // Keep [0 .. max), and remove (as dynamic) [max .. size) 1070 for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) { 1071 final ShortcutInfo shortcut = list.get(inner); 1072 1073 if (shortcut.isManifestShortcut()) { 1074 // This shouldn't happen -- excess shortcuts should all be non-manifest. 1075 // But just in case. 1076 service.wtf("Found manifest shortcuts in excess list."); 1077 continue; 1078 } 1079 deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true); 1080 } 1081 } 1082 1083 return changed; 1084 } 1085 1086 /** 1087 * To sort by isManifestShortcut() and getRank(). i.e. manifest shortcuts come before 1088 * non-manifest shortcuts, then sort by rank. 1089 * 1090 * This is used to decide which dynamic shortcuts to remove when an upgraded version has more 1091 * manifest shortcuts than before and as a result we need to remove some of the dynamic 1092 * shortcuts. We sort manifest + dynamic shortcuts by this order, and remove the ones with 1093 * the last ones. 1094 * 1095 * (Note the number of manifest shortcuts is always <= the max number, because if there are 1096 * more, ShortcutParser would ignore the rest.) 1097 */ 1098 final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a, 1099 ShortcutInfo b) -> { 1100 if (a.isManifestShortcut() && !b.isManifestShortcut()) { 1101 return -1; 1102 } 1103 if (!a.isManifestShortcut() && b.isManifestShortcut()) { 1104 return 1; 1105 } 1106 return Integer.compare(a.getRank(), b.getRank()); 1107 }; 1108 1109 /** 1110 * Build a list of shortcuts for each target activity and return as a map. The result won't 1111 * contain "floating" shortcuts because they don't belong on any activities. 1112 */ sortShortcutsToActivities()1113 private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() { 1114 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts 1115 = new ArrayMap<>(); 1116 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1117 final ShortcutInfo si = mShortcuts.valueAt(i); 1118 if (si.isFloating()) { 1119 continue; // Ignore floating shortcuts, which are not tied to any activities. 1120 } 1121 1122 final ComponentName activity = si.getActivity(); 1123 if (activity == null) { 1124 mShortcutUser.mService.wtf("null activity detected."); 1125 continue; 1126 } 1127 1128 ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity); 1129 if (list == null) { 1130 list = new ArrayList<>(); 1131 activitiesToShortcuts.put(activity, list); 1132 } 1133 list.add(si); 1134 } 1135 return activitiesToShortcuts; 1136 } 1137 1138 /** Used by {@link #enforceShortcutCountsBeforeOperation} */ incrementCountForActivity(ArrayMap<ComponentName, Integer> counts, ComponentName cn, int increment)1139 private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts, 1140 ComponentName cn, int increment) { 1141 Integer oldValue = counts.get(cn); 1142 if (oldValue == null) { 1143 oldValue = 0; 1144 } 1145 1146 counts.put(cn, oldValue + increment); 1147 } 1148 1149 /** 1150 * Called by 1151 * {@link android.content.pm.ShortcutManager#setDynamicShortcuts}, 1152 * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and 1153 * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing 1154 * the operation to make sure the operation wouldn't result in the target activities having 1155 * more than the allowed number of dynamic/manifest shortcuts. 1156 * 1157 * @param newList shortcut list passed to set, add or updateShortcuts(). 1158 * @param operation add, set or update. 1159 * @throws IllegalArgumentException if the operation would result in going over the max 1160 * shortcut count for any activity. 1161 */ enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList, @ShortcutOperation int operation)1162 public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList, 1163 @ShortcutOperation int operation) { 1164 final ShortcutService service = mShortcutUser.mService; 1165 1166 // Current # of dynamic / manifest shortcuts for each activity. 1167 // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced 1168 // anyway.) 1169 final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4); 1170 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1171 final ShortcutInfo shortcut = mShortcuts.valueAt(i); 1172 1173 if (shortcut.isManifestShortcut()) { 1174 incrementCountForActivity(counts, shortcut.getActivity(), 1); 1175 } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) { 1176 incrementCountForActivity(counts, shortcut.getActivity(), 1); 1177 } 1178 } 1179 1180 for (int i = newList.size() - 1; i >= 0; i--) { 1181 final ShortcutInfo newShortcut = newList.get(i); 1182 final ComponentName newActivity = newShortcut.getActivity(); 1183 if (newActivity == null) { 1184 if (operation != ShortcutService.OPERATION_UPDATE) { 1185 service.wtf("Activity must not be null at this point"); 1186 continue; // Just ignore this invalid case. 1187 } 1188 continue; // Activity can be null for update. 1189 } 1190 1191 final ShortcutInfo original = mShortcuts.get(newShortcut.getId()); 1192 if (original == null) { 1193 if (operation == ShortcutService.OPERATION_UPDATE) { 1194 continue; // When updating, ignore if there's no target. 1195 } 1196 // Add() or set(), and there's no existing shortcut with the same ID. We're 1197 // simply publishing (as opposed to updating) this shortcut, so just +1. 1198 incrementCountForActivity(counts, newActivity, 1); 1199 continue; 1200 } 1201 if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) { 1202 // Updating floating shortcuts doesn't affect the count, so ignore. 1203 continue; 1204 } 1205 1206 // If it's add() or update(), then need to decrement for the previous activity. 1207 // Skip it for set() since it's already been taken care of by not counting the original 1208 // dynamic shortcuts in the first loop. 1209 if (operation != ShortcutService.OPERATION_SET) { 1210 final ComponentName oldActivity = original.getActivity(); 1211 if (!original.isFloating()) { 1212 incrementCountForActivity(counts, oldActivity, -1); 1213 } 1214 } 1215 incrementCountForActivity(counts, newActivity, 1); 1216 } 1217 1218 // Then make sure none of the activities have more than the max number of shortcuts. 1219 for (int i = counts.size() - 1; i >= 0; i--) { 1220 service.enforceMaxActivityShortcuts(counts.valueAt(i)); 1221 } 1222 } 1223 1224 /** 1225 * For all the text fields, refresh the string values if they're from resources. 1226 */ resolveResourceStrings()1227 public void resolveResourceStrings() { 1228 final ShortcutService s = mShortcutUser.mService; 1229 boolean changed = false; 1230 1231 Resources publisherRes = null; 1232 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1233 final ShortcutInfo si = mShortcuts.valueAt(i); 1234 1235 if (si.hasStringResources()) { 1236 changed = true; 1237 1238 if (publisherRes == null) { 1239 publisherRes = getPackageResources(); 1240 if (publisherRes == null) { 1241 break; // Resources couldn't be loaded. 1242 } 1243 } 1244 1245 si.resolveResourceStrings(publisherRes); 1246 si.setTimestamp(s.injectCurrentTimeMillis()); 1247 } 1248 } 1249 if (changed) { 1250 s.packageShortcutsChanged(getPackageName(), getPackageUserId()); 1251 } 1252 } 1253 1254 /** Clears the implicit ranks for all shortcuts. */ clearAllImplicitRanks()1255 public void clearAllImplicitRanks() { 1256 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1257 final ShortcutInfo si = mShortcuts.valueAt(i); 1258 si.clearImplicitRankAndRankChangedFlag(); 1259 } 1260 } 1261 1262 /** 1263 * Used to sort shortcuts for rank auto-adjusting. 1264 */ 1265 final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> { 1266 // First, sort by rank. 1267 int ret = Integer.compare(a.getRank(), b.getRank()); 1268 if (ret != 0) { 1269 return ret; 1270 } 1271 // When ranks are tie, then prioritize the ones that have just been assigned new ranks. 1272 // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively, 1273 // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because 1274 // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set. 1275 // Similarly, updating s3's rank to 1 will insert it between s1 and s2. 1276 if (a.isRankChanged() != b.isRankChanged()) { 1277 return a.isRankChanged() ? -1 : 1; 1278 } 1279 // If they're still tie, sort by implicit rank -- i.e. preserve the order in which 1280 // they're passed to the API. 1281 ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank()); 1282 if (ret != 0) { 1283 return ret; 1284 } 1285 // If they're still tie, just sort by their IDs. 1286 // This may happen with updateShortcuts() -- see 1287 // the testUpdateShortcuts_noManifestShortcuts() test. 1288 return a.getId().compareTo(b.getId()); 1289 }; 1290 1291 /** 1292 * Re-calculate the ranks for all shortcuts. 1293 */ adjustRanks()1294 public void adjustRanks() { 1295 final ShortcutService s = mShortcutUser.mService; 1296 final long now = s.injectCurrentTimeMillis(); 1297 1298 // First, clear ranks for floating shortcuts. 1299 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1300 final ShortcutInfo si = mShortcuts.valueAt(i); 1301 if (si.isFloating()) { 1302 if (si.getRank() != 0) { 1303 si.setTimestamp(now); 1304 si.setRank(0); 1305 } 1306 } 1307 } 1308 1309 // Then adjust ranks. Ranks are unique for each activity, so we first need to sort 1310 // shortcuts to each activity. 1311 // Then sort the shortcuts within each activity with mShortcutRankComparator, and 1312 // assign ranks from 0. 1313 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 1314 sortShortcutsToActivities(); 1315 for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity. 1316 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 1317 1318 // Sort by ranks and other signals. 1319 Collections.sort(list, mShortcutRankComparator); 1320 1321 int rank = 0; 1322 1323 final int size = list.size(); 1324 for (int i = 0; i < size; i++) { 1325 final ShortcutInfo si = list.get(i); 1326 if (si.isManifestShortcut()) { 1327 // Don't adjust ranks for manifest shortcuts. 1328 continue; 1329 } 1330 // At this point, it must be dynamic. 1331 if (!si.isDynamic()) { 1332 s.wtf("Non-dynamic shortcut found."); 1333 continue; 1334 } 1335 final int thisRank = rank++; 1336 if (si.getRank() != thisRank) { 1337 si.setTimestamp(now); 1338 si.setRank(thisRank); 1339 } 1340 } 1341 } 1342 } 1343 1344 /** @return true if there's any shortcuts that are not manifest shortcuts. */ hasNonManifestShortcuts()1345 public boolean hasNonManifestShortcuts() { 1346 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1347 final ShortcutInfo si = mShortcuts.valueAt(i); 1348 if (!si.isDeclaredInManifest()) { 1349 return true; 1350 } 1351 } 1352 return false; 1353 } 1354 dump(@onNull PrintWriter pw, @NonNull String prefix, DumpFilter filter)1355 public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) { 1356 pw.println(); 1357 1358 pw.print(prefix); 1359 pw.print("Package: "); 1360 pw.print(getPackageName()); 1361 pw.print(" UID: "); 1362 pw.print(mPackageUid); 1363 pw.println(); 1364 1365 pw.print(prefix); 1366 pw.print(" "); 1367 pw.print("Calls: "); 1368 pw.print(getApiCallCount(/*unlimited=*/ false)); 1369 pw.println(); 1370 1371 // getApiCallCount() may have updated mLastKnownForegroundElapsedTime. 1372 pw.print(prefix); 1373 pw.print(" "); 1374 pw.print("Last known FG: "); 1375 pw.print(mLastKnownForegroundElapsedTime); 1376 pw.println(); 1377 1378 // This should be after getApiCallCount(), which may update it. 1379 pw.print(prefix); 1380 pw.print(" "); 1381 pw.print("Last reset: ["); 1382 pw.print(mLastResetTime); 1383 pw.print("] "); 1384 pw.print(ShortcutService.formatTime(mLastResetTime)); 1385 pw.println(); 1386 1387 getPackageInfo().dump(pw, prefix + " "); 1388 pw.println(); 1389 1390 pw.print(prefix); 1391 pw.println(" Shortcuts:"); 1392 long totalBitmapSize = 0; 1393 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 1394 final int size = shortcuts.size(); 1395 for (int i = 0; i < size; i++) { 1396 final ShortcutInfo si = shortcuts.valueAt(i); 1397 pw.println(si.toDumpString(prefix + " ")); 1398 if (si.getBitmapPath() != null) { 1399 final long len = new File(si.getBitmapPath()).length(); 1400 pw.print(prefix); 1401 pw.print(" "); 1402 pw.print("bitmap size="); 1403 pw.println(len); 1404 1405 totalBitmapSize += len; 1406 } 1407 } 1408 pw.print(prefix); 1409 pw.print(" "); 1410 pw.print("Total bitmap size: "); 1411 pw.print(totalBitmapSize); 1412 pw.print(" ("); 1413 pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize)); 1414 pw.println(")"); 1415 } 1416 1417 @Override dumpCheckin(boolean clear)1418 public JSONObject dumpCheckin(boolean clear) throws JSONException { 1419 final JSONObject result = super.dumpCheckin(clear); 1420 1421 int numDynamic = 0; 1422 int numPinned = 0; 1423 int numManifest = 0; 1424 int numBitmaps = 0; 1425 long totalBitmapSize = 0; 1426 1427 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 1428 final int size = shortcuts.size(); 1429 for (int i = 0; i < size; i++) { 1430 final ShortcutInfo si = shortcuts.valueAt(i); 1431 1432 if (si.isDynamic()) numDynamic++; 1433 if (si.isDeclaredInManifest()) numManifest++; 1434 if (si.isPinned()) numPinned++; 1435 1436 if (si.getBitmapPath() != null) { 1437 numBitmaps++; 1438 totalBitmapSize += new File(si.getBitmapPath()).length(); 1439 } 1440 } 1441 1442 result.put(KEY_DYNAMIC, numDynamic); 1443 result.put(KEY_MANIFEST, numManifest); 1444 result.put(KEY_PINNED, numPinned); 1445 result.put(KEY_BITMAPS, numBitmaps); 1446 result.put(KEY_BITMAP_BYTES, totalBitmapSize); 1447 1448 // TODO Log update frequency too. 1449 1450 return result; 1451 } 1452 1453 @Override saveToXml(@onNull XmlSerializer out, boolean forBackup)1454 public void saveToXml(@NonNull XmlSerializer out, boolean forBackup) 1455 throws IOException, XmlPullParserException { 1456 final int size = mShortcuts.size(); 1457 final int shareTargetSize = mShareTargets.size(); 1458 1459 if (size == 0 && shareTargetSize == 0 && mApiCallCount == 0) { 1460 return; // nothing to write. 1461 } 1462 1463 out.startTag(null, TAG_ROOT); 1464 1465 ShortcutService.writeAttr(out, ATTR_NAME, getPackageName()); 1466 ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount); 1467 ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime); 1468 getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup); 1469 1470 for (int j = 0; j < size; j++) { 1471 saveShortcut(out, mShortcuts.valueAt(j), forBackup, 1472 getPackageInfo().isBackupAllowed()); 1473 } 1474 1475 if (!forBackup) { 1476 for (int j = 0; j < shareTargetSize; j++) { 1477 mShareTargets.get(j).saveToXml(out); 1478 } 1479 } 1480 1481 out.endTag(null, TAG_ROOT); 1482 } 1483 saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup, boolean appSupportsBackup)1484 private void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup, 1485 boolean appSupportsBackup) 1486 throws IOException, XmlPullParserException { 1487 1488 final ShortcutService s = mShortcutUser.mService; 1489 1490 if (forBackup) { 1491 if (!(si.isPinned() && si.isEnabled())) { 1492 // We only backup pinned shortcuts that are enabled. 1493 // Note, this means, shortcuts that are restored but are blocked restore, e.g. due 1494 // to a lower version code, will not be ported to a new device. 1495 return; 1496 } 1497 } 1498 final boolean shouldBackupDetails = 1499 !forBackup // It's not backup 1500 || appSupportsBackup; // Or, it's a backup and app supports backup. 1501 1502 // Note: at this point no shortcuts should have bitmaps pending save, but if they do, 1503 // just remove the bitmap. 1504 if (si.isIconPendingSave()) { 1505 s.removeIconLocked(si); 1506 } 1507 out.startTag(null, TAG_SHORTCUT); 1508 ShortcutService.writeAttr(out, ATTR_ID, si.getId()); 1509 // writeAttr(out, "package", si.getPackageName()); // not needed 1510 ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity()); 1511 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 1512 ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle()); 1513 ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId()); 1514 ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName()); 1515 ShortcutService.writeAttr(out, ATTR_TEXT, si.getText()); 1516 ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId()); 1517 ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName()); 1518 if (shouldBackupDetails) { 1519 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage()); 1520 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID, 1521 si.getDisabledMessageResourceId()); 1522 ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME, 1523 si.getDisabledMessageResName()); 1524 } 1525 ShortcutService.writeAttr(out, ATTR_DISABLED_REASON, si.getDisabledReason()); 1526 ShortcutService.writeAttr(out, ATTR_TIMESTAMP, 1527 si.getLastChangedTimestamp()); 1528 final LocusId locusId = si.getLocusId(); 1529 if (locusId != null) { 1530 ShortcutService.writeAttr(out, ATTR_LOCUS_ID, si.getLocusId().getId()); 1531 } 1532 if (forBackup) { 1533 // Don't write icon information. Also drop the dynamic flag. 1534 1535 int flags = si.getFlags() & 1536 ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES 1537 | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE 1538 | ShortcutInfo.FLAG_DYNAMIC); 1539 ShortcutService.writeAttr(out, ATTR_FLAGS, flags); 1540 1541 // Set the publisher version code at every backup. 1542 final long packageVersionCode = getPackageInfo().getVersionCode(); 1543 if (packageVersionCode == 0) { 1544 s.wtf("Package version code should be available at this point."); 1545 // However, 0 is a valid version code, so we just go ahead with it... 1546 } 1547 } else { 1548 // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored 1549 // as dynamic. 1550 ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); 1551 1552 ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); 1553 ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId()); 1554 ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName()); 1555 ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath()); 1556 } 1557 1558 if (shouldBackupDetails) { 1559 { 1560 final Set<String> cat = si.getCategories(); 1561 if (cat != null && cat.size() > 0) { 1562 out.startTag(null, TAG_CATEGORIES); 1563 XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]), 1564 NAME_CATEGORIES, out); 1565 out.endTag(null, TAG_CATEGORIES); 1566 } 1567 } 1568 if (!forBackup) { // Don't backup the persons field. 1569 final Person[] persons = si.getPersons(); 1570 if (!ArrayUtils.isEmpty(persons)) { 1571 for (int i = 0; i < persons.length; i++) { 1572 final Person p = persons[i]; 1573 1574 out.startTag(null, TAG_PERSON); 1575 ShortcutService.writeAttr(out, ATTR_PERSON_NAME, p.getName()); 1576 ShortcutService.writeAttr(out, ATTR_PERSON_URI, p.getUri()); 1577 ShortcutService.writeAttr(out, ATTR_PERSON_KEY, p.getKey()); 1578 ShortcutService.writeAttr(out, ATTR_PERSON_IS_BOT, p.isBot()); 1579 ShortcutService.writeAttr(out, ATTR_PERSON_IS_IMPORTANT, p.isImportant()); 1580 out.endTag(null, TAG_PERSON); 1581 } 1582 } 1583 } 1584 final Intent[] intentsNoExtras = si.getIntentsNoExtras(); 1585 final PersistableBundle[] intentsExtras = si.getIntentPersistableExtrases(); 1586 final int numIntents = intentsNoExtras.length; 1587 for (int i = 0; i < numIntents; i++) { 1588 out.startTag(null, TAG_INTENT); 1589 ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]); 1590 ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]); 1591 out.endTag(null, TAG_INTENT); 1592 } 1593 1594 ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras()); 1595 } 1596 1597 out.endTag(null, TAG_SHORTCUT); 1598 } 1599 loadFromXml(ShortcutService s, ShortcutUser shortcutUser, XmlPullParser parser, boolean fromBackup)1600 public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser, 1601 XmlPullParser parser, boolean fromBackup) 1602 throws IOException, XmlPullParserException { 1603 1604 final String packageName = ShortcutService.parseStringAttribute(parser, 1605 ATTR_NAME); 1606 1607 final ShortcutPackage ret = new ShortcutPackage(shortcutUser, 1608 shortcutUser.getUserId(), packageName); 1609 1610 ret.mApiCallCount = 1611 ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT); 1612 ret.mLastResetTime = 1613 ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET); 1614 1615 1616 final int outerDepth = parser.getDepth(); 1617 int type; 1618 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 1619 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 1620 if (type != XmlPullParser.START_TAG) { 1621 continue; 1622 } 1623 final int depth = parser.getDepth(); 1624 final String tag = parser.getName(); 1625 if (depth == outerDepth + 1) { 1626 switch (tag) { 1627 case ShortcutPackageInfo.TAG_ROOT: 1628 ret.getPackageInfo().loadFromXml(parser, fromBackup); 1629 1630 continue; 1631 case TAG_SHORTCUT: 1632 final ShortcutInfo si = parseShortcut(parser, packageName, 1633 shortcutUser.getUserId(), fromBackup); 1634 1635 // Don't use addShortcut(), we don't need to save the icon. 1636 ret.mShortcuts.put(si.getId(), si); 1637 continue; 1638 case TAG_SHARE_TARGET: 1639 ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser)); 1640 continue; 1641 } 1642 } 1643 ShortcutService.warnForInvalidTag(depth, tag); 1644 } 1645 return ret; 1646 } 1647 parseShortcut(XmlPullParser parser, String packageName, @UserIdInt int userId, boolean fromBackup)1648 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName, 1649 @UserIdInt int userId, boolean fromBackup) 1650 throws IOException, XmlPullParserException { 1651 String id; 1652 ComponentName activityComponent; 1653 // Icon icon; 1654 String title; 1655 int titleResId; 1656 String titleResName; 1657 String text; 1658 int textResId; 1659 String textResName; 1660 String disabledMessage; 1661 int disabledMessageResId; 1662 String disabledMessageResName; 1663 int disabledReason; 1664 Intent intentLegacy; 1665 PersistableBundle intentPersistableExtrasLegacy = null; 1666 ArrayList<Intent> intents = new ArrayList<>(); 1667 int rank; 1668 PersistableBundle extras = null; 1669 long lastChangedTimestamp; 1670 int flags; 1671 int iconResId; 1672 String iconResName; 1673 String bitmapPath; 1674 final String locusIdString; 1675 int backupVersionCode; 1676 ArraySet<String> categories = null; 1677 ArrayList<Person> persons = new ArrayList<>(); 1678 1679 id = ShortcutService.parseStringAttribute(parser, ATTR_ID); 1680 activityComponent = ShortcutService.parseComponentNameAttribute(parser, 1681 ATTR_ACTIVITY); 1682 title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE); 1683 titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID); 1684 titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME); 1685 text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT); 1686 textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID); 1687 textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME); 1688 disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE); 1689 disabledMessageResId = ShortcutService.parseIntAttribute(parser, 1690 ATTR_DISABLED_MESSAGE_RES_ID); 1691 disabledMessageResName = ShortcutService.parseStringAttribute(parser, 1692 ATTR_DISABLED_MESSAGE_RES_NAME); 1693 disabledReason = ShortcutService.parseIntAttribute(parser, ATTR_DISABLED_REASON); 1694 intentLegacy = ShortcutService.parseIntentAttributeNoDefault(parser, ATTR_INTENT_LEGACY); 1695 rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK); 1696 lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP); 1697 flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS); 1698 iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID); 1699 iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME); 1700 bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH); 1701 locusIdString = ShortcutService.parseStringAttribute(parser, ATTR_LOCUS_ID); 1702 1703 final int outerDepth = parser.getDepth(); 1704 int type; 1705 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 1706 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 1707 if (type != XmlPullParser.START_TAG) { 1708 continue; 1709 } 1710 final int depth = parser.getDepth(); 1711 final String tag = parser.getName(); 1712 if (ShortcutService.DEBUG_LOAD) { 1713 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 1714 depth, type, tag)); 1715 } 1716 switch (tag) { 1717 case TAG_INTENT_EXTRAS_LEGACY: 1718 intentPersistableExtrasLegacy = PersistableBundle.restoreFromXml(parser); 1719 continue; 1720 case TAG_INTENT: 1721 intents.add(parseIntent(parser)); 1722 continue; 1723 case TAG_EXTRAS: 1724 extras = PersistableBundle.restoreFromXml(parser); 1725 continue; 1726 case TAG_CATEGORIES: 1727 // This just contains string-array. 1728 continue; 1729 case TAG_PERSON: 1730 persons.add(parsePerson(parser)); 1731 continue; 1732 case TAG_STRING_ARRAY_XMLUTILS: 1733 if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser, 1734 ATTR_NAME_XMLUTILS))) { 1735 final String[] ar = XmlUtils.readThisStringArrayXml( 1736 parser, TAG_STRING_ARRAY_XMLUTILS, null); 1737 categories = new ArraySet<>(ar.length); 1738 for (int i = 0; i < ar.length; i++) { 1739 categories.add(ar[i]); 1740 } 1741 } 1742 continue; 1743 } 1744 throw ShortcutService.throwForInvalidTag(depth, tag); 1745 } 1746 1747 if (intentLegacy != null) { 1748 // For the legacy file format which supported only one intent per shortcut. 1749 ShortcutInfo.setIntentExtras(intentLegacy, intentPersistableExtrasLegacy); 1750 intents.clear(); 1751 intents.add(intentLegacy); 1752 } 1753 1754 1755 if ((disabledReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED) 1756 && ((flags & ShortcutInfo.FLAG_DISABLED) != 0)) { 1757 // We didn't used to have the disabled reason, so if a shortcut is disabled 1758 // and has no reason, we assume it was disabled by publisher. 1759 disabledReason = ShortcutInfo.DISABLED_REASON_BY_APP; 1760 } 1761 1762 // All restored shortcuts are initially "shadow". 1763 if (fromBackup) { 1764 flags |= ShortcutInfo.FLAG_SHADOW; 1765 } 1766 1767 final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString); 1768 1769 return new ShortcutInfo( 1770 userId, id, packageName, activityComponent, /* icon= */ null, 1771 title, titleResId, titleResName, text, textResId, textResName, 1772 disabledMessage, disabledMessageResId, disabledMessageResName, 1773 categories, 1774 intents.toArray(new Intent[intents.size()]), 1775 rank, extras, lastChangedTimestamp, flags, 1776 iconResId, iconResName, bitmapPath, disabledReason, 1777 persons.toArray(new Person[persons.size()]), locusId); 1778 } 1779 parseIntent(XmlPullParser parser)1780 private static Intent parseIntent(XmlPullParser parser) 1781 throws IOException, XmlPullParserException { 1782 1783 Intent intent = ShortcutService.parseIntentAttribute(parser, 1784 ATTR_INTENT_NO_EXTRA); 1785 1786 final int outerDepth = parser.getDepth(); 1787 int type; 1788 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 1789 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 1790 if (type != XmlPullParser.START_TAG) { 1791 continue; 1792 } 1793 final int depth = parser.getDepth(); 1794 final String tag = parser.getName(); 1795 if (ShortcutService.DEBUG_LOAD) { 1796 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 1797 depth, type, tag)); 1798 } 1799 switch (tag) { 1800 case TAG_EXTRAS: 1801 ShortcutInfo.setIntentExtras(intent, 1802 PersistableBundle.restoreFromXml(parser)); 1803 continue; 1804 } 1805 throw ShortcutService.throwForInvalidTag(depth, tag); 1806 } 1807 return intent; 1808 } 1809 parsePerson(XmlPullParser parser)1810 private static Person parsePerson(XmlPullParser parser) 1811 throws IOException, XmlPullParserException { 1812 CharSequence name = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_NAME); 1813 String uri = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_URI); 1814 String key = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_KEY); 1815 boolean isBot = ShortcutService.parseBooleanAttribute(parser, ATTR_PERSON_IS_BOT); 1816 boolean isImportant = ShortcutService.parseBooleanAttribute(parser, 1817 ATTR_PERSON_IS_IMPORTANT); 1818 1819 Person.Builder builder = new Person.Builder(); 1820 builder.setName(name).setUri(uri).setKey(key).setBot(isBot).setImportant(isImportant); 1821 return builder.build(); 1822 } 1823 1824 @VisibleForTesting getAllShortcutsForTest()1825 List<ShortcutInfo> getAllShortcutsForTest() { 1826 return new ArrayList<>(mShortcuts.values()); 1827 } 1828 1829 @VisibleForTesting getAllShareTargetsForTest()1830 List<ShareTargetInfo> getAllShareTargetsForTest() { 1831 return new ArrayList<>(mShareTargets); 1832 } 1833 1834 @Override verifyStates()1835 public void verifyStates() { 1836 super.verifyStates(); 1837 1838 boolean failed = false; 1839 1840 final ShortcutService s = mShortcutUser.mService; 1841 1842 final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all = 1843 sortShortcutsToActivities(); 1844 1845 // Make sure each activity won't have more than max shortcuts. 1846 for (int outer = all.size() - 1; outer >= 0; outer--) { 1847 final ArrayList<ShortcutInfo> list = all.valueAt(outer); 1848 if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) { 1849 failed = true; 1850 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer) 1851 + " has " + all.valueAt(outer).size() + " shortcuts."); 1852 } 1853 1854 // Sort by rank. 1855 Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank())); 1856 1857 // Split into two arrays for each kind. 1858 final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list); 1859 dynamicList.removeIf((si) -> !si.isDynamic()); 1860 1861 final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list); 1862 dynamicList.removeIf((si) -> !si.isManifestShortcut()); 1863 1864 verifyRanksSequential(dynamicList); 1865 verifyRanksSequential(manifestList); 1866 } 1867 1868 // Verify each shortcut's status. 1869 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1870 final ShortcutInfo si = mShortcuts.valueAt(i); 1871 if (!(si.isDeclaredInManifest() || si.isDynamic() || si.isPinned())) { 1872 failed = true; 1873 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1874 + " is not manifest, dynamic or pinned."); 1875 } 1876 if (si.isDeclaredInManifest() && si.isDynamic()) { 1877 failed = true; 1878 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1879 + " is both dynamic and manifest at the same time."); 1880 } 1881 if (si.getActivity() == null && !si.isFloating()) { 1882 failed = true; 1883 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1884 + " has null activity, but not floating."); 1885 } 1886 if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) { 1887 failed = true; 1888 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1889 + " is not floating, but is disabled."); 1890 } 1891 if (si.isFloating() && si.getRank() != 0) { 1892 failed = true; 1893 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1894 + " is floating, but has rank=" + si.getRank()); 1895 } 1896 if (si.getIcon() != null) { 1897 failed = true; 1898 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1899 + " still has an icon"); 1900 } 1901 if (si.hasAdaptiveBitmap() && !si.hasIconFile()) { 1902 failed = true; 1903 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1904 + " has adaptive bitmap but was not saved to a file."); 1905 } 1906 if (si.hasIconFile() && si.hasIconResource()) { 1907 failed = true; 1908 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1909 + " has both resource and bitmap icons"); 1910 } 1911 if (si.isEnabled() 1912 != (si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)) { 1913 failed = true; 1914 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1915 + " isEnabled() and getDisabledReason() disagree: " 1916 + si.isEnabled() + " vs " + si.getDisabledReason()); 1917 } 1918 if ((si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER) 1919 && (getPackageInfo().getBackupSourceVersionCode() 1920 == ShortcutInfo.VERSION_CODE_UNKNOWN)) { 1921 failed = true; 1922 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1923 + " RESTORED_VERSION_LOWER with no backup source version code."); 1924 } 1925 if (s.isDummyMainActivity(si.getActivity())) { 1926 failed = true; 1927 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1928 + " has a dummy target activity"); 1929 } 1930 } 1931 1932 if (failed) { 1933 throw new IllegalStateException("See logcat for errors"); 1934 } 1935 } 1936 verifyRanksSequential(List<ShortcutInfo> list)1937 private boolean verifyRanksSequential(List<ShortcutInfo> list) { 1938 boolean failed = false; 1939 1940 for (int i = 0; i < list.size(); i++) { 1941 final ShortcutInfo si = list.get(i); 1942 if (si.getRank() != i) { 1943 failed = true; 1944 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() 1945 + " rank=" + si.getRank() + " but expected to be "+ i); 1946 } 1947 } 1948 return failed; 1949 } 1950 } 1951