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