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