ShortcutService.java revision 5504622fb01ab9774b5e73d05f86ee03a8b68ab7
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.app.ActivityManager; 22import android.content.ComponentName; 23import android.content.ContentProvider; 24import android.content.Context; 25import android.content.Intent; 26import android.content.pm.IShortcutService; 27import android.content.pm.LauncherApps; 28import android.content.pm.LauncherApps.ShortcutQuery; 29import android.content.pm.PackageManager; 30import android.content.pm.PackageManager.NameNotFoundException; 31import android.content.pm.ParceledListSlice; 32import android.content.pm.ShortcutInfo; 33import android.content.pm.ShortcutServiceInternal; 34import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; 35import android.graphics.Bitmap; 36import android.graphics.Bitmap.CompressFormat; 37import android.graphics.BitmapFactory; 38import android.graphics.Canvas; 39import android.graphics.RectF; 40import android.graphics.drawable.Icon; 41import android.net.Uri; 42import android.os.Binder; 43import android.os.Environment; 44import android.os.Handler; 45import android.os.ParcelFileDescriptor; 46import android.os.PersistableBundle; 47import android.os.Process; 48import android.os.RemoteException; 49import android.os.ResultReceiver; 50import android.os.SELinux; 51import android.os.ShellCommand; 52import android.os.UserHandle; 53import android.text.TextUtils; 54import android.text.format.Formatter; 55import android.text.format.Time; 56import android.util.ArrayMap; 57import android.util.ArraySet; 58import android.util.AtomicFile; 59import android.util.Slog; 60import android.util.SparseArray; 61import android.util.TypedValue; 62import android.util.Xml; 63 64import com.android.internal.annotations.GuardedBy; 65import com.android.internal.annotations.VisibleForTesting; 66import com.android.internal.os.BackgroundThread; 67import com.android.internal.util.FastXmlSerializer; 68import com.android.internal.util.Preconditions; 69import com.android.server.LocalServices; 70import com.android.server.SystemService; 71 72import libcore.io.IoUtils; 73 74import org.xmlpull.v1.XmlPullParser; 75import org.xmlpull.v1.XmlPullParserException; 76import org.xmlpull.v1.XmlSerializer; 77 78import java.io.File; 79import java.io.FileDescriptor; 80import java.io.FileInputStream; 81import java.io.FileNotFoundException; 82import java.io.FileOutputStream; 83import java.io.IOException; 84import java.io.InputStream; 85import java.io.PrintWriter; 86import java.net.URISyntaxException; 87import java.nio.charset.StandardCharsets; 88import java.util.ArrayList; 89import java.util.List; 90import java.util.function.Predicate; 91 92/** 93 * TODO: 94 * 95 * - Implement launchShortcut 96 * 97 * - Detect when already registered instances are passed to APIs again, which might break 98 * internal bitmap handling. 99 * 100 * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res 101 * -> Need to scan all packages when a user starts too. 102 * -> Clear data -> remove all dynamic? but not the pinned? 103 * 104 * - Pinned per each launcher package (multiple launchers) 105 * 106 * - Load config from settings 107 * 108 * - Make save async (should we?) 109 * 110 * - Scan and remove orphan bitmaps (just in case). 111 * 112 * - Backup & restore 113 */ 114public class ShortcutService extends IShortcutService.Stub { 115 static final String TAG = "ShortcutService"; 116 117 private static final boolean DEBUG = true; // STOPSHIP if true 118 private static final boolean DEBUG_LOAD = true; // STOPSHIP if true 119 120 private static final int DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day 121 private static final int DEFAULT_MAX_DAILY_UPDATES = 10; 122 private static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5; 123 private static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96; 124 private static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48; 125 126 private static final int SAVE_DELAY_MS = 5000; // in milliseconds. 127 128 @VisibleForTesting 129 static final String FILENAME_BASE_STATE = "shortcut_service.xml"; 130 131 @VisibleForTesting 132 static final String DIRECTORY_PER_USER = "shortcut_service"; 133 134 @VisibleForTesting 135 static final String FILENAME_USER_PACKAGES = "shortcuts.xml"; 136 137 static final String DIRECTORY_BITMAPS = "bitmaps"; 138 139 private static final String TAG_ROOT = "root"; 140 private static final String TAG_PACKAGE = "package"; 141 private static final String TAG_LAST_RESET_TIME = "last_reset_time"; 142 private static final String TAG_INTENT_EXTRAS = "intent-extras"; 143 private static final String TAG_EXTRAS = "extras"; 144 private static final String TAG_SHORTCUT = "shortcut"; 145 146 private static final String ATTR_VALUE = "value"; 147 private static final String ATTR_NAME = "name"; 148 private static final String ATTR_DYNAMIC_COUNT = "dynamic-count"; 149 private static final String ATTR_CALL_COUNT = "call-count"; 150 private static final String ATTR_LAST_RESET = "last-reset"; 151 private static final String ATTR_ID = "id"; 152 private static final String ATTR_ACTIVITY = "activity"; 153 private static final String ATTR_TITLE = "title"; 154 private static final String ATTR_INTENT = "intent"; 155 private static final String ATTR_WEIGHT = "weight"; 156 private static final String ATTR_TIMESTAMP = "timestamp"; 157 private static final String ATTR_FLAGS = "flags"; 158 private static final String ATTR_ICON_RES = "icon-res"; 159 private static final String ATTR_BITMAP_PATH = "bitmap-path"; 160 161 private final Context mContext; 162 163 private final Object mLock = new Object(); 164 165 private final Handler mHandler; 166 167 @GuardedBy("mLock") 168 private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1); 169 170 @GuardedBy("mLock") 171 private long mRawLastResetTime; 172 173 /** 174 * All the information relevant to shortcuts from a single package (per-user). 175 * 176 * TODO Move the persisting code to this class. 177 * 178 * Only save/load/dump should look/touch inside this class. 179 */ 180 private static class PackageShortcuts { 181 @UserIdInt 182 private final int mUserId; 183 184 @NonNull 185 private final String mPackageName; 186 187 /** 188 * All the shortcuts from the package, keyed on IDs. 189 */ 190 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 191 192 /** 193 * # of dynamic shortcuts. 194 */ 195 private int mDynamicShortcutCount = 0; 196 197 /** 198 * # of times the package has called rate-limited APIs. 199 */ 200 private int mApiCallCount; 201 202 /** 203 * When {@link #mApiCallCount} was reset last time. 204 */ 205 private long mLastResetTime; 206 207 private PackageShortcuts(int userId, String packageName) { 208 mUserId = userId; 209 mPackageName = packageName; 210 } 211 212 @GuardedBy("mLock") 213 @Nullable 214 public ShortcutInfo findShortcutById(String id) { 215 return mShortcuts.get(id); 216 } 217 218 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s, 219 @NonNull String id) { 220 final ShortcutInfo shortcut = mShortcuts.remove(id); 221 if (shortcut != null) { 222 s.removeIcon(mUserId, shortcut); 223 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED); 224 } 225 return shortcut; 226 } 227 228 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) { 229 deleteShortcut(s, newShortcut.getId()); 230 s.saveIconAndFixUpShortcut(mUserId, newShortcut); 231 mShortcuts.put(newShortcut.getId(), newShortcut); 232 } 233 234 /** 235 * Add a shortcut, or update one with the same ID, with taking over existing flags. 236 * 237 * It checks the max number of dynamic shortcuts. 238 */ 239 @GuardedBy("mLock") 240 public void updateShortcutWithCapping(@NonNull ShortcutService s, 241 @NonNull ShortcutInfo newShortcut) { 242 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 243 244 int oldFlags = 0; 245 int newDynamicCount = mDynamicShortcutCount; 246 247 if (oldShortcut != null) { 248 oldFlags = oldShortcut.getFlags(); 249 if (oldShortcut.isDynamic()) { 250 newDynamicCount--; 251 } 252 } 253 if (newShortcut.isDynamic()) { 254 newDynamicCount++; 255 } 256 // Make sure there's still room. 257 s.enforceMaxDynamicShortcuts(newDynamicCount); 258 259 // Okay, make it dynamic and add. 260 newShortcut.addFlags(oldFlags); 261 262 addShortcut(s, newShortcut); 263 mDynamicShortcutCount = newDynamicCount; 264 } 265 266 /** 267 * Remove all shortcuts that aren't pinned nor dynamic. 268 */ 269 private void removeOrphans(@NonNull ShortcutService s) { 270 ArrayList<String> removeList = null; // Lazily initialize. 271 272 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 273 final ShortcutInfo si = mShortcuts.valueAt(i); 274 275 if (si.isPinned() || si.isDynamic()) continue; 276 277 if (removeList == null) { 278 removeList = new ArrayList<>(); 279 } 280 removeList.add(si.getId()); 281 } 282 if (removeList != null) { 283 for (int i = removeList.size() - 1 ; i >= 0; i--) { 284 deleteShortcut(s, removeList.get(i)); 285 } 286 } 287 } 288 289 @GuardedBy("mLock") 290 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) { 291 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 292 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC); 293 } 294 removeOrphans(s); 295 mDynamicShortcutCount = 0; 296 } 297 298 @GuardedBy("mLock") 299 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) { 300 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 301 302 if (oldShortcut == null) { 303 return; 304 } 305 if (oldShortcut.isDynamic()) { 306 mDynamicShortcutCount--; 307 } 308 if (oldShortcut.isPinned()) { 309 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 310 } else { 311 deleteShortcut(s, shortcutId); 312 } 313 } 314 315 @GuardedBy("mLock") 316 public void replacePinned(@NonNull ShortcutService s, String launcherPackage, 317 List<String> shortcutIds) { 318 319 // TODO Should be per launcherPackage. 320 321 // First, un-pin all shortcuts 322 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 323 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 324 } 325 326 // Then pin ALL 327 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 328 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i)); 329 if (shortcut != null) { 330 shortcut.addFlags(ShortcutInfo.FLAG_PINNED); 331 } 332 } 333 334 removeOrphans(s); 335 } 336 337 /** 338 * Number of calls that the caller has made, since the last reset. 339 */ 340 @GuardedBy("mLock") 341 public int getApiCallCount(@NonNull ShortcutService s) { 342 final long last = s.getLastResetTimeLocked(); 343 344 final long now = s.injectCurrentTimeMillis(); 345 if (mLastResetTime > now) { 346 // Clock rewound. // TODO Test it 347 mLastResetTime = now; 348 } 349 350 // If not reset yet, then reset. 351 if (mLastResetTime < last) { 352 mApiCallCount = 0; 353 mLastResetTime = last; 354 } 355 return mApiCallCount; 356 } 357 358 /** 359 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 360 * and return true. Otherwise just return false. 361 */ 362 @GuardedBy("mLock") 363 public boolean tryApiCall(@NonNull ShortcutService s) { 364 if (getApiCallCount(s) >= s.mMaxDailyUpdates) { 365 return false; 366 } 367 mApiCallCount++; 368 return true; 369 } 370 371 @GuardedBy("mLock") 372 public void resetRateLimitingForCommandLine() { 373 mApiCallCount = 0; 374 mLastResetTime = 0; 375 } 376 377 /** 378 * Find all shortcuts that match {@code query}. 379 */ 380 @GuardedBy("mLock") 381 public void findAll(@NonNull List<ShortcutInfo> result, 382 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 383 for (int i = 0; i < mShortcuts.size(); i++) { 384 final ShortcutInfo si = mShortcuts.valueAt(i); 385 if (query == null || query.test(si)) { 386 result.add(si.clone(cloneFlag)); 387 } 388 } 389 } 390 } 391 392 /** 393 * User ID -> package name -> list of ShortcutInfos. 394 */ 395 @GuardedBy("mLock") 396 private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts = 397 new SparseArray<>(); 398 399 /** 400 * Max number of dynamic shortcuts that each application can have at a time. 401 */ 402 private int mMaxDynamicShortcuts; 403 404 /** 405 * Max number of updating API calls that each application can make a day. 406 */ 407 private int mMaxDailyUpdates; 408 409 /** 410 * Actual throttling-reset interval. By default it's a day. 411 */ 412 private long mResetInterval; 413 414 /** 415 * Icon max width/height in pixels. 416 */ 417 private int mMaxIconDimension; 418 419 private CompressFormat mIconPersistFormat = CompressFormat.PNG; 420 421 private int mIconPersistQuality = 100; 422 423 public ShortcutService(Context context) { 424 mContext = Preconditions.checkNotNull(context); 425 LocalServices.addService(ShortcutServiceInternal.class, new LocalService()); 426 mHandler = new Handler(BackgroundThread.get().getLooper()); 427 } 428 429 /** 430 * System service lifecycle. 431 */ 432 public static final class Lifecycle extends SystemService { 433 final ShortcutService mService; 434 435 public Lifecycle(Context context) { 436 super(context); 437 mService = new ShortcutService(context); 438 } 439 440 @Override 441 public void onStart() { 442 publishBinderService(Context.SHORTCUT_SERVICE, mService); 443 } 444 445 @Override 446 public void onBootPhase(int phase) { 447 mService.onBootPhase(phase); 448 } 449 450 @Override 451 public void onCleanupUser(int userHandle) { 452 synchronized (mService.mLock) { 453 mService.onCleanupUserInner(userHandle); 454 } 455 } 456 457 @Override 458 public void onStartUser(int userId) { 459 synchronized (mService.mLock) { 460 mService.onStartUserLocked(userId); 461 } 462 } 463 } 464 465 /** lifecycle event */ 466 void onBootPhase(int phase) { 467 if (DEBUG) { 468 Slog.d(TAG, "onBootPhase: " + phase); 469 } 470 switch (phase) { 471 case SystemService.PHASE_LOCK_SETTINGS_READY: 472 initialize(); 473 break; 474 } 475 } 476 477 /** lifecycle event */ 478 void onStartUserLocked(int userId) { 479 // Preload 480 getUserShortcutsLocked(userId); 481 } 482 483 /** lifecycle event */ 484 void onCleanupUserInner(int userId) { 485 // Unload 486 mShortcuts.delete(userId); 487 } 488 489 /** Return the base state file name */ 490 private AtomicFile getBaseStateFile() { 491 final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE); 492 path.mkdirs(); 493 return new AtomicFile(path); 494 } 495 496 /** 497 * Init the instance. (load the state file, etc) 498 */ 499 private void initialize() { 500 synchronized (mLock) { 501 injectLoadConfigurationLocked(); 502 loadBaseStateLocked(); 503 } 504 } 505 506 // Test overrides it to inject different values. 507 @VisibleForTesting 508 void injectLoadConfigurationLocked() { 509 mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L; 510 mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES; 511 mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP; 512 513 final int iconDimensionDp = (injectIsLowRamDevice() 514 ? DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP : DEFAULT_MAX_ICON_DIMENSION_DP); 515 mMaxIconDimension = 516 (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, iconDimensionDp, 517 mContext.getResources().getDisplayMetrics()); 518 } 519 520 // === Persisting === 521 522 @Nullable 523 private String parseStringAttribute(XmlPullParser parser, String attribute) { 524 return parser.getAttributeValue(null, attribute); 525 } 526 527 private long parseLongAttribute(XmlPullParser parser, String attribute) { 528 final String value = parseStringAttribute(parser, attribute); 529 if (TextUtils.isEmpty(value)) { 530 return 0; 531 } 532 try { 533 return Long.parseLong(value); 534 } catch (NumberFormatException e) { 535 Slog.e(TAG, "Error parsing long " + value); 536 return 0; 537 } 538 } 539 540 @Nullable 541 private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) { 542 final String value = parseStringAttribute(parser, attribute); 543 if (TextUtils.isEmpty(value)) { 544 return null; 545 } 546 return ComponentName.unflattenFromString(value); 547 } 548 549 @Nullable 550 private Intent parseIntentAttribute(XmlPullParser parser, String attribute) { 551 final String value = parseStringAttribute(parser, attribute); 552 if (TextUtils.isEmpty(value)) { 553 return null; 554 } 555 try { 556 return Intent.parseUri(value, /* flags =*/ 0); 557 } catch (URISyntaxException e) { 558 Slog.e(TAG, "Error parsing intent", e); 559 return null; 560 } 561 } 562 563 private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException { 564 if (TextUtils.isEmpty(value)) return; 565 566 out.startTag(null, tag); 567 out.attribute(null, ATTR_VALUE, value); 568 out.endTag(null, tag); 569 } 570 571 private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException { 572 writeTagValue(out, tag, Long.toString(value)); 573 } 574 575 private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle) 576 throws IOException, XmlPullParserException { 577 if (bundle == null) return; 578 579 out.startTag(null, tag); 580 bundle.saveToXml(out); 581 out.endTag(null, tag); 582 } 583 584 private void writeAttr(XmlSerializer out, String name, String value) throws IOException { 585 if (TextUtils.isEmpty(value)) return; 586 587 out.attribute(null, name, value); 588 } 589 590 private void writeAttr(XmlSerializer out, String name, long value) throws IOException { 591 writeAttr(out, name, String.valueOf(value)); 592 } 593 594 private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException { 595 if (comp == null) return; 596 writeAttr(out, name, comp.flattenToString()); 597 } 598 599 private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException { 600 if (intent == null) return; 601 602 writeAttr(out, name, intent.toUri(/* flags =*/ 0)); 603 } 604 605 @VisibleForTesting 606 void saveBaseStateLocked() { 607 final AtomicFile file = getBaseStateFile(); 608 if (DEBUG) { 609 Slog.i(TAG, "Saving to " + file.getBaseFile()); 610 } 611 612 FileOutputStream outs = null; 613 try { 614 outs = file.startWrite(); 615 616 // Write to XML 617 XmlSerializer out = new FastXmlSerializer(); 618 out.setOutput(outs, StandardCharsets.UTF_8.name()); 619 out.startDocument(null, true); 620 out.startTag(null, TAG_ROOT); 621 622 // Body. 623 writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime); 624 625 // Epilogue. 626 out.endTag(null, TAG_ROOT); 627 out.endDocument(); 628 629 // Close. 630 file.finishWrite(outs); 631 } catch (IOException e) { 632 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 633 file.failWrite(outs); 634 } 635 } 636 637 private void loadBaseStateLocked() { 638 mRawLastResetTime = 0; 639 640 final AtomicFile file = getBaseStateFile(); 641 if (DEBUG) { 642 Slog.i(TAG, "Loading from " + file.getBaseFile()); 643 } 644 try (FileInputStream in = file.openRead()) { 645 XmlPullParser parser = Xml.newPullParser(); 646 parser.setInput(in, StandardCharsets.UTF_8.name()); 647 648 int type; 649 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 650 if (type != XmlPullParser.START_TAG) { 651 continue; 652 } 653 final int depth = parser.getDepth(); 654 // Check the root tag 655 final String tag = parser.getName(); 656 if (depth == 1) { 657 if (!TAG_ROOT.equals(tag)) { 658 Slog.e(TAG, "Invalid root tag: " + tag); 659 return; 660 } 661 continue; 662 } 663 // Assume depth == 2 664 switch (tag) { 665 case TAG_LAST_RESET_TIME: 666 mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE); 667 break; 668 default: 669 Slog.e(TAG, "Invalid tag: " + tag); 670 break; 671 } 672 } 673 } catch (FileNotFoundException e) { 674 // Use the default 675 } catch (IOException|XmlPullParserException e) { 676 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 677 678 mRawLastResetTime = 0; 679 } 680 // Adjust the last reset time. 681 getLastResetTimeLocked(); 682 } 683 684 private void saveUserLocked(@UserIdInt int userId) { 685 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); 686 if (DEBUG) { 687 Slog.i(TAG, "Saving to " + path); 688 } 689 path.mkdirs(); 690 final AtomicFile file = new AtomicFile(path); 691 FileOutputStream outs = null; 692 try { 693 outs = file.startWrite(); 694 695 // Write to XML 696 XmlSerializer out = new FastXmlSerializer(); 697 out.setOutput(outs, StandardCharsets.UTF_8.name()); 698 out.startDocument(null, true); 699 out.startTag(null, TAG_ROOT); 700 701 final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId); 702 703 // Body. 704 for (int i = 0; i < packages.size(); i++) { 705 final String packageName = packages.keyAt(i); 706 final PackageShortcuts packageShortcuts = packages.valueAt(i); 707 708 // TODO Move this to PackageShortcuts. 709 710 out.startTag(null, TAG_PACKAGE); 711 712 writeAttr(out, ATTR_NAME, packageName); 713 writeAttr(out, ATTR_DYNAMIC_COUNT, packageShortcuts.mDynamicShortcutCount); 714 writeAttr(out, ATTR_CALL_COUNT, packageShortcuts.mApiCallCount); 715 writeAttr(out, ATTR_LAST_RESET, packageShortcuts.mLastResetTime); 716 717 final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts; 718 final int size = shortcuts.size(); 719 for (int j = 0; j < size; j++) { 720 saveShortcut(out, shortcuts.valueAt(j)); 721 } 722 723 out.endTag(null, TAG_PACKAGE); 724 } 725 726 // Epilogue. 727 out.endTag(null, TAG_ROOT); 728 out.endDocument(); 729 730 // Close. 731 file.finishWrite(outs); 732 } catch (IOException|XmlPullParserException e) { 733 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 734 file.failWrite(outs); 735 } 736 } 737 738 private void saveShortcut(XmlSerializer out, ShortcutInfo si) 739 throws IOException, XmlPullParserException { 740 out.startTag(null, TAG_SHORTCUT); 741 writeAttr(out, ATTR_ID, si.getId()); 742 // writeAttr(out, "package", si.getPackageName()); // not needed 743 writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent()); 744 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 745 writeAttr(out, ATTR_TITLE, si.getTitle()); 746 writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); 747 writeAttr(out, ATTR_WEIGHT, si.getWeight()); 748 writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp()); 749 writeAttr(out, ATTR_FLAGS, si.getFlags()); 750 writeAttr(out, ATTR_ICON_RES, si.getIconResourceId()); 751 writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath()); 752 753 writeTagExtra(out, TAG_INTENT_EXTRAS, si.getIntentPersistableExtras()); 754 writeTagExtra(out, TAG_EXTRAS, si.getExtras()); 755 756 out.endTag(null, TAG_SHORTCUT); 757 } 758 759 private static IOException throwForInvalidTag(int depth, String tag) throws IOException { 760 throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth)); 761 } 762 763 @Nullable 764 private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) { 765 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); 766 if (DEBUG) { 767 Slog.i(TAG, "Loading from " + path); 768 } 769 path.mkdirs(); 770 final AtomicFile file = new AtomicFile(path); 771 772 final FileInputStream in; 773 try { 774 in = file.openRead(); 775 } catch (FileNotFoundException e) { 776 if (DEBUG) { 777 Slog.i(TAG, "Not found " + path); 778 } 779 return null; 780 } 781 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>(); 782 try { 783 XmlPullParser parser = Xml.newPullParser(); 784 parser.setInput(in, StandardCharsets.UTF_8.name()); 785 786 String packageName = null; 787 PackageShortcuts shortcuts = null; 788 789 int type; 790 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 791 if (type != XmlPullParser.START_TAG) { 792 continue; 793 } 794 final int depth = parser.getDepth(); 795 796 // TODO Move some of this to PackageShortcuts. 797 798 final String tag = parser.getName(); 799 if (DEBUG_LOAD) { 800 Slog.d(TAG, String.format("depth=%d type=%d name=%s", 801 depth, type, tag)); 802 } 803 switch (depth) { 804 case 1: { 805 if (TAG_ROOT.equals(tag)) { 806 continue; 807 } 808 break; 809 } 810 case 2: { 811 switch (tag) { 812 case TAG_PACKAGE: 813 packageName = parseStringAttribute(parser, ATTR_NAME); 814 shortcuts = new PackageShortcuts(userId, packageName); 815 ret.put(packageName, shortcuts); 816 817 shortcuts.mDynamicShortcutCount = 818 (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT); 819 shortcuts.mApiCallCount = 820 (int) parseLongAttribute(parser, ATTR_CALL_COUNT); 821 shortcuts.mLastResetTime = parseLongAttribute(parser, 822 ATTR_LAST_RESET); 823 continue; 824 } 825 break; 826 } 827 case 3: { 828 switch (tag) { 829 case TAG_SHORTCUT: 830 final ShortcutInfo si = parseShortcut(parser, packageName); 831 832 // Don't use addShortcut(), we don't need to save the icon. 833 shortcuts.mShortcuts.put(si.getId(), si); 834 continue; 835 } 836 break; 837 } 838 } 839 throwForInvalidTag(depth, tag); 840 } 841 return ret; 842 } catch (IOException|XmlPullParserException e) { 843 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 844 return null; 845 } finally { 846 IoUtils.closeQuietly(in); 847 } 848 } 849 850 private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName) 851 throws IOException, XmlPullParserException { 852 String id; 853 ComponentName activityComponent; 854 Icon icon; 855 String title; 856 Intent intent; 857 PersistableBundle intentPersistableExtras = null; 858 int weight; 859 PersistableBundle extras = null; 860 long lastChangedTimestamp; 861 int flags; 862 int iconRes; 863 String bitmapPath; 864 865 id = parseStringAttribute(parser, ATTR_ID); 866 activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY); 867 title = parseStringAttribute(parser, ATTR_TITLE); 868 intent = parseIntentAttribute(parser, ATTR_INTENT); 869 weight = (int) parseLongAttribute(parser, ATTR_WEIGHT); 870 lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP); 871 flags = (int) parseLongAttribute(parser, ATTR_FLAGS); 872 iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES); 873 bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH); 874 875 final int outerDepth = parser.getDepth(); 876 int type; 877 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 878 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 879 if (type != XmlPullParser.START_TAG) { 880 continue; 881 } 882 final int depth = parser.getDepth(); 883 final String tag = parser.getName(); 884 if (DEBUG_LOAD) { 885 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 886 depth, type, tag)); 887 } 888 switch (tag) { 889 case TAG_INTENT_EXTRAS: 890 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 891 continue; 892 case TAG_EXTRAS: 893 extras = PersistableBundle.restoreFromXml(parser); 894 continue; 895 } 896 throw throwForInvalidTag(depth, tag); 897 } 898 return new ShortcutInfo( 899 id, packgeName, activityComponent, /* icon =*/ null, title, intent, 900 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 901 iconRes, bitmapPath); 902 } 903 904 // TODO Actually make it async. 905 private void scheduleSaveBaseState() { 906 synchronized (mLock) { 907 saveBaseStateLocked(); 908 } 909 } 910 911 // TODO Actually make it async. 912 private void scheduleSaveUser(@UserIdInt int userId) { 913 synchronized (mLock) { 914 saveUserLocked(userId); 915 } 916 } 917 918 /** Return the last reset time. */ 919 long getLastResetTimeLocked() { 920 updateTimes(); 921 return mRawLastResetTime; 922 } 923 924 /** Return the next reset time. */ 925 long getNextResetTimeLocked() { 926 updateTimes(); 927 return mRawLastResetTime + mResetInterval; 928 } 929 930 /** 931 * Update the last reset time. 932 */ 933 private void updateTimes() { 934 935 final long now = injectCurrentTimeMillis(); 936 937 final long prevLastResetTime = mRawLastResetTime; 938 939 if (mRawLastResetTime == 0) { // first launch. 940 // TODO Randomize?? 941 mRawLastResetTime = now; 942 } else if (now < mRawLastResetTime) { 943 // Clock rewound. 944 // TODO Randomize?? 945 mRawLastResetTime = now; 946 } else { 947 // TODO Do it properly. 948 while ((mRawLastResetTime + mResetInterval) <= now) { 949 mRawLastResetTime += mResetInterval; 950 } 951 } 952 if (prevLastResetTime != mRawLastResetTime) { 953 scheduleSaveBaseState(); 954 } 955 } 956 957 /** Return the per-user state. */ 958 @GuardedBy("mLock") 959 @NonNull 960 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) { 961 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId); 962 if (userPackages == null) { 963 userPackages = loadUserLocked(userId); 964 if (userPackages == null) { 965 userPackages = new ArrayMap<>(); 966 } 967 mShortcuts.put(userId, userPackages); 968 } 969 return userPackages; 970 } 971 972 /** Return the per-user per-package state. */ 973 @GuardedBy("mLock") 974 @NonNull 975 private PackageShortcuts getPackageShortcutsLocked( 976 @NonNull String packageName, @UserIdInt int userId) { 977 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId); 978 PackageShortcuts shortcuts = userPackages.get(packageName); 979 if (shortcuts == null) { 980 shortcuts = new PackageShortcuts(userId, packageName); 981 userPackages.put(packageName, shortcuts); 982 } 983 return shortcuts; 984 } 985 986 // === Caller validation === 987 988 void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) { 989 if (shortcut.getBitmapPath() != null) { 990 if (DEBUG) { 991 Slog.d(TAG, "Removing " + shortcut.getBitmapPath()); 992 } 993 new File(shortcut.getBitmapPath()).delete(); 994 995 shortcut.setBitmapPath(null); 996 shortcut.setIconResourceId(0); 997 shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES); 998 } 999 } 1000 1001 @VisibleForTesting 1002 static class FileOutputStreamWithPath extends FileOutputStream { 1003 private final File mFile; 1004 1005 public FileOutputStreamWithPath(File file) throws FileNotFoundException { 1006 super(file); 1007 mFile = file; 1008 } 1009 1010 public File getFile() { 1011 return mFile; 1012 } 1013 } 1014 1015 /** 1016 * Build the cached bitmap filename for a shortcut icon. 1017 * 1018 * The filename will be based on the ID, except certain characters will be escaped. 1019 */ 1020 @VisibleForTesting 1021 FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut) 1022 throws IOException { 1023 final File packagePath = new File(getUserBitmapFilePath(userId), 1024 shortcut.getPackageName()); 1025 if (!packagePath.isDirectory()) { 1026 packagePath.mkdirs(); 1027 if (!packagePath.isDirectory()) { 1028 throw new IOException("Unable to create directory " + packagePath); 1029 } 1030 SELinux.restorecon(packagePath); 1031 } 1032 1033 final String baseName = String.valueOf(injectCurrentTimeMillis()); 1034 for (int suffix = 0;; suffix++) { 1035 final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png"; 1036 final File file = new File(packagePath, filename); 1037 if (!file.exists()) { 1038 if (DEBUG) { 1039 Slog.d(TAG, "Saving icon to " + file.getAbsolutePath()); 1040 } 1041 return new FileOutputStreamWithPath(file); 1042 } 1043 } 1044 } 1045 1046 void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) { 1047 if (shortcut.hasIconFile() || shortcut.hasIconResource()) { 1048 return; 1049 } 1050 1051 final long token = Binder.clearCallingIdentity(); 1052 try { 1053 // Clear icon info on the shortcut. 1054 shortcut.setIconResourceId(0); 1055 shortcut.setBitmapPath(null); 1056 1057 final Icon icon = shortcut.getIcon(); 1058 if (icon == null) { 1059 return; // has no icon 1060 } 1061 1062 Bitmap bitmap = null; 1063 try { 1064 switch (icon.getType()) { 1065 case Icon.TYPE_RESOURCE: { 1066 injectValidateIconResPackage(shortcut, icon); 1067 1068 shortcut.setIconResourceId(icon.getResId()); 1069 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES); 1070 return; 1071 } 1072 case Icon.TYPE_BITMAP: { 1073 bitmap = icon.getBitmap(); 1074 break; 1075 } 1076 case Icon.TYPE_URI: { 1077 final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId); 1078 1079 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { 1080 1081 bitmap = BitmapFactory.decodeStream(is); 1082 1083 } catch (IOException e) { 1084 Slog.e(TAG, "Unable to load icon from " + uri); 1085 return; 1086 } 1087 break; 1088 } 1089 default: 1090 // This shouldn't happen because we've already validated the icon, but 1091 // just in case. 1092 throw ShortcutInfo.getInvalidIconException(); 1093 } 1094 if (bitmap == null) { 1095 Slog.e(TAG, "Null bitmap detected"); 1096 return; 1097 } 1098 // Shrink and write to the file. 1099 File path = null; 1100 try { 1101 final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut); 1102 try { 1103 path = out.getFile(); 1104 1105 shrinkBitmap(bitmap, mMaxIconDimension) 1106 .compress(mIconPersistFormat, mIconPersistQuality, out); 1107 1108 shortcut.setBitmapPath(out.getFile().getAbsolutePath()); 1109 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE); 1110 } finally { 1111 IoUtils.closeQuietly(out); 1112 } 1113 } catch (IOException|RuntimeException e) { 1114 // STOPSHIP Change wtf to e 1115 Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e); 1116 if (path != null && path.exists()) { 1117 path.delete(); 1118 } 1119 } 1120 } finally { 1121 if (bitmap != null) { 1122 bitmap.recycle(); 1123 } 1124 // Once saved, we won't use the original icon information, so null it out. 1125 shortcut.clearIcon(); 1126 } 1127 } finally { 1128 Binder.restoreCallingIdentity(token); 1129 } 1130 } 1131 1132 // Unfortunately we can't do this check in unit tests because we fake creator package names, 1133 // so override in unit tests. 1134 // TODO CTS this case. 1135 void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) { 1136 if (!shortcut.getPackageName().equals(icon.getResPackage())) { 1137 throw new IllegalArgumentException( 1138 "Icon resource must reside in shortcut owner package"); 1139 } 1140 } 1141 1142 @VisibleForTesting 1143 static Bitmap shrinkBitmap(Bitmap in, int maxSize) { 1144 // Original width/height. 1145 final int ow = in.getWidth(); 1146 final int oh = in.getHeight(); 1147 if ((ow <= maxSize) && (oh <= maxSize)) { 1148 if (DEBUG) { 1149 Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh)); 1150 } 1151 return in; 1152 } 1153 final int longerDimension = Math.max(ow, oh); 1154 1155 // New width and height. 1156 final int nw = ow * maxSize / longerDimension; 1157 final int nh = oh * maxSize / longerDimension; 1158 if (DEBUG) { 1159 Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d", 1160 ow, oh, nw, nh)); 1161 } 1162 1163 final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888); 1164 final Canvas c = new Canvas(scaledBitmap); 1165 1166 final RectF dst = new RectF(0, 0, nw, nh); 1167 1168 c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null); 1169 1170 in.recycle(); 1171 1172 return scaledBitmap; 1173 } 1174 1175 // === Caller validation === 1176 1177 private boolean isCallerSystem() { 1178 final int callingUid = injectBinderCallingUid(); 1179 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); 1180 } 1181 1182 private boolean isCallerShell() { 1183 final int callingUid = injectBinderCallingUid(); 1184 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; 1185 } 1186 1187 private void enforceSystemOrShell() { 1188 Preconditions.checkState(isCallerSystem() || isCallerShell(), 1189 "Caller must be system or shell"); 1190 } 1191 1192 private void enforceShell() { 1193 Preconditions.checkState(isCallerShell(), "Caller must be shell"); 1194 } 1195 1196 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { 1197 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1198 1199 if (isCallerSystem()) { 1200 return; // no check 1201 } 1202 1203 final int callingUid = injectBinderCallingUid(); 1204 1205 // Otherwise, make sure the arguments are valid. 1206 if (UserHandle.getUserId(callingUid) != userId) { 1207 throw new SecurityException("Invalid user-ID"); 1208 } 1209 if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) { 1210 return; // Caller is valid. 1211 } 1212 throw new SecurityException("Caller UID= doesn't own " + packageName); 1213 } 1214 1215 // Test overrides it. 1216 int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) { 1217 try { 1218 1219 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? 1220 1221 return mContext.getPackageManager().getPackageUidAsUser(packageName, 1222 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE 1223 | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 1224 } catch (NameNotFoundException e) { 1225 return -1; 1226 } 1227 } 1228 1229 /** 1230 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. 1231 */ 1232 void enforceMaxDynamicShortcuts(int numShortcuts) { 1233 if (numShortcuts > mMaxDynamicShortcuts) { 1234 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); 1235 } 1236 } 1237 1238 /** 1239 * - Sends a notification to LauncherApps 1240 * - Write to file 1241 */ 1242 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { 1243 notifyListeners(packageName, userId); 1244 scheduleSaveUser(userId); 1245 } 1246 1247 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { 1248 final ArrayList<ShortcutChangeListener> copy; 1249 final List<ShortcutInfo> shortcuts = new ArrayList<>(); 1250 synchronized (mLock) { 1251 copy = new ArrayList<>(mListeners); 1252 1253 getPackageShortcutsLocked(packageName, userId) 1254 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); 1255 } 1256 for (int i = copy.size() - 1; i >= 0; i--) { 1257 copy.get(i).onShortcutChanged(packageName, shortcuts, userId); 1258 } 1259 } 1260 1261 /** 1262 * Clean up / validate an incoming shortcut. 1263 * - Make sure all mandatory fields are set. 1264 * - Make sure the intent's extras are persistable, and them to set 1265 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. 1266 * - Clear flags. 1267 * 1268 * TODO Detailed unit tests 1269 */ 1270 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { 1271 Preconditions.checkNotNull(shortcut, "Null shortcut detected"); 1272 if (shortcut.getActivityComponent() != null) { 1273 Preconditions.checkState( 1274 shortcut.getPackageName().equals( 1275 shortcut.getActivityComponent().getPackageName()), 1276 "Activity package name mismatch"); 1277 } 1278 1279 if (!forUpdate) { 1280 shortcut.enforceMandatoryFields(); 1281 } 1282 if (shortcut.getIcon() != null) { 1283 ShortcutInfo.validateIcon(shortcut.getIcon()); 1284 } 1285 1286 validateForXml(shortcut.getId()); 1287 validateForXml(shortcut.getTitle()); 1288 validatePersistableBundleForXml(shortcut.getIntentPersistableExtras()); 1289 validatePersistableBundleForXml(shortcut.getExtras()); 1290 1291 shortcut.setFlags(0); 1292 } 1293 1294 // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those 1295 // characters. 1296 1297 private static void validatePersistableBundleForXml(PersistableBundle b) { 1298 if (b == null || b.size() == 0) { 1299 return; 1300 } 1301 for (String key : b.keySet()) { 1302 validateForXml(key); 1303 final Object value = b.get(key); 1304 if (value == null) { 1305 continue; 1306 } else if (value instanceof String) { 1307 validateForXml((String) value); 1308 } else if (value instanceof String[]) { 1309 for (String v : (String[]) value) { 1310 validateForXml(v); 1311 } 1312 } else if (value instanceof PersistableBundle) { 1313 validatePersistableBundleForXml((PersistableBundle) value); 1314 } 1315 } 1316 } 1317 1318 private static void validateForXml(String s) { 1319 if (TextUtils.isEmpty(s)) { 1320 return; 1321 } 1322 for (int i = s.length() - 1; i >= 0; i--) { 1323 if (!isAllowedInXml(s.charAt(i))) { 1324 throw new IllegalArgumentException("Unsupported character detected in: " + s); 1325 } 1326 } 1327 } 1328 1329 private static boolean isAllowedInXml(char c) { 1330 return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 1331 } 1332 1333 // === APIs === 1334 1335 @Override 1336 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1337 @UserIdInt int userId) { 1338 verifyCaller(packageName, userId); 1339 1340 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1341 final int size = newShortcuts.size(); 1342 1343 synchronized (mLock) { 1344 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1345 1346 // Throttling. 1347 if (!ps.tryApiCall(this)) { 1348 return false; 1349 } 1350 enforceMaxDynamicShortcuts(size); 1351 1352 // Validate the shortcuts. 1353 for (int i = 0; i < size; i++) { 1354 fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); 1355 } 1356 1357 // First, remove all un-pinned; dynamic shortcuts 1358 ps.deleteAllDynamicShortcuts(this); 1359 1360 // Then, add/update all. We need to make sure to take over "pinned" flag. 1361 for (int i = 0; i < size; i++) { 1362 final ShortcutInfo newShortcut = newShortcuts.get(i); 1363 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1364 ps.updateShortcutWithCapping(this, newShortcut); 1365 } 1366 } 1367 userPackageChanged(packageName, userId); 1368 return true; 1369 } 1370 1371 @Override 1372 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1373 @UserIdInt int userId) { 1374 verifyCaller(packageName, userId); 1375 1376 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1377 final int size = newShortcuts.size(); 1378 1379 synchronized (mLock) { 1380 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1381 1382 // Throttling. 1383 if (!ps.tryApiCall(this)) { 1384 return false; 1385 } 1386 1387 for (int i = 0; i < size; i++) { 1388 final ShortcutInfo source = newShortcuts.get(i); 1389 fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); 1390 1391 final ShortcutInfo target = ps.findShortcutById(source.getId()); 1392 if (target != null) { 1393 final boolean replacingIcon = (source.getIcon() != null); 1394 if (replacingIcon) { 1395 removeIcon(userId, target); 1396 } 1397 1398 target.copyNonNullFieldsFrom(source); 1399 1400 if (replacingIcon) { 1401 saveIconAndFixUpShortcut(userId, target); 1402 } 1403 } 1404 } 1405 } 1406 userPackageChanged(packageName, userId); 1407 1408 return true; 1409 } 1410 1411 @Override 1412 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, 1413 @UserIdInt int userId) { 1414 verifyCaller(packageName, userId); 1415 1416 synchronized (mLock) { 1417 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1418 1419 // Throttling. 1420 if (!ps.tryApiCall(this)) { 1421 return false; 1422 } 1423 1424 // Validate the shortcut. 1425 fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); 1426 1427 // Add it. 1428 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1429 ps.updateShortcutWithCapping(this, newShortcut); 1430 } 1431 userPackageChanged(packageName, userId); 1432 1433 return true; 1434 } 1435 1436 @Override 1437 public void deleteDynamicShortcut(String packageName, String shortcutId, 1438 @UserIdInt int userId) { 1439 verifyCaller(packageName, userId); 1440 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); 1441 1442 synchronized (mLock) { 1443 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId); 1444 } 1445 userPackageChanged(packageName, userId); 1446 } 1447 1448 @Override 1449 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { 1450 verifyCaller(packageName, userId); 1451 1452 synchronized (mLock) { 1453 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this); 1454 } 1455 userPackageChanged(packageName, userId); 1456 } 1457 1458 @Override 1459 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName, 1460 @UserIdInt int userId) { 1461 verifyCaller(packageName, userId); 1462 synchronized (mLock) { 1463 return getShortcutsWithQueryLocked( 1464 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1465 ShortcutInfo::isDynamic); 1466 } 1467 } 1468 1469 @Override 1470 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName, 1471 @UserIdInt int userId) { 1472 verifyCaller(packageName, userId); 1473 synchronized (mLock) { 1474 return getShortcutsWithQueryLocked( 1475 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1476 ShortcutInfo::isPinned); 1477 } 1478 } 1479 1480 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, 1481 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) { 1482 1483 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1484 1485 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); 1486 1487 return new ParceledListSlice<>(ret); 1488 } 1489 1490 @Override 1491 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) 1492 throws RemoteException { 1493 verifyCaller(packageName, userId); 1494 1495 return mMaxDynamicShortcuts; 1496 } 1497 1498 @Override 1499 public int getRemainingCallCount(String packageName, @UserIdInt int userId) { 1500 verifyCaller(packageName, userId); 1501 1502 synchronized (mLock) { 1503 return mMaxDailyUpdates 1504 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); 1505 } 1506 } 1507 1508 @Override 1509 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { 1510 verifyCaller(packageName, userId); 1511 1512 synchronized (mLock) { 1513 return getNextResetTimeLocked(); 1514 } 1515 } 1516 1517 @Override 1518 public int getIconMaxDimensions(String packageName, int userId) throws RemoteException { 1519 synchronized (mLock) { 1520 return mMaxIconDimension; 1521 } 1522 } 1523 1524 /** 1525 * Reset all throttling, for developer options and command line. Only system/shell can call it. 1526 */ 1527 @Override 1528 public void resetThrottling() { 1529 enforceSystemOrShell(); 1530 1531 resetThrottlingInner(); 1532 } 1533 1534 @VisibleForTesting 1535 void resetThrottlingInner() { 1536 synchronized (mLock) { 1537 mRawLastResetTime = injectCurrentTimeMillis(); 1538 } 1539 scheduleSaveBaseState(); 1540 Slog.i(TAG, "ShortcutManager: throttling counter reset"); 1541 } 1542 1543 /** 1544 * Entry point from {@link LauncherApps}. 1545 */ 1546 private class LocalService extends ShortcutServiceInternal { 1547 @Override 1548 public List<ShortcutInfo> getShortcuts( 1549 @NonNull String callingPackage, long changedSince, 1550 @Nullable String packageName, @Nullable ComponentName componentName, 1551 int queryFlags, int userId) { 1552 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1553 final int cloneFlag = 1554 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) 1555 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER 1556 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; 1557 1558 synchronized (mLock) { 1559 if (packageName != null) { 1560 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, 1561 userId, ret, cloneFlag); 1562 } else { 1563 final ArrayMap<String, PackageShortcuts> packages = 1564 getUserShortcutsLocked(userId); 1565 for (int i = packages.size() - 1; i >= 0; i--) { 1566 getShortcutsInnerLocked( 1567 packages.keyAt(i), 1568 changedSince, componentName, queryFlags, userId, ret, cloneFlag); 1569 } 1570 } 1571 } 1572 return ret; 1573 } 1574 1575 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, 1576 @Nullable ComponentName componentName, int queryFlags, 1577 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) { 1578 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1579 (ShortcutInfo si) -> { 1580 if (si.getLastChangedTimestamp() < changedSince) { 1581 return false; 1582 } 1583 if (componentName != null 1584 && !componentName.equals(si.getActivityComponent())) { 1585 return false; 1586 } 1587 final boolean matchDynamic = 1588 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) 1589 && si.isDynamic(); 1590 final boolean matchPinned = 1591 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) 1592 && si.isPinned(); 1593 return matchDynamic || matchPinned; 1594 }, cloneFlag); 1595 } 1596 1597 @Override 1598 public List<ShortcutInfo> getShortcutInfo( 1599 @NonNull String callingPackage, 1600 @NonNull String packageName, @Nullable List<String> ids, int userId) { 1601 // Calling permission must be checked by LauncherAppsImpl. 1602 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1603 1604 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size()); 1605 final ArraySet<String> idSet = new ArraySet<>(ids); 1606 synchronized (mLock) { 1607 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1608 (ShortcutInfo si) -> idSet.contains(si.getId()), 1609 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 1610 } 1611 return ret; 1612 } 1613 1614 @Override 1615 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, 1616 @NonNull List<String> shortcutIds, int userId) { 1617 // Calling permission must be checked by LauncherAppsImpl. 1618 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1619 Preconditions.checkNotNull(shortcutIds, "shortcutIds"); 1620 1621 synchronized (mLock) { 1622 getPackageShortcutsLocked(packageName, userId).replacePinned( 1623 ShortcutService.this, callingPackage, shortcutIds); 1624 } 1625 userPackageChanged(packageName, userId); 1626 } 1627 1628 @Override 1629 public Intent createShortcutIntent(@NonNull String callingPackage, 1630 @NonNull String packageName, @NonNull String shortcutId, int userId) { 1631 // Calling permission must be checked by LauncherAppsImpl. 1632 Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty"); 1633 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); 1634 1635 synchronized (mLock) { 1636 final ShortcutInfo fullShortcut = 1637 getPackageShortcutsLocked(packageName, userId) 1638 .findShortcutById(shortcutId); 1639 return fullShortcut == null ? null : fullShortcut.getIntent(); 1640 } 1641 } 1642 1643 @Override 1644 public void addListener(@NonNull ShortcutChangeListener listener) { 1645 synchronized (mLock) { 1646 mListeners.add(Preconditions.checkNotNull(listener)); 1647 } 1648 } 1649 1650 @Override 1651 public int getShortcutIconResId(@NonNull String callingPackage, 1652 @NonNull ShortcutInfo shortcut, int userId) { 1653 Preconditions.checkNotNull(shortcut, "shortcut"); 1654 1655 synchronized (mLock) { 1656 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1657 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1658 return (shortcutInfo != null && shortcutInfo.hasIconResource()) 1659 ? shortcutInfo.getIconResourceId() : 0; 1660 } 1661 } 1662 1663 @Override 1664 public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage, 1665 @NonNull ShortcutInfo shortcut, int userId) { 1666 Preconditions.checkNotNull(shortcut, "shortcut"); 1667 1668 synchronized (mLock) { 1669 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1670 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1671 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) { 1672 return null; 1673 } 1674 try { 1675 return ParcelFileDescriptor.open( 1676 new File(shortcutInfo.getBitmapPath()), 1677 ParcelFileDescriptor.MODE_READ_ONLY); 1678 } catch (FileNotFoundException e) { 1679 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath()); 1680 return null; 1681 } 1682 } 1683 } 1684 } 1685 1686 // === Dump === 1687 1688 @Override 1689 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1690 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) 1691 != PackageManager.PERMISSION_GRANTED) { 1692 pw.println("Permission Denial: can't dump UserManager from from pid=" 1693 + Binder.getCallingPid() 1694 + ", uid=" + Binder.getCallingUid() 1695 + " without permission " 1696 + android.Manifest.permission.DUMP); 1697 return; 1698 } 1699 dumpInner(pw); 1700 } 1701 1702 @VisibleForTesting 1703 void dumpInner(PrintWriter pw) { 1704 synchronized (mLock) { 1705 final long now = injectCurrentTimeMillis(); 1706 pw.print("Now: ["); 1707 pw.print(now); 1708 pw.print("] "); 1709 pw.print(formatTime(now)); 1710 1711 pw.print(" Raw last reset: ["); 1712 pw.print(mRawLastResetTime); 1713 pw.print("] "); 1714 pw.print(formatTime(mRawLastResetTime)); 1715 1716 final long last = getLastResetTimeLocked(); 1717 pw.print(" Last reset: ["); 1718 pw.print(last); 1719 pw.print("] "); 1720 pw.print(formatTime(last)); 1721 1722 final long next = getNextResetTimeLocked(); 1723 pw.print(" Next reset: ["); 1724 pw.print(next); 1725 pw.print("] "); 1726 pw.print(formatTime(next)); 1727 pw.println(); 1728 1729 pw.print(" Max icon dim: "); 1730 pw.print(mMaxIconDimension); 1731 pw.print(" Icon format: "); 1732 pw.print(mIconPersistFormat); 1733 pw.print(" Icon quality: "); 1734 pw.print(mIconPersistQuality); 1735 pw.println(); 1736 1737 pw.println(); 1738 1739 for (int i = 0; i < mShortcuts.size(); i++) { 1740 dumpUserLocked(pw, mShortcuts.keyAt(i)); 1741 } 1742 } 1743 } 1744 1745 private void dumpUserLocked(PrintWriter pw, int userId) { 1746 pw.print(" User: "); 1747 pw.print(userId); 1748 pw.println(); 1749 1750 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); 1751 if (packages == null) { 1752 return; 1753 } 1754 for (int j = 0; j < packages.size(); j++) { 1755 dumpPackageLocked(pw, userId, packages.keyAt(j)); 1756 } 1757 pw.println(); 1758 } 1759 1760 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { 1761 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName); 1762 if (packageShortcuts == null) { 1763 return; 1764 } 1765 1766 pw.print(" Package: "); 1767 pw.print(packageName); 1768 pw.println(); 1769 1770 pw.print(" Calls: "); 1771 pw.print(packageShortcuts.getApiCallCount(this)); 1772 pw.println(); 1773 1774 // This should be after getApiCallCount(), which may update it. 1775 pw.print(" Last reset: ["); 1776 pw.print(packageShortcuts.mLastResetTime); 1777 pw.print("] "); 1778 pw.print(formatTime(packageShortcuts.mLastResetTime)); 1779 pw.println(); 1780 1781 pw.println(" Shortcuts:"); 1782 long totalBitmapSize = 0; 1783 final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts; 1784 final int size = shortcuts.size(); 1785 for (int i = 0; i < size; i++) { 1786 final ShortcutInfo si = shortcuts.valueAt(i); 1787 pw.print(" "); 1788 pw.println(si.toInsecureString()); 1789 if (si.hasIconFile()) { 1790 final long len = new File(si.getBitmapPath()).length(); 1791 pw.print(" "); 1792 pw.print("bitmap size="); 1793 pw.println(len); 1794 1795 totalBitmapSize += len; 1796 } 1797 } 1798 pw.print(" Total bitmap size: "); 1799 pw.print(totalBitmapSize); 1800 pw.print(" ("); 1801 pw.print(Formatter.formatFileSize(mContext, totalBitmapSize)); 1802 pw.println(")"); 1803 } 1804 1805 private static String formatTime(long time) { 1806 Time tobj = new Time(); 1807 tobj.set(time); 1808 return tobj.format("%Y-%m-%d %H:%M:%S"); 1809 } 1810 1811 // === Shell support === 1812 1813 @Override 1814 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 1815 String[] args, ResultReceiver resultReceiver) throws RemoteException { 1816 1817 enforceShell(); 1818 1819 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); 1820 } 1821 1822 /** 1823 * Handle "adb shell cmd". 1824 */ 1825 private class MyShellCommand extends ShellCommand { 1826 @Override 1827 public int onCommand(String cmd) { 1828 if (cmd == null) { 1829 return handleDefaultCommands(cmd); 1830 } 1831 final PrintWriter pw = getOutPrintWriter(); 1832 switch(cmd) { 1833 case "reset-package-throttling": 1834 return handleResetPackageThrottling(); 1835 case "reset-throttling": 1836 return handleResetThrottling(); 1837 default: 1838 return handleDefaultCommands(cmd); 1839 } 1840 } 1841 1842 @Override 1843 public void onHelp() { 1844 final PrintWriter pw = getOutPrintWriter(); 1845 pw.println("Usage: cmd shortcut COMMAND [options ...]"); 1846 pw.println(); 1847 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); 1848 pw.println(" Reset throttling for a package"); 1849 pw.println(); 1850 pw.println("cmd shortcut reset-throttling"); 1851 pw.println(" Reset throttling for all packages and users"); 1852 pw.println(); 1853 } 1854 1855 private int handleResetThrottling() { 1856 resetThrottling(); 1857 return 0; 1858 } 1859 1860 private int handleResetPackageThrottling() { 1861 final PrintWriter pw = getOutPrintWriter(); 1862 1863 int userId = UserHandle.USER_SYSTEM; 1864 String opt; 1865 while ((opt = getNextOption()) != null) { 1866 switch (opt) { 1867 case "--user": 1868 userId = UserHandle.parseUserArg(getNextArgRequired()); 1869 break; 1870 default: 1871 pw.println("Error: Unknown option: " + opt); 1872 return 1; 1873 } 1874 } 1875 final String packageName = getNextArgRequired(); 1876 1877 synchronized (mLock) { 1878 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); 1879 saveUserLocked(userId); 1880 } 1881 1882 return 0; 1883 } 1884 } 1885 1886 // === Unit test support === 1887 1888 // Injection point. 1889 long injectCurrentTimeMillis() { 1890 return System.currentTimeMillis(); 1891 } 1892 1893 // Injection point. 1894 int injectBinderCallingUid() { 1895 return getCallingUid(); 1896 } 1897 1898 File injectSystemDataPath() { 1899 return Environment.getDataSystemDirectory(); 1900 } 1901 1902 File injectUserDataPath(@UserIdInt int userId) { 1903 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER); 1904 } 1905 1906 boolean injectIsLowRamDevice() { 1907 return ActivityManager.isLowRamDeviceStatic(); 1908 } 1909 1910 File getUserBitmapFilePath(@UserIdInt int userId) { 1911 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS); 1912 } 1913 1914 @VisibleForTesting 1915 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { 1916 return mShortcuts; 1917 } 1918 1919 @VisibleForTesting 1920 void setMaxDynamicShortcutsForTest(int max) { 1921 mMaxDynamicShortcuts = max; 1922 } 1923 1924 @VisibleForTesting 1925 void setMaxDailyUpdatesForTest(int max) { 1926 mMaxDailyUpdates = max; 1927 } 1928 1929 @VisibleForTesting 1930 void setMaxIconDimensionForTest(int dimension) { 1931 mMaxIconDimension = dimension; 1932 } 1933 1934 @VisibleForTesting 1935 public void setResetIntervalForTest(long interval) { 1936 mResetInterval = interval; 1937 } 1938} 1939