ShortcutService.java revision be4f470038bec4179c86459534c7e21653589bdf
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 shortcut, int userId) { 1440 Preconditions.checkNotNull(shortcut, "shortcut"); 1441 1442 synchronized (mLock) { 1443 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1444 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1445 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) { 1446 return null; 1447 } 1448 try { 1449 return ParcelFileDescriptor.open( 1450 new File(shortcutInfo.getBitmapPath()), 1451 ParcelFileDescriptor.MODE_READ_ONLY); 1452 } catch (FileNotFoundException e) { 1453 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath()); 1454 return null; 1455 } 1456 } 1457 } 1458 } 1459 1460 // === Dump === 1461 1462 @Override 1463 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1464 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) 1465 != PackageManager.PERMISSION_GRANTED) { 1466 pw.println("Permission Denial: can't dump UserManager from from pid=" 1467 + Binder.getCallingPid() 1468 + ", uid=" + Binder.getCallingUid() 1469 + " without permission " 1470 + android.Manifest.permission.DUMP); 1471 return; 1472 } 1473 dumpInner(pw); 1474 } 1475 1476 @VisibleForTesting 1477 void dumpInner(PrintWriter pw) { 1478 synchronized (mLock) { 1479 final long now = injectCurrentTimeMillis(); 1480 pw.print("Now: ["); 1481 pw.print(now); 1482 pw.print("] "); 1483 pw.print(formatTime(now)); 1484 1485 pw.print(" Raw last reset: ["); 1486 pw.print(mRawLastResetTime); 1487 pw.print("] "); 1488 pw.print(formatTime(mRawLastResetTime)); 1489 1490 final long last = getLastResetTimeLocked(); 1491 pw.print(" Last reset: ["); 1492 pw.print(last); 1493 pw.print("] "); 1494 pw.print(formatTime(last)); 1495 1496 final long next = getNextResetTimeLocked(); 1497 pw.print(" Next reset: ["); 1498 pw.print(next); 1499 pw.print("] "); 1500 pw.print(formatTime(next)); 1501 pw.println(); 1502 1503 pw.print(" Max icon dim: "); 1504 pw.print(mMaxIconDimension); 1505 pw.print(" Icon format: "); 1506 pw.print(mIconPersistFormat); 1507 pw.print(" Icon quality: "); 1508 pw.print(mIconPersistQuality); 1509 pw.println(); 1510 1511 pw.println(); 1512 1513 for (int i = 0; i < mShortcuts.size(); i++) { 1514 dumpUserLocked(pw, mShortcuts.keyAt(i)); 1515 } 1516 } 1517 } 1518 1519 private void dumpUserLocked(PrintWriter pw, int userId) { 1520 pw.print(" User: "); 1521 pw.print(userId); 1522 pw.println(); 1523 1524 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); 1525 if (packages == null) { 1526 return; 1527 } 1528 for (int j = 0; j < packages.size(); j++) { 1529 dumpPackageLocked(pw, userId, packages.keyAt(j)); 1530 } 1531 pw.println(); 1532 } 1533 1534 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { 1535 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName); 1536 if (packageShortcuts == null) { 1537 return; 1538 } 1539 1540 packageShortcuts.dump(this, pw, " "); 1541 } 1542 1543 static String formatTime(long time) { 1544 Time tobj = new Time(); 1545 tobj.set(time); 1546 return tobj.format("%Y-%m-%d %H:%M:%S"); 1547 } 1548 1549 // === Shell support === 1550 1551 @Override 1552 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 1553 String[] args, ResultReceiver resultReceiver) throws RemoteException { 1554 1555 enforceShell(); 1556 1557 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); 1558 } 1559 1560 /** 1561 * Handle "adb shell cmd". 1562 */ 1563 private class MyShellCommand extends ShellCommand { 1564 @Override 1565 public int onCommand(String cmd) { 1566 if (cmd == null) { 1567 return handleDefaultCommands(cmd); 1568 } 1569 final PrintWriter pw = getOutPrintWriter(); 1570 int ret = 1; 1571 switch (cmd) { 1572 case "reset-package-throttling": 1573 ret = handleResetPackageThrottling(); 1574 break; 1575 case "reset-throttling": 1576 ret = handleResetThrottling(); 1577 break; 1578 case "override-config": 1579 ret = handleOverrideConfig(); 1580 break; 1581 case "reset-config": 1582 ret = handleResetConfig(); 1583 break; 1584 default: 1585 return handleDefaultCommands(cmd); 1586 } 1587 if (ret == 0) { 1588 pw.println("Success"); 1589 } 1590 return ret; 1591 } 1592 1593 @Override 1594 public void onHelp() { 1595 final PrintWriter pw = getOutPrintWriter(); 1596 pw.println("Usage: cmd shortcut COMMAND [options ...]"); 1597 pw.println(); 1598 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); 1599 pw.println(" Reset throttling for a package"); 1600 pw.println(); 1601 pw.println("cmd shortcut reset-throttling"); 1602 pw.println(" Reset throttling for all packages and users"); 1603 pw.println(); 1604 pw.println("cmd shortcut override-config CONFIG"); 1605 pw.println(" Override the configuration for testing (will last until reboot)"); 1606 pw.println(); 1607 pw.println("cmd shortcut reset-config"); 1608 pw.println(" Reset the configuration set with \"update-config\""); 1609 pw.println(); 1610 } 1611 1612 private int handleResetThrottling() { 1613 resetThrottling(); 1614 return 0; 1615 } 1616 1617 private int handleResetPackageThrottling() { 1618 final PrintWriter pw = getOutPrintWriter(); 1619 1620 int userId = UserHandle.USER_SYSTEM; 1621 String opt; 1622 while ((opt = getNextOption()) != null) { 1623 switch (opt) { 1624 case "--user": 1625 userId = UserHandle.parseUserArg(getNextArgRequired()); 1626 break; 1627 default: 1628 pw.println("Error: Unknown option: " + opt); 1629 return 1; 1630 } 1631 } 1632 final String packageName = getNextArgRequired(); 1633 1634 synchronized (mLock) { 1635 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); 1636 saveUserLocked(userId); 1637 } 1638 1639 return 0; 1640 } 1641 1642 private int handleOverrideConfig() { 1643 final PrintWriter pw = getOutPrintWriter(); 1644 final String config = getNextArgRequired(); 1645 1646 synchronized (mLock) { 1647 if (!updateConfigurationLocked(config)) { 1648 pw.println("override-config failed. See logcat for details."); 1649 return 1; 1650 } 1651 } 1652 return 0; 1653 } 1654 1655 private int handleResetConfig() { 1656 synchronized (mLock) { 1657 loadConfigurationLocked(); 1658 } 1659 return 0; 1660 } 1661 } 1662 1663 // === Unit test support === 1664 1665 // Injection point. 1666 long injectCurrentTimeMillis() { 1667 return System.currentTimeMillis(); 1668 } 1669 1670 // Injection point. 1671 int injectBinderCallingUid() { 1672 return getCallingUid(); 1673 } 1674 1675 File injectSystemDataPath() { 1676 return Environment.getDataSystemDirectory(); 1677 } 1678 1679 File injectUserDataPath(@UserIdInt int userId) { 1680 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER); 1681 } 1682 1683 @VisibleForTesting 1684 boolean injectIsLowRamDevice() { 1685 return ActivityManager.isLowRamDeviceStatic(); 1686 } 1687 1688 File getUserBitmapFilePath(@UserIdInt int userId) { 1689 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS); 1690 } 1691 1692 @VisibleForTesting 1693 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { 1694 return mShortcuts; 1695 } 1696 1697 @VisibleForTesting 1698 int getMaxDynamicShortcutsForTest() { 1699 return mMaxDynamicShortcuts; 1700 } 1701 1702 @VisibleForTesting 1703 int getMaxDailyUpdatesForTest() { 1704 return mMaxDailyUpdates; 1705 } 1706 1707 @VisibleForTesting 1708 long getResetIntervalForTest() { 1709 return mResetInterval; 1710 } 1711 1712 @VisibleForTesting 1713 int getMaxIconDimensionForTest() { 1714 return mMaxIconDimension; 1715 } 1716 1717 @VisibleForTesting 1718 CompressFormat getIconPersistFormatForTest() { 1719 return mIconPersistFormat; 1720 } 1721 1722 @VisibleForTesting 1723 int getIconPersistQualityForTest() { 1724 return mIconPersistQuality; 1725 } 1726 1727 @VisibleForTesting 1728 ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) { 1729 synchronized (mLock) { 1730 return getPackageShortcutsLocked(packageName, userId).findShortcutById(shortcutId); 1731 } 1732 } 1733} 1734 1735/** 1736 * All the information relevant to shortcuts from a single package (per-user). 1737 */ 1738class PackageShortcuts { 1739 private static final String TAG = ShortcutService.TAG; 1740 1741 @UserIdInt 1742 final int mUserId; 1743 1744 @NonNull 1745 final String mPackageName; 1746 1747 /** 1748 * All the shortcuts from the package, keyed on IDs. 1749 */ 1750 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 1751 1752 /** 1753 * # of dynamic shortcuts. 1754 */ 1755 private int mDynamicShortcutCount = 0; 1756 1757 /** 1758 * # of times the package has called rate-limited APIs. 1759 */ 1760 private int mApiCallCount; 1761 1762 /** 1763 * When {@link #mApiCallCount} was reset last time. 1764 */ 1765 private long mLastResetTime; 1766 1767 PackageShortcuts(int userId, String packageName) { 1768 mUserId = userId; 1769 mPackageName = packageName; 1770 } 1771 1772 @GuardedBy("mLock") 1773 @Nullable 1774 public ShortcutInfo findShortcutById(String id) { 1775 return mShortcuts.get(id); 1776 } 1777 1778 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s, 1779 @NonNull String id) { 1780 final ShortcutInfo shortcut = mShortcuts.remove(id); 1781 if (shortcut != null) { 1782 s.removeIcon(mUserId, shortcut); 1783 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED); 1784 } 1785 return shortcut; 1786 } 1787 1788 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) { 1789 deleteShortcut(s, newShortcut.getId()); 1790 s.saveIconAndFixUpShortcut(mUserId, newShortcut); 1791 mShortcuts.put(newShortcut.getId(), newShortcut); 1792 } 1793 1794 /** 1795 * Add a shortcut, or update one with the same ID, with taking over existing flags. 1796 * 1797 * It checks the max number of dynamic shortcuts. 1798 */ 1799 @GuardedBy("mLock") 1800 public void updateShortcutWithCapping(@NonNull ShortcutService s, 1801 @NonNull ShortcutInfo newShortcut) { 1802 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 1803 1804 int oldFlags = 0; 1805 int newDynamicCount = mDynamicShortcutCount; 1806 1807 if (oldShortcut != null) { 1808 oldFlags = oldShortcut.getFlags(); 1809 if (oldShortcut.isDynamic()) { 1810 newDynamicCount--; 1811 } 1812 } 1813 if (newShortcut.isDynamic()) { 1814 newDynamicCount++; 1815 } 1816 // Make sure there's still room. 1817 s.enforceMaxDynamicShortcuts(newDynamicCount); 1818 1819 // Okay, make it dynamic and add. 1820 newShortcut.addFlags(oldFlags); 1821 1822 addShortcut(s, newShortcut); 1823 mDynamicShortcutCount = newDynamicCount; 1824 } 1825 1826 /** 1827 * Remove all shortcuts that aren't pinned nor dynamic. 1828 */ 1829 private void removeOrphans(@NonNull ShortcutService s) { 1830 ArrayList<String> removeList = null; // Lazily initialize. 1831 1832 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1833 final ShortcutInfo si = mShortcuts.valueAt(i); 1834 1835 if (si.isPinned() || si.isDynamic()) continue; 1836 1837 if (removeList == null) { 1838 removeList = new ArrayList<>(); 1839 } 1840 removeList.add(si.getId()); 1841 } 1842 if (removeList != null) { 1843 for (int i = removeList.size() - 1 ; i >= 0; i--) { 1844 deleteShortcut(s, removeList.get(i)); 1845 } 1846 } 1847 } 1848 1849 @GuardedBy("mLock") 1850 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) { 1851 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1852 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC); 1853 } 1854 removeOrphans(s); 1855 mDynamicShortcutCount = 0; 1856 } 1857 1858 @GuardedBy("mLock") 1859 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) { 1860 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 1861 1862 if (oldShortcut == null) { 1863 return; 1864 } 1865 if (oldShortcut.isDynamic()) { 1866 mDynamicShortcutCount--; 1867 } 1868 if (oldShortcut.isPinned()) { 1869 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 1870 } else { 1871 deleteShortcut(s, shortcutId); 1872 } 1873 } 1874 1875 @GuardedBy("mLock") 1876 public void replacePinned(@NonNull ShortcutService s, String launcherPackage, 1877 List<String> shortcutIds) { 1878 1879 // TODO Should be per launcherPackage. 1880 1881 // First, un-pin all shortcuts 1882 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1883 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 1884 } 1885 1886 // Then pin ALL 1887 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 1888 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i)); 1889 if (shortcut != null) { 1890 shortcut.addFlags(ShortcutInfo.FLAG_PINNED); 1891 } 1892 } 1893 1894 removeOrphans(s); 1895 } 1896 1897 /** 1898 * Number of calls that the caller has made, since the last reset. 1899 */ 1900 @GuardedBy("mLock") 1901 public int getApiCallCount(@NonNull ShortcutService s) { 1902 final long last = s.getLastResetTimeLocked(); 1903 1904 final long now = s.injectCurrentTimeMillis(); 1905 if (mLastResetTime > now) { 1906 // Clock rewound. // TODO Test it 1907 mLastResetTime = now; 1908 } 1909 1910 // If not reset yet, then reset. 1911 if (mLastResetTime < last) { 1912 mApiCallCount = 0; 1913 mLastResetTime = last; 1914 } 1915 return mApiCallCount; 1916 } 1917 1918 /** 1919 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 1920 * and return true. Otherwise just return false. 1921 */ 1922 @GuardedBy("mLock") 1923 public boolean tryApiCall(@NonNull ShortcutService s) { 1924 if (getApiCallCount(s) >= s.mMaxDailyUpdates) { 1925 return false; 1926 } 1927 mApiCallCount++; 1928 return true; 1929 } 1930 1931 @GuardedBy("mLock") 1932 public void resetRateLimitingForCommandLine() { 1933 mApiCallCount = 0; 1934 mLastResetTime = 0; 1935 } 1936 1937 /** 1938 * Find all shortcuts that match {@code query}. 1939 */ 1940 @GuardedBy("mLock") 1941 public void findAll(@NonNull List<ShortcutInfo> result, 1942 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 1943 for (int i = 0; i < mShortcuts.size(); i++) { 1944 final ShortcutInfo si = mShortcuts.valueAt(i); 1945 if (query == null || query.test(si)) { 1946 result.add(si.clone(cloneFlag)); 1947 } 1948 } 1949 } 1950 1951 public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) { 1952 pw.print(prefix); 1953 pw.print("Package: "); 1954 pw.print(mPackageName); 1955 pw.println(); 1956 1957 pw.print(prefix); 1958 pw.print(" "); 1959 pw.print("Calls: "); 1960 pw.print(getApiCallCount(s)); 1961 pw.println(); 1962 1963 // This should be after getApiCallCount(), which may update it. 1964 pw.print(prefix); 1965 pw.print(" "); 1966 pw.print("Last reset: ["); 1967 pw.print(mLastResetTime); 1968 pw.print("] "); 1969 pw.print(s.formatTime(mLastResetTime)); 1970 pw.println(); 1971 1972 pw.println(" Shortcuts:"); 1973 long totalBitmapSize = 0; 1974 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 1975 final int size = shortcuts.size(); 1976 for (int i = 0; i < size; i++) { 1977 final ShortcutInfo si = shortcuts.valueAt(i); 1978 pw.print(" "); 1979 pw.println(si.toInsecureString()); 1980 if (si.hasIconFile()) { 1981 final long len = new File(si.getBitmapPath()).length(); 1982 pw.print(" "); 1983 pw.print("bitmap size="); 1984 pw.println(len); 1985 1986 totalBitmapSize += len; 1987 } 1988 } 1989 pw.print(prefix); 1990 pw.print(" "); 1991 pw.print("Total bitmap size: "); 1992 pw.print(totalBitmapSize); 1993 pw.print(" ("); 1994 pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize)); 1995 pw.println(")"); 1996 } 1997 1998 public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException { 1999 out.startTag(null, ShortcutService.TAG_PACKAGE); 2000 2001 ShortcutService.writeAttr(out, ShortcutService.ATTR_NAME, mPackageName); 2002 ShortcutService.writeAttr(out, ShortcutService.ATTR_DYNAMIC_COUNT, mDynamicShortcutCount); 2003 ShortcutService.writeAttr(out, ShortcutService.ATTR_CALL_COUNT, mApiCallCount); 2004 ShortcutService.writeAttr(out, ShortcutService.ATTR_LAST_RESET, mLastResetTime); 2005 2006 final int size = mShortcuts.size(); 2007 for (int j = 0; j < size; j++) { 2008 saveShortcut(out, mShortcuts.valueAt(j)); 2009 } 2010 2011 out.endTag(null, ShortcutService.TAG_PACKAGE); 2012 } 2013 2014 private static void saveShortcut(XmlSerializer out, ShortcutInfo si) 2015 throws IOException, XmlPullParserException { 2016 out.startTag(null, ShortcutService.TAG_SHORTCUT); 2017 ShortcutService.writeAttr(out, ShortcutService.ATTR_ID, si.getId()); 2018 // writeAttr(out, "package", si.getPackageName()); // not needed 2019 ShortcutService.writeAttr(out, ShortcutService.ATTR_ACTIVITY, si.getActivityComponent()); 2020 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 2021 ShortcutService.writeAttr(out, ShortcutService.ATTR_TITLE, si.getTitle()); 2022 ShortcutService.writeAttr(out, ShortcutService.ATTR_INTENT, si.getIntentNoExtras()); 2023 ShortcutService.writeAttr(out, ShortcutService.ATTR_WEIGHT, si.getWeight()); 2024 ShortcutService.writeAttr(out, ShortcutService.ATTR_TIMESTAMP, 2025 si.getLastChangedTimestamp()); 2026 ShortcutService.writeAttr(out, ShortcutService.ATTR_FLAGS, si.getFlags()); 2027 ShortcutService.writeAttr(out, ShortcutService.ATTR_ICON_RES, si.getIconResourceId()); 2028 ShortcutService.writeAttr(out, ShortcutService.ATTR_BITMAP_PATH, si.getBitmapPath()); 2029 2030 ShortcutService.writeTagExtra(out, ShortcutService.TAG_INTENT_EXTRAS, 2031 si.getIntentPersistableExtras()); 2032 ShortcutService.writeTagExtra(out, ShortcutService.TAG_EXTRAS, si.getExtras()); 2033 2034 out.endTag(null, ShortcutService.TAG_SHORTCUT); 2035 } 2036 2037 public static PackageShortcuts loadFromXml(XmlPullParser parser, int userId) 2038 throws IOException, XmlPullParserException { 2039 2040 final String packageName = ShortcutService.parseStringAttribute(parser, 2041 ShortcutService.ATTR_NAME); 2042 2043 final PackageShortcuts ret = new PackageShortcuts(userId, packageName); 2044 2045 ret.mDynamicShortcutCount = 2046 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_DYNAMIC_COUNT); 2047 ret.mApiCallCount = 2048 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_CALL_COUNT); 2049 ret.mLastResetTime = 2050 ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_LAST_RESET); 2051 2052 final int outerDepth = parser.getDepth(); 2053 int type; 2054 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 2055 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 2056 if (type != XmlPullParser.START_TAG) { 2057 continue; 2058 } 2059 final int depth = parser.getDepth(); 2060 final String tag = parser.getName(); 2061 switch (tag) { 2062 case ShortcutService.TAG_SHORTCUT: 2063 final ShortcutInfo si = parseShortcut(parser, packageName); 2064 2065 // Don't use addShortcut(), we don't need to save the icon. 2066 ret.mShortcuts.put(si.getId(), si); 2067 continue; 2068 } 2069 throw ShortcutService.throwForInvalidTag(depth, tag); 2070 } 2071 return ret; 2072 } 2073 2074 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName) 2075 throws IOException, XmlPullParserException { 2076 String id; 2077 ComponentName activityComponent; 2078 // Icon icon; 2079 String title; 2080 Intent intent; 2081 PersistableBundle intentPersistableExtras = null; 2082 int weight; 2083 PersistableBundle extras = null; 2084 long lastChangedTimestamp; 2085 int flags; 2086 int iconRes; 2087 String bitmapPath; 2088 2089 id = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_ID); 2090 activityComponent = ShortcutService.parseComponentNameAttribute(parser, 2091 ShortcutService.ATTR_ACTIVITY); 2092 title = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_TITLE); 2093 intent = ShortcutService.parseIntentAttribute(parser, ShortcutService.ATTR_INTENT); 2094 weight = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_WEIGHT); 2095 lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser, 2096 ShortcutService.ATTR_TIMESTAMP); 2097 flags = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_FLAGS); 2098 iconRes = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_ICON_RES); 2099 bitmapPath = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_BITMAP_PATH); 2100 2101 final int outerDepth = parser.getDepth(); 2102 int type; 2103 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 2104 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 2105 if (type != XmlPullParser.START_TAG) { 2106 continue; 2107 } 2108 final int depth = parser.getDepth(); 2109 final String tag = parser.getName(); 2110 if (ShortcutService.DEBUG_LOAD) { 2111 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 2112 depth, type, tag)); 2113 } 2114 switch (tag) { 2115 case ShortcutService.TAG_INTENT_EXTRAS: 2116 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 2117 continue; 2118 case ShortcutService.TAG_EXTRAS: 2119 extras = PersistableBundle.restoreFromXml(parser); 2120 continue; 2121 } 2122 throw ShortcutService.throwForInvalidTag(depth, tag); 2123 } 2124 return new ShortcutInfo( 2125 id, packageName, activityComponent, /* icon =*/ null, title, intent, 2126 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 2127 iconRes, bitmapPath); 2128 } 2129} 2130