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