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