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