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