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