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