ShortcutService.java revision f3a572b5c0cab23a435bd90414d25de84e00398e
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 onUnlockUser(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 final AtomicFile file = new AtomicFile(path); 872 873 final FileInputStream in; 874 try { 875 in = file.openRead(); 876 } catch (FileNotFoundException e) { 877 if (DEBUG) { 878 Slog.i(TAG, "Not found " + path); 879 } 880 return null; 881 } 882 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>(); 883 try { 884 XmlPullParser parser = Xml.newPullParser(); 885 parser.setInput(in, StandardCharsets.UTF_8.name()); 886 887 String packageName = null; 888 PackageShortcuts shortcuts = null; 889 890 int type; 891 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 892 if (type != XmlPullParser.START_TAG) { 893 continue; 894 } 895 final int depth = parser.getDepth(); 896 897 // TODO Move some of this to PackageShortcuts. 898 899 final String tag = parser.getName(); 900 if (DEBUG_LOAD) { 901 Slog.d(TAG, String.format("depth=%d type=%d name=%s", 902 depth, type, tag)); 903 } 904 switch (depth) { 905 case 1: { 906 if (TAG_ROOT.equals(tag)) { 907 continue; 908 } 909 break; 910 } 911 case 2: { 912 switch (tag) { 913 case TAG_PACKAGE: 914 packageName = parseStringAttribute(parser, ATTR_NAME); 915 shortcuts = new PackageShortcuts(userId, packageName); 916 ret.put(packageName, shortcuts); 917 918 shortcuts.mDynamicShortcutCount = 919 (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT); 920 shortcuts.mApiCallCount = 921 (int) parseLongAttribute(parser, ATTR_CALL_COUNT); 922 shortcuts.mLastResetTime = parseLongAttribute(parser, 923 ATTR_LAST_RESET); 924 continue; 925 } 926 break; 927 } 928 case 3: { 929 switch (tag) { 930 case TAG_SHORTCUT: 931 final ShortcutInfo si = parseShortcut(parser, packageName); 932 933 // Don't use addShortcut(), we don't need to save the icon. 934 shortcuts.mShortcuts.put(si.getId(), si); 935 continue; 936 } 937 break; 938 } 939 } 940 throwForInvalidTag(depth, tag); 941 } 942 return ret; 943 } catch (IOException|XmlPullParserException e) { 944 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 945 return null; 946 } finally { 947 IoUtils.closeQuietly(in); 948 } 949 } 950 951 private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName) 952 throws IOException, XmlPullParserException { 953 String id; 954 ComponentName activityComponent; 955 Icon icon; 956 String title; 957 Intent intent; 958 PersistableBundle intentPersistableExtras = null; 959 int weight; 960 PersistableBundle extras = null; 961 long lastChangedTimestamp; 962 int flags; 963 int iconRes; 964 String bitmapPath; 965 966 id = parseStringAttribute(parser, ATTR_ID); 967 activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY); 968 title = parseStringAttribute(parser, ATTR_TITLE); 969 intent = parseIntentAttribute(parser, ATTR_INTENT); 970 weight = (int) parseLongAttribute(parser, ATTR_WEIGHT); 971 lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP); 972 flags = (int) parseLongAttribute(parser, ATTR_FLAGS); 973 iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES); 974 bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH); 975 976 final int outerDepth = parser.getDepth(); 977 int type; 978 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 979 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 980 if (type != XmlPullParser.START_TAG) { 981 continue; 982 } 983 final int depth = parser.getDepth(); 984 final String tag = parser.getName(); 985 if (DEBUG_LOAD) { 986 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 987 depth, type, tag)); 988 } 989 switch (tag) { 990 case TAG_INTENT_EXTRAS: 991 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 992 continue; 993 case TAG_EXTRAS: 994 extras = PersistableBundle.restoreFromXml(parser); 995 continue; 996 } 997 throw throwForInvalidTag(depth, tag); 998 } 999 return new ShortcutInfo( 1000 id, packgeName, activityComponent, /* icon =*/ null, title, intent, 1001 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 1002 iconRes, bitmapPath); 1003 } 1004 1005 // TODO Actually make it async. 1006 private void scheduleSaveBaseState() { 1007 synchronized (mLock) { 1008 saveBaseStateLocked(); 1009 } 1010 } 1011 1012 // TODO Actually make it async. 1013 private void scheduleSaveUser(@UserIdInt int userId) { 1014 synchronized (mLock) { 1015 saveUserLocked(userId); 1016 } 1017 } 1018 1019 /** Return the last reset time. */ 1020 long getLastResetTimeLocked() { 1021 updateTimes(); 1022 return mRawLastResetTime; 1023 } 1024 1025 /** Return the next reset time. */ 1026 long getNextResetTimeLocked() { 1027 updateTimes(); 1028 return mRawLastResetTime + mResetInterval; 1029 } 1030 1031 /** 1032 * Update the last reset time. 1033 */ 1034 private void updateTimes() { 1035 1036 final long now = injectCurrentTimeMillis(); 1037 1038 final long prevLastResetTime = mRawLastResetTime; 1039 1040 if (mRawLastResetTime == 0) { // first launch. 1041 // TODO Randomize?? 1042 mRawLastResetTime = now; 1043 } else if (now < mRawLastResetTime) { 1044 // Clock rewound. 1045 // TODO Randomize?? 1046 mRawLastResetTime = now; 1047 } else { 1048 // TODO Do it properly. 1049 while ((mRawLastResetTime + mResetInterval) <= now) { 1050 mRawLastResetTime += mResetInterval; 1051 } 1052 } 1053 if (prevLastResetTime != mRawLastResetTime) { 1054 scheduleSaveBaseState(); 1055 } 1056 } 1057 1058 /** Return the per-user state. */ 1059 @GuardedBy("mLock") 1060 @NonNull 1061 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) { 1062 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId); 1063 if (userPackages == null) { 1064 userPackages = loadUserLocked(userId); 1065 if (userPackages == null) { 1066 userPackages = new ArrayMap<>(); 1067 } 1068 mShortcuts.put(userId, userPackages); 1069 } 1070 return userPackages; 1071 } 1072 1073 /** Return the per-user per-package state. */ 1074 @GuardedBy("mLock") 1075 @NonNull 1076 private PackageShortcuts getPackageShortcutsLocked( 1077 @NonNull String packageName, @UserIdInt int userId) { 1078 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId); 1079 PackageShortcuts shortcuts = userPackages.get(packageName); 1080 if (shortcuts == null) { 1081 shortcuts = new PackageShortcuts(userId, packageName); 1082 userPackages.put(packageName, shortcuts); 1083 } 1084 return shortcuts; 1085 } 1086 1087 // === Caller validation === 1088 1089 void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) { 1090 if (shortcut.getBitmapPath() != null) { 1091 if (DEBUG) { 1092 Slog.d(TAG, "Removing " + shortcut.getBitmapPath()); 1093 } 1094 new File(shortcut.getBitmapPath()).delete(); 1095 1096 shortcut.setBitmapPath(null); 1097 shortcut.setIconResourceId(0); 1098 shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES); 1099 } 1100 } 1101 1102 @VisibleForTesting 1103 static class FileOutputStreamWithPath extends FileOutputStream { 1104 private final File mFile; 1105 1106 public FileOutputStreamWithPath(File file) throws FileNotFoundException { 1107 super(file); 1108 mFile = file; 1109 } 1110 1111 public File getFile() { 1112 return mFile; 1113 } 1114 } 1115 1116 /** 1117 * Build the cached bitmap filename for a shortcut icon. 1118 * 1119 * The filename will be based on the ID, except certain characters will be escaped. 1120 */ 1121 @VisibleForTesting 1122 FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut) 1123 throws IOException { 1124 final File packagePath = new File(getUserBitmapFilePath(userId), 1125 shortcut.getPackageName()); 1126 if (!packagePath.isDirectory()) { 1127 packagePath.mkdirs(); 1128 if (!packagePath.isDirectory()) { 1129 throw new IOException("Unable to create directory " + packagePath); 1130 } 1131 SELinux.restorecon(packagePath); 1132 } 1133 1134 final String baseName = String.valueOf(injectCurrentTimeMillis()); 1135 for (int suffix = 0;; suffix++) { 1136 final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png"; 1137 final File file = new File(packagePath, filename); 1138 if (!file.exists()) { 1139 if (DEBUG) { 1140 Slog.d(TAG, "Saving icon to " + file.getAbsolutePath()); 1141 } 1142 return new FileOutputStreamWithPath(file); 1143 } 1144 } 1145 } 1146 1147 void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) { 1148 if (shortcut.hasIconFile() || shortcut.hasIconResource()) { 1149 return; 1150 } 1151 1152 final long token = Binder.clearCallingIdentity(); 1153 try { 1154 // Clear icon info on the shortcut. 1155 shortcut.setIconResourceId(0); 1156 shortcut.setBitmapPath(null); 1157 1158 final Icon icon = shortcut.getIcon(); 1159 if (icon == null) { 1160 return; // has no icon 1161 } 1162 1163 Bitmap bitmap = null; 1164 try { 1165 switch (icon.getType()) { 1166 case Icon.TYPE_RESOURCE: { 1167 injectValidateIconResPackage(shortcut, icon); 1168 1169 shortcut.setIconResourceId(icon.getResId()); 1170 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES); 1171 return; 1172 } 1173 case Icon.TYPE_BITMAP: { 1174 bitmap = icon.getBitmap(); 1175 break; 1176 } 1177 case Icon.TYPE_URI: { 1178 final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId); 1179 1180 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { 1181 1182 bitmap = BitmapFactory.decodeStream(is); 1183 1184 } catch (IOException e) { 1185 Slog.e(TAG, "Unable to load icon from " + uri); 1186 return; 1187 } 1188 break; 1189 } 1190 default: 1191 // This shouldn't happen because we've already validated the icon, but 1192 // just in case. 1193 throw ShortcutInfo.getInvalidIconException(); 1194 } 1195 if (bitmap == null) { 1196 Slog.e(TAG, "Null bitmap detected"); 1197 return; 1198 } 1199 // Shrink and write to the file. 1200 File path = null; 1201 try { 1202 final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut); 1203 try { 1204 path = out.getFile(); 1205 1206 shrinkBitmap(bitmap, mMaxIconDimension) 1207 .compress(mIconPersistFormat, mIconPersistQuality, out); 1208 1209 shortcut.setBitmapPath(out.getFile().getAbsolutePath()); 1210 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE); 1211 } finally { 1212 IoUtils.closeQuietly(out); 1213 } 1214 } catch (IOException|RuntimeException e) { 1215 // STOPSHIP Change wtf to e 1216 Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e); 1217 if (path != null && path.exists()) { 1218 path.delete(); 1219 } 1220 } 1221 } finally { 1222 if (bitmap != null) { 1223 bitmap.recycle(); 1224 } 1225 // Once saved, we won't use the original icon information, so null it out. 1226 shortcut.clearIcon(); 1227 } 1228 } finally { 1229 Binder.restoreCallingIdentity(token); 1230 } 1231 } 1232 1233 // Unfortunately we can't do this check in unit tests because we fake creator package names, 1234 // so override in unit tests. 1235 // TODO CTS this case. 1236 void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) { 1237 if (!shortcut.getPackageName().equals(icon.getResPackage())) { 1238 throw new IllegalArgumentException( 1239 "Icon resource must reside in shortcut owner package"); 1240 } 1241 } 1242 1243 @VisibleForTesting 1244 static Bitmap shrinkBitmap(Bitmap in, int maxSize) { 1245 // Original width/height. 1246 final int ow = in.getWidth(); 1247 final int oh = in.getHeight(); 1248 if ((ow <= maxSize) && (oh <= maxSize)) { 1249 if (DEBUG) { 1250 Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh)); 1251 } 1252 return in; 1253 } 1254 final int longerDimension = Math.max(ow, oh); 1255 1256 // New width and height. 1257 final int nw = ow * maxSize / longerDimension; 1258 final int nh = oh * maxSize / longerDimension; 1259 if (DEBUG) { 1260 Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d", 1261 ow, oh, nw, nh)); 1262 } 1263 1264 final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888); 1265 final Canvas c = new Canvas(scaledBitmap); 1266 1267 final RectF dst = new RectF(0, 0, nw, nh); 1268 1269 c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null); 1270 1271 in.recycle(); 1272 1273 return scaledBitmap; 1274 } 1275 1276 // === Caller validation === 1277 1278 private boolean isCallerSystem() { 1279 final int callingUid = injectBinderCallingUid(); 1280 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); 1281 } 1282 1283 private boolean isCallerShell() { 1284 final int callingUid = injectBinderCallingUid(); 1285 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; 1286 } 1287 1288 private void enforceSystemOrShell() { 1289 Preconditions.checkState(isCallerSystem() || isCallerShell(), 1290 "Caller must be system or shell"); 1291 } 1292 1293 private void enforceShell() { 1294 Preconditions.checkState(isCallerShell(), "Caller must be shell"); 1295 } 1296 1297 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { 1298 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1299 1300 if (isCallerSystem()) { 1301 return; // no check 1302 } 1303 1304 final int callingUid = injectBinderCallingUid(); 1305 1306 // Otherwise, make sure the arguments are valid. 1307 if (UserHandle.getUserId(callingUid) != userId) { 1308 throw new SecurityException("Invalid user-ID"); 1309 } 1310 if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) { 1311 return; // Caller is valid. 1312 } 1313 throw new SecurityException("Caller UID= doesn't own " + packageName); 1314 } 1315 1316 // Test overrides it. 1317 int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) { 1318 try { 1319 1320 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? 1321 1322 return mContext.getPackageManager().getPackageUidAsUser(packageName, 1323 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE 1324 | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 1325 } catch (NameNotFoundException e) { 1326 return -1; 1327 } 1328 } 1329 1330 /** 1331 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. 1332 */ 1333 void enforceMaxDynamicShortcuts(int numShortcuts) { 1334 if (numShortcuts > mMaxDynamicShortcuts) { 1335 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); 1336 } 1337 } 1338 1339 /** 1340 * - Sends a notification to LauncherApps 1341 * - Write to file 1342 */ 1343 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { 1344 notifyListeners(packageName, userId); 1345 scheduleSaveUser(userId); 1346 } 1347 1348 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { 1349 final ArrayList<ShortcutChangeListener> copy; 1350 final List<ShortcutInfo> shortcuts = new ArrayList<>(); 1351 synchronized (mLock) { 1352 copy = new ArrayList<>(mListeners); 1353 1354 getPackageShortcutsLocked(packageName, userId) 1355 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); 1356 } 1357 for (int i = copy.size() - 1; i >= 0; i--) { 1358 copy.get(i).onShortcutChanged(packageName, shortcuts, userId); 1359 } 1360 } 1361 1362 /** 1363 * Clean up / validate an incoming shortcut. 1364 * - Make sure all mandatory fields are set. 1365 * - Make sure the intent's extras are persistable, and them to set 1366 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. 1367 * - Clear flags. 1368 * 1369 * TODO Detailed unit tests 1370 */ 1371 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { 1372 Preconditions.checkNotNull(shortcut, "Null shortcut detected"); 1373 if (shortcut.getActivityComponent() != null) { 1374 Preconditions.checkState( 1375 shortcut.getPackageName().equals( 1376 shortcut.getActivityComponent().getPackageName()), 1377 "Activity package name mismatch"); 1378 } 1379 1380 if (!forUpdate) { 1381 shortcut.enforceMandatoryFields(); 1382 } 1383 if (shortcut.getIcon() != null) { 1384 ShortcutInfo.validateIcon(shortcut.getIcon()); 1385 } 1386 1387 validateForXml(shortcut.getId()); 1388 validateForXml(shortcut.getTitle()); 1389 validatePersistableBundleForXml(shortcut.getIntentPersistableExtras()); 1390 validatePersistableBundleForXml(shortcut.getExtras()); 1391 1392 shortcut.setFlags(0); 1393 } 1394 1395 // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those 1396 // characters. 1397 1398 private static void validatePersistableBundleForXml(PersistableBundle b) { 1399 if (b == null || b.size() == 0) { 1400 return; 1401 } 1402 for (String key : b.keySet()) { 1403 validateForXml(key); 1404 final Object value = b.get(key); 1405 if (value == null) { 1406 continue; 1407 } else if (value instanceof String) { 1408 validateForXml((String) value); 1409 } else if (value instanceof String[]) { 1410 for (String v : (String[]) value) { 1411 validateForXml(v); 1412 } 1413 } else if (value instanceof PersistableBundle) { 1414 validatePersistableBundleForXml((PersistableBundle) value); 1415 } 1416 } 1417 } 1418 1419 private static void validateForXml(String s) { 1420 if (TextUtils.isEmpty(s)) { 1421 return; 1422 } 1423 for (int i = s.length() - 1; i >= 0; i--) { 1424 if (!isAllowedInXml(s.charAt(i))) { 1425 throw new IllegalArgumentException("Unsupported character detected in: " + s); 1426 } 1427 } 1428 } 1429 1430 private static boolean isAllowedInXml(char c) { 1431 return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 1432 } 1433 1434 // === APIs === 1435 1436 @Override 1437 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1438 @UserIdInt int userId) { 1439 verifyCaller(packageName, userId); 1440 1441 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1442 final int size = newShortcuts.size(); 1443 1444 synchronized (mLock) { 1445 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1446 1447 // Throttling. 1448 if (!ps.tryApiCall(this)) { 1449 return false; 1450 } 1451 enforceMaxDynamicShortcuts(size); 1452 1453 // Validate the shortcuts. 1454 for (int i = 0; i < size; i++) { 1455 fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); 1456 } 1457 1458 // First, remove all un-pinned; dynamic shortcuts 1459 ps.deleteAllDynamicShortcuts(this); 1460 1461 // Then, add/update all. We need to make sure to take over "pinned" flag. 1462 for (int i = 0; i < size; i++) { 1463 final ShortcutInfo newShortcut = newShortcuts.get(i); 1464 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1465 ps.updateShortcutWithCapping(this, newShortcut); 1466 } 1467 } 1468 userPackageChanged(packageName, userId); 1469 return true; 1470 } 1471 1472 @Override 1473 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1474 @UserIdInt int userId) { 1475 verifyCaller(packageName, userId); 1476 1477 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1478 final int size = newShortcuts.size(); 1479 1480 synchronized (mLock) { 1481 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1482 1483 // Throttling. 1484 if (!ps.tryApiCall(this)) { 1485 return false; 1486 } 1487 1488 for (int i = 0; i < size; i++) { 1489 final ShortcutInfo source = newShortcuts.get(i); 1490 fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); 1491 1492 final ShortcutInfo target = ps.findShortcutById(source.getId()); 1493 if (target != null) { 1494 final boolean replacingIcon = (source.getIcon() != null); 1495 if (replacingIcon) { 1496 removeIcon(userId, target); 1497 } 1498 1499 target.copyNonNullFieldsFrom(source); 1500 1501 if (replacingIcon) { 1502 saveIconAndFixUpShortcut(userId, target); 1503 } 1504 } 1505 } 1506 } 1507 userPackageChanged(packageName, userId); 1508 1509 return true; 1510 } 1511 1512 @Override 1513 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, 1514 @UserIdInt int userId) { 1515 verifyCaller(packageName, userId); 1516 1517 synchronized (mLock) { 1518 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1519 1520 // Throttling. 1521 if (!ps.tryApiCall(this)) { 1522 return false; 1523 } 1524 1525 // Validate the shortcut. 1526 fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); 1527 1528 // Add it. 1529 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1530 ps.updateShortcutWithCapping(this, newShortcut); 1531 } 1532 userPackageChanged(packageName, userId); 1533 1534 return true; 1535 } 1536 1537 @Override 1538 public void deleteDynamicShortcut(String packageName, String shortcutId, 1539 @UserIdInt int userId) { 1540 verifyCaller(packageName, userId); 1541 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); 1542 1543 synchronized (mLock) { 1544 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId); 1545 } 1546 userPackageChanged(packageName, userId); 1547 } 1548 1549 @Override 1550 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { 1551 verifyCaller(packageName, userId); 1552 1553 synchronized (mLock) { 1554 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this); 1555 } 1556 userPackageChanged(packageName, userId); 1557 } 1558 1559 @Override 1560 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName, 1561 @UserIdInt int userId) { 1562 verifyCaller(packageName, userId); 1563 synchronized (mLock) { 1564 return getShortcutsWithQueryLocked( 1565 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1566 ShortcutInfo::isDynamic); 1567 } 1568 } 1569 1570 @Override 1571 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName, 1572 @UserIdInt int userId) { 1573 verifyCaller(packageName, userId); 1574 synchronized (mLock) { 1575 return getShortcutsWithQueryLocked( 1576 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1577 ShortcutInfo::isPinned); 1578 } 1579 } 1580 1581 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, 1582 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) { 1583 1584 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1585 1586 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); 1587 1588 return new ParceledListSlice<>(ret); 1589 } 1590 1591 @Override 1592 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) 1593 throws RemoteException { 1594 verifyCaller(packageName, userId); 1595 1596 return mMaxDynamicShortcuts; 1597 } 1598 1599 @Override 1600 public int getRemainingCallCount(String packageName, @UserIdInt int userId) { 1601 verifyCaller(packageName, userId); 1602 1603 synchronized (mLock) { 1604 return mMaxDailyUpdates 1605 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); 1606 } 1607 } 1608 1609 @Override 1610 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { 1611 verifyCaller(packageName, userId); 1612 1613 synchronized (mLock) { 1614 return getNextResetTimeLocked(); 1615 } 1616 } 1617 1618 @Override 1619 public int getIconMaxDimensions(String packageName, int userId) throws RemoteException { 1620 synchronized (mLock) { 1621 return mMaxIconDimension; 1622 } 1623 } 1624 1625 /** 1626 * Reset all throttling, for developer options and command line. Only system/shell can call it. 1627 */ 1628 @Override 1629 public void resetThrottling() { 1630 enforceSystemOrShell(); 1631 1632 resetThrottlingInner(); 1633 } 1634 1635 @VisibleForTesting 1636 void resetThrottlingInner() { 1637 synchronized (mLock) { 1638 mRawLastResetTime = injectCurrentTimeMillis(); 1639 } 1640 scheduleSaveBaseState(); 1641 Slog.i(TAG, "ShortcutManager: throttling counter reset"); 1642 } 1643 1644 /** 1645 * Entry point from {@link LauncherApps}. 1646 */ 1647 private class LocalService extends ShortcutServiceInternal { 1648 @Override 1649 public List<ShortcutInfo> getShortcuts( 1650 @NonNull String callingPackage, long changedSince, 1651 @Nullable String packageName, @Nullable ComponentName componentName, 1652 int queryFlags, int userId) { 1653 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1654 final int cloneFlag = 1655 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) 1656 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER 1657 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; 1658 1659 synchronized (mLock) { 1660 if (packageName != null) { 1661 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, 1662 userId, ret, cloneFlag); 1663 } else { 1664 final ArrayMap<String, PackageShortcuts> packages = 1665 getUserShortcutsLocked(userId); 1666 for (int i = packages.size() - 1; i >= 0; i--) { 1667 getShortcutsInnerLocked( 1668 packages.keyAt(i), 1669 changedSince, componentName, queryFlags, userId, ret, cloneFlag); 1670 } 1671 } 1672 } 1673 return ret; 1674 } 1675 1676 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, 1677 @Nullable ComponentName componentName, int queryFlags, 1678 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) { 1679 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1680 (ShortcutInfo si) -> { 1681 if (si.getLastChangedTimestamp() < changedSince) { 1682 return false; 1683 } 1684 if (componentName != null 1685 && !componentName.equals(si.getActivityComponent())) { 1686 return false; 1687 } 1688 final boolean matchDynamic = 1689 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) 1690 && si.isDynamic(); 1691 final boolean matchPinned = 1692 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) 1693 && si.isPinned(); 1694 return matchDynamic || matchPinned; 1695 }, cloneFlag); 1696 } 1697 1698 @Override 1699 public List<ShortcutInfo> getShortcutInfo( 1700 @NonNull String callingPackage, 1701 @NonNull String packageName, @Nullable List<String> ids, int userId) { 1702 // Calling permission must be checked by LauncherAppsImpl. 1703 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1704 1705 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size()); 1706 final ArraySet<String> idSet = new ArraySet<>(ids); 1707 synchronized (mLock) { 1708 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1709 (ShortcutInfo si) -> idSet.contains(si.getId()), 1710 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 1711 } 1712 return ret; 1713 } 1714 1715 @Override 1716 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, 1717 @NonNull List<String> shortcutIds, int userId) { 1718 // Calling permission must be checked by LauncherAppsImpl. 1719 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1720 Preconditions.checkNotNull(shortcutIds, "shortcutIds"); 1721 1722 synchronized (mLock) { 1723 getPackageShortcutsLocked(packageName, userId).replacePinned( 1724 ShortcutService.this, callingPackage, shortcutIds); 1725 } 1726 userPackageChanged(packageName, userId); 1727 } 1728 1729 @Override 1730 public Intent createShortcutIntent(@NonNull String callingPackage, 1731 @NonNull String packageName, @NonNull String shortcutId, int userId) { 1732 // Calling permission must be checked by LauncherAppsImpl. 1733 Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty"); 1734 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); 1735 1736 synchronized (mLock) { 1737 final ShortcutInfo fullShortcut = 1738 getPackageShortcutsLocked(packageName, userId) 1739 .findShortcutById(shortcutId); 1740 return fullShortcut == null ? null : fullShortcut.getIntent(); 1741 } 1742 } 1743 1744 @Override 1745 public void addListener(@NonNull ShortcutChangeListener listener) { 1746 synchronized (mLock) { 1747 mListeners.add(Preconditions.checkNotNull(listener)); 1748 } 1749 } 1750 1751 @Override 1752 public int getShortcutIconResId(@NonNull String callingPackage, 1753 @NonNull ShortcutInfo shortcut, int userId) { 1754 Preconditions.checkNotNull(shortcut, "shortcut"); 1755 1756 synchronized (mLock) { 1757 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1758 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1759 return (shortcutInfo != null && shortcutInfo.hasIconResource()) 1760 ? shortcutInfo.getIconResourceId() : 0; 1761 } 1762 } 1763 1764 @Override 1765 public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage, 1766 @NonNull ShortcutInfo shortcut, int userId) { 1767 Preconditions.checkNotNull(shortcut, "shortcut"); 1768 1769 synchronized (mLock) { 1770 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1771 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1772 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) { 1773 return null; 1774 } 1775 try { 1776 return ParcelFileDescriptor.open( 1777 new File(shortcutInfo.getBitmapPath()), 1778 ParcelFileDescriptor.MODE_READ_ONLY); 1779 } catch (FileNotFoundException e) { 1780 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath()); 1781 return null; 1782 } 1783 } 1784 } 1785 } 1786 1787 // === Dump === 1788 1789 @Override 1790 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1791 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) 1792 != PackageManager.PERMISSION_GRANTED) { 1793 pw.println("Permission Denial: can't dump UserManager from from pid=" 1794 + Binder.getCallingPid() 1795 + ", uid=" + Binder.getCallingUid() 1796 + " without permission " 1797 + android.Manifest.permission.DUMP); 1798 return; 1799 } 1800 dumpInner(pw); 1801 } 1802 1803 @VisibleForTesting 1804 void dumpInner(PrintWriter pw) { 1805 synchronized (mLock) { 1806 final long now = injectCurrentTimeMillis(); 1807 pw.print("Now: ["); 1808 pw.print(now); 1809 pw.print("] "); 1810 pw.print(formatTime(now)); 1811 1812 pw.print(" Raw last reset: ["); 1813 pw.print(mRawLastResetTime); 1814 pw.print("] "); 1815 pw.print(formatTime(mRawLastResetTime)); 1816 1817 final long last = getLastResetTimeLocked(); 1818 pw.print(" Last reset: ["); 1819 pw.print(last); 1820 pw.print("] "); 1821 pw.print(formatTime(last)); 1822 1823 final long next = getNextResetTimeLocked(); 1824 pw.print(" Next reset: ["); 1825 pw.print(next); 1826 pw.print("] "); 1827 pw.print(formatTime(next)); 1828 pw.println(); 1829 1830 pw.print(" Max icon dim: "); 1831 pw.print(mMaxIconDimension); 1832 pw.print(" Icon format: "); 1833 pw.print(mIconPersistFormat); 1834 pw.print(" Icon quality: "); 1835 pw.print(mIconPersistQuality); 1836 pw.println(); 1837 1838 pw.println(); 1839 1840 for (int i = 0; i < mShortcuts.size(); i++) { 1841 dumpUserLocked(pw, mShortcuts.keyAt(i)); 1842 } 1843 } 1844 } 1845 1846 private void dumpUserLocked(PrintWriter pw, int userId) { 1847 pw.print(" User: "); 1848 pw.print(userId); 1849 pw.println(); 1850 1851 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); 1852 if (packages == null) { 1853 return; 1854 } 1855 for (int j = 0; j < packages.size(); j++) { 1856 dumpPackageLocked(pw, userId, packages.keyAt(j)); 1857 } 1858 pw.println(); 1859 } 1860 1861 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { 1862 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName); 1863 if (packageShortcuts == null) { 1864 return; 1865 } 1866 1867 pw.print(" Package: "); 1868 pw.print(packageName); 1869 pw.println(); 1870 1871 pw.print(" Calls: "); 1872 pw.print(packageShortcuts.getApiCallCount(this)); 1873 pw.println(); 1874 1875 // This should be after getApiCallCount(), which may update it. 1876 pw.print(" Last reset: ["); 1877 pw.print(packageShortcuts.mLastResetTime); 1878 pw.print("] "); 1879 pw.print(formatTime(packageShortcuts.mLastResetTime)); 1880 pw.println(); 1881 1882 pw.println(" Shortcuts:"); 1883 long totalBitmapSize = 0; 1884 final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts; 1885 final int size = shortcuts.size(); 1886 for (int i = 0; i < size; i++) { 1887 final ShortcutInfo si = shortcuts.valueAt(i); 1888 pw.print(" "); 1889 pw.println(si.toInsecureString()); 1890 if (si.hasIconFile()) { 1891 final long len = new File(si.getBitmapPath()).length(); 1892 pw.print(" "); 1893 pw.print("bitmap size="); 1894 pw.println(len); 1895 1896 totalBitmapSize += len; 1897 } 1898 } 1899 pw.print(" Total bitmap size: "); 1900 pw.print(totalBitmapSize); 1901 pw.print(" ("); 1902 pw.print(Formatter.formatFileSize(mContext, totalBitmapSize)); 1903 pw.println(")"); 1904 } 1905 1906 private static String formatTime(long time) { 1907 Time tobj = new Time(); 1908 tobj.set(time); 1909 return tobj.format("%Y-%m-%d %H:%M:%S"); 1910 } 1911 1912 // === Shell support === 1913 1914 @Override 1915 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 1916 String[] args, ResultReceiver resultReceiver) throws RemoteException { 1917 1918 enforceShell(); 1919 1920 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); 1921 } 1922 1923 /** 1924 * Handle "adb shell cmd". 1925 */ 1926 private class MyShellCommand extends ShellCommand { 1927 @Override 1928 public int onCommand(String cmd) { 1929 if (cmd == null) { 1930 return handleDefaultCommands(cmd); 1931 } 1932 final PrintWriter pw = getOutPrintWriter(); 1933 int ret = 1; 1934 switch (cmd) { 1935 case "reset-package-throttling": 1936 ret = handleResetPackageThrottling(); 1937 break; 1938 case "reset-throttling": 1939 ret = handleResetThrottling(); 1940 break; 1941 case "override-config": 1942 ret = handleOverrideConfig(); 1943 break; 1944 case "reset-config": 1945 ret = handleResetConfig(); 1946 break; 1947 default: 1948 return handleDefaultCommands(cmd); 1949 } 1950 if (ret == 0) { 1951 pw.println("Success"); 1952 } 1953 return ret; 1954 } 1955 1956 @Override 1957 public void onHelp() { 1958 final PrintWriter pw = getOutPrintWriter(); 1959 pw.println("Usage: cmd shortcut COMMAND [options ...]"); 1960 pw.println(); 1961 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); 1962 pw.println(" Reset throttling for a package"); 1963 pw.println(); 1964 pw.println("cmd shortcut reset-throttling"); 1965 pw.println(" Reset throttling for all packages and users"); 1966 pw.println(); 1967 pw.println("cmd shortcut override-config CONFIG"); 1968 pw.println(" Override the configuration for testing (will last until reboot)"); 1969 pw.println(); 1970 pw.println("cmd shortcut reset-config"); 1971 pw.println(" Reset the configuration set with \"update-config\""); 1972 pw.println(); 1973 } 1974 1975 private int handleResetThrottling() { 1976 resetThrottling(); 1977 return 0; 1978 } 1979 1980 private int handleResetPackageThrottling() { 1981 final PrintWriter pw = getOutPrintWriter(); 1982 1983 int userId = UserHandle.USER_SYSTEM; 1984 String opt; 1985 while ((opt = getNextOption()) != null) { 1986 switch (opt) { 1987 case "--user": 1988 userId = UserHandle.parseUserArg(getNextArgRequired()); 1989 break; 1990 default: 1991 pw.println("Error: Unknown option: " + opt); 1992 return 1; 1993 } 1994 } 1995 final String packageName = getNextArgRequired(); 1996 1997 synchronized (mLock) { 1998 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); 1999 saveUserLocked(userId); 2000 } 2001 2002 return 0; 2003 } 2004 2005 private int handleOverrideConfig() { 2006 final PrintWriter pw = getOutPrintWriter(); 2007 final String config = getNextArgRequired(); 2008 2009 synchronized (mLock) { 2010 if (!updateConfigurationLocked(config)) { 2011 pw.println("override-config failed. See logcat for details."); 2012 return 1; 2013 } 2014 } 2015 return 0; 2016 } 2017 2018 private int handleResetConfig() { 2019 synchronized (mLock) { 2020 loadConfigurationLocked(); 2021 } 2022 return 0; 2023 } 2024 } 2025 2026 // === Unit test support === 2027 2028 // Injection point. 2029 long injectCurrentTimeMillis() { 2030 return System.currentTimeMillis(); 2031 } 2032 2033 // Injection point. 2034 int injectBinderCallingUid() { 2035 return getCallingUid(); 2036 } 2037 2038 File injectSystemDataPath() { 2039 return Environment.getDataSystemDirectory(); 2040 } 2041 2042 File injectUserDataPath(@UserIdInt int userId) { 2043 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER); 2044 } 2045 2046 @VisibleForTesting 2047 boolean injectIsLowRamDevice() { 2048 return ActivityManager.isLowRamDeviceStatic(); 2049 } 2050 2051 File getUserBitmapFilePath(@UserIdInt int userId) { 2052 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS); 2053 } 2054 2055 @VisibleForTesting 2056 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { 2057 return mShortcuts; 2058 } 2059 2060 @VisibleForTesting 2061 int getMaxDynamicShortcutsForTest() { 2062 return mMaxDynamicShortcuts; 2063 } 2064 2065 @VisibleForTesting 2066 int getMaxDailyUpdatesForTest() { 2067 return mMaxDailyUpdates; 2068 } 2069 2070 @VisibleForTesting 2071 long getResetIntervalForTest() { 2072 return mResetInterval; 2073 } 2074 2075 @VisibleForTesting 2076 int getMaxIconDimensionForTest() { 2077 return mMaxIconDimension; 2078 } 2079 2080 @VisibleForTesting 2081 CompressFormat getIconPersistFormatForTest() { 2082 return mIconPersistFormat; 2083 } 2084 2085 @VisibleForTesting 2086 int getIconPersistQualityForTest() { 2087 return mIconPersistQuality; 2088 } 2089} 2090