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