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