ShortcutService.java revision 41066a61b912f22dd0342a002b4b5e060719cec9
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 onStartUser(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 path.mkdirs(); 621 final AtomicFile file = new AtomicFile(path); 622 623 final FileInputStream in; 624 try { 625 in = file.openRead(); 626 } catch (FileNotFoundException e) { 627 if (DEBUG) { 628 Slog.i(TAG, "Not found " + path); 629 } 630 return null; 631 } 632 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<>(); 633 try { 634 XmlPullParser parser = Xml.newPullParser(); 635 parser.setInput(in, StandardCharsets.UTF_8.name()); 636 637 PackageShortcuts shortcuts = null; 638 639 int type; 640 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 641 if (type != XmlPullParser.START_TAG) { 642 continue; 643 } 644 final int depth = parser.getDepth(); 645 646 final String tag = parser.getName(); 647 if (DEBUG_LOAD) { 648 Slog.d(TAG, String.format("depth=%d type=%d name=%s", 649 depth, type, tag)); 650 } 651 switch (depth) { 652 case 1: { 653 if (TAG_ROOT.equals(tag)) { 654 continue; 655 } 656 break; 657 } 658 case 2: { 659 switch (tag) { 660 case TAG_PACKAGE: 661 shortcuts = PackageShortcuts.loadFromXml(parser, userId); 662 ret.put(shortcuts.mPackageName, shortcuts); 663 continue; 664 } 665 break; 666 } 667 } 668 throwForInvalidTag(depth, tag); 669 } 670 return ret; 671 } catch (IOException|XmlPullParserException e) { 672 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 673 return null; 674 } finally { 675 IoUtils.closeQuietly(in); 676 } 677 } 678 679 // TODO Actually make it async. 680 private void scheduleSaveBaseState() { 681 synchronized (mLock) { 682 saveBaseStateLocked(); 683 } 684 } 685 686 // TODO Actually make it async. 687 private void scheduleSaveUser(@UserIdInt int userId) { 688 synchronized (mLock) { 689 saveUserLocked(userId); 690 } 691 } 692 693 /** Return the last reset time. */ 694 long getLastResetTimeLocked() { 695 updateTimes(); 696 return mRawLastResetTime; 697 } 698 699 /** Return the next reset time. */ 700 long getNextResetTimeLocked() { 701 updateTimes(); 702 return mRawLastResetTime + mResetInterval; 703 } 704 705 /** 706 * Update the last reset time. 707 */ 708 private void updateTimes() { 709 710 final long now = injectCurrentTimeMillis(); 711 712 final long prevLastResetTime = mRawLastResetTime; 713 714 if (mRawLastResetTime == 0) { // first launch. 715 // TODO Randomize?? 716 mRawLastResetTime = now; 717 } else if (now < mRawLastResetTime) { 718 // Clock rewound. 719 // TODO Randomize?? 720 mRawLastResetTime = now; 721 } else { 722 // TODO Do it properly. 723 while ((mRawLastResetTime + mResetInterval) <= now) { 724 mRawLastResetTime += mResetInterval; 725 } 726 } 727 if (prevLastResetTime != mRawLastResetTime) { 728 scheduleSaveBaseState(); 729 } 730 } 731 732 /** Return the per-user state. */ 733 @GuardedBy("mLock") 734 @NonNull 735 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) { 736 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId); 737 if (userPackages == null) { 738 userPackages = loadUserLocked(userId); 739 if (userPackages == null) { 740 userPackages = new ArrayMap<>(); 741 } 742 mShortcuts.put(userId, userPackages); 743 } 744 return userPackages; 745 } 746 747 /** Return the per-user per-package state. */ 748 @GuardedBy("mLock") 749 @NonNull 750 private PackageShortcuts getPackageShortcutsLocked( 751 @NonNull String packageName, @UserIdInt int userId) { 752 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId); 753 PackageShortcuts shortcuts = userPackages.get(packageName); 754 if (shortcuts == null) { 755 shortcuts = new PackageShortcuts(userId, packageName); 756 userPackages.put(packageName, shortcuts); 757 } 758 return shortcuts; 759 } 760 761 // === Caller validation === 762 763 void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) { 764 if (shortcut.getBitmapPath() != null) { 765 if (DEBUG) { 766 Slog.d(TAG, "Removing " + shortcut.getBitmapPath()); 767 } 768 new File(shortcut.getBitmapPath()).delete(); 769 770 shortcut.setBitmapPath(null); 771 shortcut.setIconResourceId(0); 772 shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES); 773 } 774 } 775 776 @VisibleForTesting 777 static class FileOutputStreamWithPath extends FileOutputStream { 778 private final File mFile; 779 780 public FileOutputStreamWithPath(File file) throws FileNotFoundException { 781 super(file); 782 mFile = file; 783 } 784 785 public File getFile() { 786 return mFile; 787 } 788 } 789 790 /** 791 * Build the cached bitmap filename for a shortcut icon. 792 * 793 * The filename will be based on the ID, except certain characters will be escaped. 794 */ 795 @VisibleForTesting 796 FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut) 797 throws IOException { 798 final File packagePath = new File(getUserBitmapFilePath(userId), 799 shortcut.getPackageName()); 800 if (!packagePath.isDirectory()) { 801 packagePath.mkdirs(); 802 if (!packagePath.isDirectory()) { 803 throw new IOException("Unable to create directory " + packagePath); 804 } 805 SELinux.restorecon(packagePath); 806 } 807 808 final String baseName = String.valueOf(injectCurrentTimeMillis()); 809 for (int suffix = 0;; suffix++) { 810 final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png"; 811 final File file = new File(packagePath, filename); 812 if (!file.exists()) { 813 if (DEBUG) { 814 Slog.d(TAG, "Saving icon to " + file.getAbsolutePath()); 815 } 816 return new FileOutputStreamWithPath(file); 817 } 818 } 819 } 820 821 void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) { 822 if (shortcut.hasIconFile() || shortcut.hasIconResource()) { 823 return; 824 } 825 826 final long token = Binder.clearCallingIdentity(); 827 try { 828 // Clear icon info on the shortcut. 829 shortcut.setIconResourceId(0); 830 shortcut.setBitmapPath(null); 831 832 final Icon icon = shortcut.getIcon(); 833 if (icon == null) { 834 return; // has no icon 835 } 836 837 Bitmap bitmap = null; 838 try { 839 switch (icon.getType()) { 840 case Icon.TYPE_RESOURCE: { 841 injectValidateIconResPackage(shortcut, icon); 842 843 shortcut.setIconResourceId(icon.getResId()); 844 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES); 845 return; 846 } 847 case Icon.TYPE_BITMAP: { 848 bitmap = icon.getBitmap(); 849 break; 850 } 851 case Icon.TYPE_URI: { 852 final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId); 853 854 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { 855 856 bitmap = BitmapFactory.decodeStream(is); 857 858 } catch (IOException e) { 859 Slog.e(TAG, "Unable to load icon from " + uri); 860 return; 861 } 862 break; 863 } 864 default: 865 // This shouldn't happen because we've already validated the icon, but 866 // just in case. 867 throw ShortcutInfo.getInvalidIconException(); 868 } 869 if (bitmap == null) { 870 Slog.e(TAG, "Null bitmap detected"); 871 return; 872 } 873 // Shrink and write to the file. 874 File path = null; 875 try { 876 final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut); 877 try { 878 path = out.getFile(); 879 880 shrinkBitmap(bitmap, mMaxIconDimension) 881 .compress(mIconPersistFormat, mIconPersistQuality, out); 882 883 shortcut.setBitmapPath(out.getFile().getAbsolutePath()); 884 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE); 885 } finally { 886 IoUtils.closeQuietly(out); 887 } 888 } catch (IOException|RuntimeException e) { 889 // STOPSHIP Change wtf to e 890 Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e); 891 if (path != null && path.exists()) { 892 path.delete(); 893 } 894 } 895 } finally { 896 if (bitmap != null) { 897 bitmap.recycle(); 898 } 899 // Once saved, we won't use the original icon information, so null it out. 900 shortcut.clearIcon(); 901 } 902 } finally { 903 Binder.restoreCallingIdentity(token); 904 } 905 } 906 907 // Unfortunately we can't do this check in unit tests because we fake creator package names, 908 // so override in unit tests. 909 // TODO CTS this case. 910 void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) { 911 if (!shortcut.getPackageName().equals(icon.getResPackage())) { 912 throw new IllegalArgumentException( 913 "Icon resource must reside in shortcut owner package"); 914 } 915 } 916 917 @VisibleForTesting 918 static Bitmap shrinkBitmap(Bitmap in, int maxSize) { 919 // Original width/height. 920 final int ow = in.getWidth(); 921 final int oh = in.getHeight(); 922 if ((ow <= maxSize) && (oh <= maxSize)) { 923 if (DEBUG) { 924 Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh)); 925 } 926 return in; 927 } 928 final int longerDimension = Math.max(ow, oh); 929 930 // New width and height. 931 final int nw = ow * maxSize / longerDimension; 932 final int nh = oh * maxSize / longerDimension; 933 if (DEBUG) { 934 Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d", 935 ow, oh, nw, nh)); 936 } 937 938 final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888); 939 final Canvas c = new Canvas(scaledBitmap); 940 941 final RectF dst = new RectF(0, 0, nw, nh); 942 943 c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null); 944 945 in.recycle(); 946 947 return scaledBitmap; 948 } 949 950 // === Caller validation === 951 952 private boolean isCallerSystem() { 953 final int callingUid = injectBinderCallingUid(); 954 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); 955 } 956 957 private boolean isCallerShell() { 958 final int callingUid = injectBinderCallingUid(); 959 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; 960 } 961 962 private void enforceSystemOrShell() { 963 Preconditions.checkState(isCallerSystem() || isCallerShell(), 964 "Caller must be system or shell"); 965 } 966 967 private void enforceShell() { 968 Preconditions.checkState(isCallerShell(), "Caller must be shell"); 969 } 970 971 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { 972 Preconditions.checkStringNotEmpty(packageName, "packageName"); 973 974 if (isCallerSystem()) { 975 return; // no check 976 } 977 978 final int callingUid = injectBinderCallingUid(); 979 980 // Otherwise, make sure the arguments are valid. 981 if (UserHandle.getUserId(callingUid) != userId) { 982 throw new SecurityException("Invalid user-ID"); 983 } 984 if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) { 985 return; // Caller is valid. 986 } 987 throw new SecurityException("Caller UID= doesn't own " + packageName); 988 } 989 990 // Test overrides it. 991 int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) { 992 try { 993 994 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? 995 996 return mContext.getPackageManager().getPackageUidAsUser(packageName, 997 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE 998 | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 999 } catch (NameNotFoundException e) { 1000 return -1; 1001 } 1002 } 1003 1004 /** 1005 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. 1006 */ 1007 void enforceMaxDynamicShortcuts(int numShortcuts) { 1008 if (numShortcuts > mMaxDynamicShortcuts) { 1009 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); 1010 } 1011 } 1012 1013 /** 1014 * - Sends a notification to LauncherApps 1015 * - Write to file 1016 */ 1017 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { 1018 notifyListeners(packageName, userId); 1019 scheduleSaveUser(userId); 1020 } 1021 1022 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { 1023 final ArrayList<ShortcutChangeListener> copy; 1024 final List<ShortcutInfo> shortcuts = new ArrayList<>(); 1025 synchronized (mLock) { 1026 copy = new ArrayList<>(mListeners); 1027 1028 getPackageShortcutsLocked(packageName, userId) 1029 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); 1030 } 1031 for (int i = copy.size() - 1; i >= 0; i--) { 1032 copy.get(i).onShortcutChanged(packageName, shortcuts, userId); 1033 } 1034 } 1035 1036 /** 1037 * Clean up / validate an incoming shortcut. 1038 * - Make sure all mandatory fields are set. 1039 * - Make sure the intent's extras are persistable, and them to set 1040 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. 1041 * - Clear flags. 1042 * 1043 * TODO Detailed unit tests 1044 */ 1045 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { 1046 Preconditions.checkNotNull(shortcut, "Null shortcut detected"); 1047 if (shortcut.getActivityComponent() != null) { 1048 Preconditions.checkState( 1049 shortcut.getPackageName().equals( 1050 shortcut.getActivityComponent().getPackageName()), 1051 "Activity package name mismatch"); 1052 } 1053 1054 if (!forUpdate) { 1055 shortcut.enforceMandatoryFields(); 1056 } 1057 if (shortcut.getIcon() != null) { 1058 ShortcutInfo.validateIcon(shortcut.getIcon()); 1059 } 1060 1061 validateForXml(shortcut.getId()); 1062 validateForXml(shortcut.getTitle()); 1063 validatePersistableBundleForXml(shortcut.getIntentPersistableExtras()); 1064 validatePersistableBundleForXml(shortcut.getExtras()); 1065 1066 shortcut.setFlags(0); 1067 } 1068 1069 // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those 1070 // characters. 1071 1072 private static void validatePersistableBundleForXml(PersistableBundle b) { 1073 if (b == null || b.size() == 0) { 1074 return; 1075 } 1076 for (String key : b.keySet()) { 1077 validateForXml(key); 1078 final Object value = b.get(key); 1079 if (value == null) { 1080 continue; 1081 } else if (value instanceof String) { 1082 validateForXml((String) value); 1083 } else if (value instanceof String[]) { 1084 for (String v : (String[]) value) { 1085 validateForXml(v); 1086 } 1087 } else if (value instanceof PersistableBundle) { 1088 validatePersistableBundleForXml((PersistableBundle) value); 1089 } 1090 } 1091 } 1092 1093 private static void validateForXml(String s) { 1094 if (TextUtils.isEmpty(s)) { 1095 return; 1096 } 1097 for (int i = s.length() - 1; i >= 0; i--) { 1098 if (!isAllowedInXml(s.charAt(i))) { 1099 throw new IllegalArgumentException("Unsupported character detected in: " + s); 1100 } 1101 } 1102 } 1103 1104 private static boolean isAllowedInXml(char c) { 1105 return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 1106 } 1107 1108 // === APIs === 1109 1110 @Override 1111 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1112 @UserIdInt int userId) { 1113 verifyCaller(packageName, userId); 1114 1115 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1116 final int size = newShortcuts.size(); 1117 1118 synchronized (mLock) { 1119 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1120 1121 // Throttling. 1122 if (!ps.tryApiCall(this)) { 1123 return false; 1124 } 1125 enforceMaxDynamicShortcuts(size); 1126 1127 // Validate the shortcuts. 1128 for (int i = 0; i < size; i++) { 1129 fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); 1130 } 1131 1132 // First, remove all un-pinned; dynamic shortcuts 1133 ps.deleteAllDynamicShortcuts(this); 1134 1135 // Then, add/update all. We need to make sure to take over "pinned" flag. 1136 for (int i = 0; i < size; i++) { 1137 final ShortcutInfo newShortcut = newShortcuts.get(i); 1138 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1139 ps.updateShortcutWithCapping(this, newShortcut); 1140 } 1141 } 1142 userPackageChanged(packageName, userId); 1143 return true; 1144 } 1145 1146 @Override 1147 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1148 @UserIdInt int userId) { 1149 verifyCaller(packageName, userId); 1150 1151 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1152 final int size = newShortcuts.size(); 1153 1154 synchronized (mLock) { 1155 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1156 1157 // Throttling. 1158 if (!ps.tryApiCall(this)) { 1159 return false; 1160 } 1161 1162 for (int i = 0; i < size; i++) { 1163 final ShortcutInfo source = newShortcuts.get(i); 1164 fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); 1165 1166 final ShortcutInfo target = ps.findShortcutById(source.getId()); 1167 if (target != null) { 1168 final boolean replacingIcon = (source.getIcon() != null); 1169 if (replacingIcon) { 1170 removeIcon(userId, target); 1171 } 1172 1173 target.copyNonNullFieldsFrom(source); 1174 1175 if (replacingIcon) { 1176 saveIconAndFixUpShortcut(userId, target); 1177 } 1178 } 1179 } 1180 } 1181 userPackageChanged(packageName, userId); 1182 1183 return true; 1184 } 1185 1186 @Override 1187 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, 1188 @UserIdInt int userId) { 1189 verifyCaller(packageName, userId); 1190 1191 synchronized (mLock) { 1192 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1193 1194 // Throttling. 1195 if (!ps.tryApiCall(this)) { 1196 return false; 1197 } 1198 1199 // Validate the shortcut. 1200 fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); 1201 1202 // Add it. 1203 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1204 ps.updateShortcutWithCapping(this, newShortcut); 1205 } 1206 userPackageChanged(packageName, userId); 1207 1208 return true; 1209 } 1210 1211 @Override 1212 public void deleteDynamicShortcut(String packageName, String shortcutId, 1213 @UserIdInt int userId) { 1214 verifyCaller(packageName, userId); 1215 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); 1216 1217 synchronized (mLock) { 1218 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId); 1219 } 1220 userPackageChanged(packageName, userId); 1221 } 1222 1223 @Override 1224 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { 1225 verifyCaller(packageName, userId); 1226 1227 synchronized (mLock) { 1228 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this); 1229 } 1230 userPackageChanged(packageName, userId); 1231 } 1232 1233 @Override 1234 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName, 1235 @UserIdInt int userId) { 1236 verifyCaller(packageName, userId); 1237 synchronized (mLock) { 1238 return getShortcutsWithQueryLocked( 1239 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1240 ShortcutInfo::isDynamic); 1241 } 1242 } 1243 1244 @Override 1245 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName, 1246 @UserIdInt int userId) { 1247 verifyCaller(packageName, userId); 1248 synchronized (mLock) { 1249 return getShortcutsWithQueryLocked( 1250 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1251 ShortcutInfo::isPinned); 1252 } 1253 } 1254 1255 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, 1256 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) { 1257 1258 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1259 1260 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); 1261 1262 return new ParceledListSlice<>(ret); 1263 } 1264 1265 @Override 1266 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) 1267 throws RemoteException { 1268 verifyCaller(packageName, userId); 1269 1270 return mMaxDynamicShortcuts; 1271 } 1272 1273 @Override 1274 public int getRemainingCallCount(String packageName, @UserIdInt int userId) { 1275 verifyCaller(packageName, userId); 1276 1277 synchronized (mLock) { 1278 return mMaxDailyUpdates 1279 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); 1280 } 1281 } 1282 1283 @Override 1284 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { 1285 verifyCaller(packageName, userId); 1286 1287 synchronized (mLock) { 1288 return getNextResetTimeLocked(); 1289 } 1290 } 1291 1292 @Override 1293 public int getIconMaxDimensions(String packageName, int userId) throws RemoteException { 1294 synchronized (mLock) { 1295 return mMaxIconDimension; 1296 } 1297 } 1298 1299 /** 1300 * Reset all throttling, for developer options and command line. Only system/shell can call it. 1301 */ 1302 @Override 1303 public void resetThrottling() { 1304 enforceSystemOrShell(); 1305 1306 resetThrottlingInner(); 1307 } 1308 1309 @VisibleForTesting 1310 void resetThrottlingInner() { 1311 synchronized (mLock) { 1312 mRawLastResetTime = injectCurrentTimeMillis(); 1313 } 1314 scheduleSaveBaseState(); 1315 Slog.i(TAG, "ShortcutManager: throttling counter reset"); 1316 } 1317 1318 /** 1319 * Entry point from {@link LauncherApps}. 1320 */ 1321 private class LocalService extends ShortcutServiceInternal { 1322 @Override 1323 public List<ShortcutInfo> getShortcuts( 1324 @NonNull String callingPackage, long changedSince, 1325 @Nullable String packageName, @Nullable ComponentName componentName, 1326 int queryFlags, int userId) { 1327 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1328 final int cloneFlag = 1329 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) 1330 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER 1331 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; 1332 1333 synchronized (mLock) { 1334 if (packageName != null) { 1335 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, 1336 userId, ret, cloneFlag); 1337 } else { 1338 final ArrayMap<String, PackageShortcuts> packages = 1339 getUserShortcutsLocked(userId); 1340 for (int i = packages.size() - 1; i >= 0; i--) { 1341 getShortcutsInnerLocked( 1342 packages.keyAt(i), 1343 changedSince, componentName, queryFlags, userId, ret, cloneFlag); 1344 } 1345 } 1346 } 1347 return ret; 1348 } 1349 1350 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, 1351 @Nullable ComponentName componentName, int queryFlags, 1352 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) { 1353 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1354 (ShortcutInfo si) -> { 1355 if (si.getLastChangedTimestamp() < changedSince) { 1356 return false; 1357 } 1358 if (componentName != null 1359 && !componentName.equals(si.getActivityComponent())) { 1360 return false; 1361 } 1362 final boolean matchDynamic = 1363 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) 1364 && si.isDynamic(); 1365 final boolean matchPinned = 1366 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) 1367 && si.isPinned(); 1368 return matchDynamic || matchPinned; 1369 }, cloneFlag); 1370 } 1371 1372 @Override 1373 public List<ShortcutInfo> getShortcutInfo( 1374 @NonNull String callingPackage, 1375 @NonNull String packageName, @Nullable List<String> ids, int userId) { 1376 // Calling permission must be checked by LauncherAppsImpl. 1377 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1378 1379 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size()); 1380 final ArraySet<String> idSet = new ArraySet<>(ids); 1381 synchronized (mLock) { 1382 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1383 (ShortcutInfo si) -> idSet.contains(si.getId()), 1384 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 1385 } 1386 return ret; 1387 } 1388 1389 @Override 1390 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, 1391 @NonNull List<String> shortcutIds, int userId) { 1392 // Calling permission must be checked by LauncherAppsImpl. 1393 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1394 Preconditions.checkNotNull(shortcutIds, "shortcutIds"); 1395 1396 synchronized (mLock) { 1397 getPackageShortcutsLocked(packageName, userId).replacePinned( 1398 ShortcutService.this, callingPackage, shortcutIds); 1399 } 1400 userPackageChanged(packageName, userId); 1401 } 1402 1403 @Override 1404 public Intent createShortcutIntent(@NonNull String callingPackage, 1405 @NonNull String packageName, @NonNull String shortcutId, int userId) { 1406 // Calling permission must be checked by LauncherAppsImpl. 1407 Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty"); 1408 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); 1409 1410 synchronized (mLock) { 1411 final ShortcutInfo fullShortcut = 1412 getPackageShortcutsLocked(packageName, userId) 1413 .findShortcutById(shortcutId); 1414 return fullShortcut == null ? null : fullShortcut.getIntent(); 1415 } 1416 } 1417 1418 @Override 1419 public void addListener(@NonNull ShortcutChangeListener listener) { 1420 synchronized (mLock) { 1421 mListeners.add(Preconditions.checkNotNull(listener)); 1422 } 1423 } 1424 1425 @Override 1426 public int getShortcutIconResId(@NonNull String callingPackage, 1427 @NonNull ShortcutInfo shortcut, int userId) { 1428 Preconditions.checkNotNull(shortcut, "shortcut"); 1429 1430 synchronized (mLock) { 1431 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1432 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1433 return (shortcutInfo != null && shortcutInfo.hasIconResource()) 1434 ? shortcutInfo.getIconResourceId() : 0; 1435 } 1436 } 1437 1438 @Override 1439 public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage, 1440 @NonNull ShortcutInfo shortcut, int userId) { 1441 Preconditions.checkNotNull(shortcut, "shortcut"); 1442 1443 synchronized (mLock) { 1444 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked( 1445 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId()); 1446 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) { 1447 return null; 1448 } 1449 try { 1450 return ParcelFileDescriptor.open( 1451 new File(shortcutInfo.getBitmapPath()), 1452 ParcelFileDescriptor.MODE_READ_ONLY); 1453 } catch (FileNotFoundException e) { 1454 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath()); 1455 return null; 1456 } 1457 } 1458 } 1459 } 1460 1461 // === Dump === 1462 1463 @Override 1464 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1465 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) 1466 != PackageManager.PERMISSION_GRANTED) { 1467 pw.println("Permission Denial: can't dump UserManager from from pid=" 1468 + Binder.getCallingPid() 1469 + ", uid=" + Binder.getCallingUid() 1470 + " without permission " 1471 + android.Manifest.permission.DUMP); 1472 return; 1473 } 1474 dumpInner(pw); 1475 } 1476 1477 @VisibleForTesting 1478 void dumpInner(PrintWriter pw) { 1479 synchronized (mLock) { 1480 final long now = injectCurrentTimeMillis(); 1481 pw.print("Now: ["); 1482 pw.print(now); 1483 pw.print("] "); 1484 pw.print(formatTime(now)); 1485 1486 pw.print(" Raw last reset: ["); 1487 pw.print(mRawLastResetTime); 1488 pw.print("] "); 1489 pw.print(formatTime(mRawLastResetTime)); 1490 1491 final long last = getLastResetTimeLocked(); 1492 pw.print(" Last reset: ["); 1493 pw.print(last); 1494 pw.print("] "); 1495 pw.print(formatTime(last)); 1496 1497 final long next = getNextResetTimeLocked(); 1498 pw.print(" Next reset: ["); 1499 pw.print(next); 1500 pw.print("] "); 1501 pw.print(formatTime(next)); 1502 pw.println(); 1503 1504 pw.print(" Max icon dim: "); 1505 pw.print(mMaxIconDimension); 1506 pw.print(" Icon format: "); 1507 pw.print(mIconPersistFormat); 1508 pw.print(" Icon quality: "); 1509 pw.print(mIconPersistQuality); 1510 pw.println(); 1511 1512 pw.println(); 1513 1514 for (int i = 0; i < mShortcuts.size(); i++) { 1515 dumpUserLocked(pw, mShortcuts.keyAt(i)); 1516 } 1517 } 1518 } 1519 1520 private void dumpUserLocked(PrintWriter pw, int userId) { 1521 pw.print(" User: "); 1522 pw.print(userId); 1523 pw.println(); 1524 1525 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); 1526 if (packages == null) { 1527 return; 1528 } 1529 for (int j = 0; j < packages.size(); j++) { 1530 dumpPackageLocked(pw, userId, packages.keyAt(j)); 1531 } 1532 pw.println(); 1533 } 1534 1535 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { 1536 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName); 1537 if (packageShortcuts == null) { 1538 return; 1539 } 1540 1541 packageShortcuts.dump(this, pw, " "); 1542 } 1543 1544 static String formatTime(long time) { 1545 Time tobj = new Time(); 1546 tobj.set(time); 1547 return tobj.format("%Y-%m-%d %H:%M:%S"); 1548 } 1549 1550 // === Shell support === 1551 1552 @Override 1553 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 1554 String[] args, ResultReceiver resultReceiver) throws RemoteException { 1555 1556 enforceShell(); 1557 1558 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); 1559 } 1560 1561 /** 1562 * Handle "adb shell cmd". 1563 */ 1564 private class MyShellCommand extends ShellCommand { 1565 @Override 1566 public int onCommand(String cmd) { 1567 if (cmd == null) { 1568 return handleDefaultCommands(cmd); 1569 } 1570 final PrintWriter pw = getOutPrintWriter(); 1571 int ret = 1; 1572 switch (cmd) { 1573 case "reset-package-throttling": 1574 ret = handleResetPackageThrottling(); 1575 break; 1576 case "reset-throttling": 1577 ret = handleResetThrottling(); 1578 break; 1579 case "override-config": 1580 ret = handleOverrideConfig(); 1581 break; 1582 case "reset-config": 1583 ret = handleResetConfig(); 1584 break; 1585 default: 1586 return handleDefaultCommands(cmd); 1587 } 1588 if (ret == 0) { 1589 pw.println("Success"); 1590 } 1591 return ret; 1592 } 1593 1594 @Override 1595 public void onHelp() { 1596 final PrintWriter pw = getOutPrintWriter(); 1597 pw.println("Usage: cmd shortcut COMMAND [options ...]"); 1598 pw.println(); 1599 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); 1600 pw.println(" Reset throttling for a package"); 1601 pw.println(); 1602 pw.println("cmd shortcut reset-throttling"); 1603 pw.println(" Reset throttling for all packages and users"); 1604 pw.println(); 1605 pw.println("cmd shortcut override-config CONFIG"); 1606 pw.println(" Override the configuration for testing (will last until reboot)"); 1607 pw.println(); 1608 pw.println("cmd shortcut reset-config"); 1609 pw.println(" Reset the configuration set with \"update-config\""); 1610 pw.println(); 1611 } 1612 1613 private int handleResetThrottling() { 1614 resetThrottling(); 1615 return 0; 1616 } 1617 1618 private int handleResetPackageThrottling() { 1619 final PrintWriter pw = getOutPrintWriter(); 1620 1621 int userId = UserHandle.USER_SYSTEM; 1622 String opt; 1623 while ((opt = getNextOption()) != null) { 1624 switch (opt) { 1625 case "--user": 1626 userId = UserHandle.parseUserArg(getNextArgRequired()); 1627 break; 1628 default: 1629 pw.println("Error: Unknown option: " + opt); 1630 return 1; 1631 } 1632 } 1633 final String packageName = getNextArgRequired(); 1634 1635 synchronized (mLock) { 1636 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); 1637 saveUserLocked(userId); 1638 } 1639 1640 return 0; 1641 } 1642 1643 private int handleOverrideConfig() { 1644 final PrintWriter pw = getOutPrintWriter(); 1645 final String config = getNextArgRequired(); 1646 1647 synchronized (mLock) { 1648 if (!updateConfigurationLocked(config)) { 1649 pw.println("override-config failed. See logcat for details."); 1650 return 1; 1651 } 1652 } 1653 return 0; 1654 } 1655 1656 private int handleResetConfig() { 1657 synchronized (mLock) { 1658 loadConfigurationLocked(); 1659 } 1660 return 0; 1661 } 1662 } 1663 1664 // === Unit test support === 1665 1666 // Injection point. 1667 long injectCurrentTimeMillis() { 1668 return System.currentTimeMillis(); 1669 } 1670 1671 // Injection point. 1672 int injectBinderCallingUid() { 1673 return getCallingUid(); 1674 } 1675 1676 File injectSystemDataPath() { 1677 return Environment.getDataSystemDirectory(); 1678 } 1679 1680 File injectUserDataPath(@UserIdInt int userId) { 1681 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER); 1682 } 1683 1684 @VisibleForTesting 1685 boolean injectIsLowRamDevice() { 1686 return ActivityManager.isLowRamDeviceStatic(); 1687 } 1688 1689 File getUserBitmapFilePath(@UserIdInt int userId) { 1690 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS); 1691 } 1692 1693 @VisibleForTesting 1694 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { 1695 return mShortcuts; 1696 } 1697 1698 @VisibleForTesting 1699 int getMaxDynamicShortcutsForTest() { 1700 return mMaxDynamicShortcuts; 1701 } 1702 1703 @VisibleForTesting 1704 int getMaxDailyUpdatesForTest() { 1705 return mMaxDailyUpdates; 1706 } 1707 1708 @VisibleForTesting 1709 long getResetIntervalForTest() { 1710 return mResetInterval; 1711 } 1712 1713 @VisibleForTesting 1714 int getMaxIconDimensionForTest() { 1715 return mMaxIconDimension; 1716 } 1717 1718 @VisibleForTesting 1719 CompressFormat getIconPersistFormatForTest() { 1720 return mIconPersistFormat; 1721 } 1722 1723 @VisibleForTesting 1724 int getIconPersistQualityForTest() { 1725 return mIconPersistQuality; 1726 } 1727 1728 @VisibleForTesting 1729 ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) { 1730 synchronized (mLock) { 1731 return getPackageShortcutsLocked(packageName, userId).findShortcutById(shortcutId); 1732 } 1733 } 1734} 1735 1736/** 1737 * All the information relevant to shortcuts from a single package (per-user). 1738 */ 1739class PackageShortcuts { 1740 private static final String TAG = ShortcutService.TAG; 1741 1742 @UserIdInt 1743 final int mUserId; 1744 1745 @NonNull 1746 final String mPackageName; 1747 1748 /** 1749 * All the shortcuts from the package, keyed on IDs. 1750 */ 1751 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 1752 1753 /** 1754 * # of dynamic shortcuts. 1755 */ 1756 private int mDynamicShortcutCount = 0; 1757 1758 /** 1759 * # of times the package has called rate-limited APIs. 1760 */ 1761 private int mApiCallCount; 1762 1763 /** 1764 * When {@link #mApiCallCount} was reset last time. 1765 */ 1766 private long mLastResetTime; 1767 1768 PackageShortcuts(int userId, String packageName) { 1769 mUserId = userId; 1770 mPackageName = packageName; 1771 } 1772 1773 @GuardedBy("mLock") 1774 @Nullable 1775 public ShortcutInfo findShortcutById(String id) { 1776 return mShortcuts.get(id); 1777 } 1778 1779 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s, 1780 @NonNull String id) { 1781 final ShortcutInfo shortcut = mShortcuts.remove(id); 1782 if (shortcut != null) { 1783 s.removeIcon(mUserId, shortcut); 1784 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED); 1785 } 1786 return shortcut; 1787 } 1788 1789 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) { 1790 deleteShortcut(s, newShortcut.getId()); 1791 s.saveIconAndFixUpShortcut(mUserId, newShortcut); 1792 mShortcuts.put(newShortcut.getId(), newShortcut); 1793 } 1794 1795 /** 1796 * Add a shortcut, or update one with the same ID, with taking over existing flags. 1797 * 1798 * It checks the max number of dynamic shortcuts. 1799 */ 1800 @GuardedBy("mLock") 1801 public void updateShortcutWithCapping(@NonNull ShortcutService s, 1802 @NonNull ShortcutInfo newShortcut) { 1803 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 1804 1805 int oldFlags = 0; 1806 int newDynamicCount = mDynamicShortcutCount; 1807 1808 if (oldShortcut != null) { 1809 oldFlags = oldShortcut.getFlags(); 1810 if (oldShortcut.isDynamic()) { 1811 newDynamicCount--; 1812 } 1813 } 1814 if (newShortcut.isDynamic()) { 1815 newDynamicCount++; 1816 } 1817 // Make sure there's still room. 1818 s.enforceMaxDynamicShortcuts(newDynamicCount); 1819 1820 // Okay, make it dynamic and add. 1821 newShortcut.addFlags(oldFlags); 1822 1823 addShortcut(s, newShortcut); 1824 mDynamicShortcutCount = newDynamicCount; 1825 } 1826 1827 /** 1828 * Remove all shortcuts that aren't pinned nor dynamic. 1829 */ 1830 private void removeOrphans(@NonNull ShortcutService s) { 1831 ArrayList<String> removeList = null; // Lazily initialize. 1832 1833 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1834 final ShortcutInfo si = mShortcuts.valueAt(i); 1835 1836 if (si.isPinned() || si.isDynamic()) continue; 1837 1838 if (removeList == null) { 1839 removeList = new ArrayList<>(); 1840 } 1841 removeList.add(si.getId()); 1842 } 1843 if (removeList != null) { 1844 for (int i = removeList.size() - 1 ; i >= 0; i--) { 1845 deleteShortcut(s, removeList.get(i)); 1846 } 1847 } 1848 } 1849 1850 @GuardedBy("mLock") 1851 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) { 1852 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1853 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC); 1854 } 1855 removeOrphans(s); 1856 mDynamicShortcutCount = 0; 1857 } 1858 1859 @GuardedBy("mLock") 1860 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) { 1861 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 1862 1863 if (oldShortcut == null) { 1864 return; 1865 } 1866 if (oldShortcut.isDynamic()) { 1867 mDynamicShortcutCount--; 1868 } 1869 if (oldShortcut.isPinned()) { 1870 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 1871 } else { 1872 deleteShortcut(s, shortcutId); 1873 } 1874 } 1875 1876 @GuardedBy("mLock") 1877 public void replacePinned(@NonNull ShortcutService s, String launcherPackage, 1878 List<String> shortcutIds) { 1879 1880 // TODO Should be per launcherPackage. 1881 1882 // First, un-pin all shortcuts 1883 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 1884 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED); 1885 } 1886 1887 // Then pin ALL 1888 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 1889 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i)); 1890 if (shortcut != null) { 1891 shortcut.addFlags(ShortcutInfo.FLAG_PINNED); 1892 } 1893 } 1894 1895 removeOrphans(s); 1896 } 1897 1898 /** 1899 * Number of calls that the caller has made, since the last reset. 1900 */ 1901 @GuardedBy("mLock") 1902 public int getApiCallCount(@NonNull ShortcutService s) { 1903 final long last = s.getLastResetTimeLocked(); 1904 1905 final long now = s.injectCurrentTimeMillis(); 1906 if (mLastResetTime > now) { 1907 // Clock rewound. // TODO Test it 1908 mLastResetTime = now; 1909 } 1910 1911 // If not reset yet, then reset. 1912 if (mLastResetTime < last) { 1913 mApiCallCount = 0; 1914 mLastResetTime = last; 1915 } 1916 return mApiCallCount; 1917 } 1918 1919 /** 1920 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount} 1921 * and return true. Otherwise just return false. 1922 */ 1923 @GuardedBy("mLock") 1924 public boolean tryApiCall(@NonNull ShortcutService s) { 1925 if (getApiCallCount(s) >= s.mMaxDailyUpdates) { 1926 return false; 1927 } 1928 mApiCallCount++; 1929 return true; 1930 } 1931 1932 @GuardedBy("mLock") 1933 public void resetRateLimitingForCommandLine() { 1934 mApiCallCount = 0; 1935 mLastResetTime = 0; 1936 } 1937 1938 /** 1939 * Find all shortcuts that match {@code query}. 1940 */ 1941 @GuardedBy("mLock") 1942 public void findAll(@NonNull List<ShortcutInfo> result, 1943 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 1944 for (int i = 0; i < mShortcuts.size(); i++) { 1945 final ShortcutInfo si = mShortcuts.valueAt(i); 1946 if (query == null || query.test(si)) { 1947 result.add(si.clone(cloneFlag)); 1948 } 1949 } 1950 } 1951 1952 public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) { 1953 pw.print(prefix); 1954 pw.print("Package: "); 1955 pw.print(mPackageName); 1956 pw.println(); 1957 1958 pw.print(prefix); 1959 pw.print(" "); 1960 pw.print("Calls: "); 1961 pw.print(getApiCallCount(s)); 1962 pw.println(); 1963 1964 // This should be after getApiCallCount(), which may update it. 1965 pw.print(prefix); 1966 pw.print(" "); 1967 pw.print("Last reset: ["); 1968 pw.print(mLastResetTime); 1969 pw.print("] "); 1970 pw.print(s.formatTime(mLastResetTime)); 1971 pw.println(); 1972 1973 pw.println(" Shortcuts:"); 1974 long totalBitmapSize = 0; 1975 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts; 1976 final int size = shortcuts.size(); 1977 for (int i = 0; i < size; i++) { 1978 final ShortcutInfo si = shortcuts.valueAt(i); 1979 pw.print(" "); 1980 pw.println(si.toInsecureString()); 1981 if (si.hasIconFile()) { 1982 final long len = new File(si.getBitmapPath()).length(); 1983 pw.print(" "); 1984 pw.print("bitmap size="); 1985 pw.println(len); 1986 1987 totalBitmapSize += len; 1988 } 1989 } 1990 pw.print(prefix); 1991 pw.print(" "); 1992 pw.print("Total bitmap size: "); 1993 pw.print(totalBitmapSize); 1994 pw.print(" ("); 1995 pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize)); 1996 pw.println(")"); 1997 } 1998 1999 public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException { 2000 out.startTag(null, ShortcutService.TAG_PACKAGE); 2001 2002 ShortcutService.writeAttr(out, ShortcutService.ATTR_NAME, mPackageName); 2003 ShortcutService.writeAttr(out, ShortcutService.ATTR_DYNAMIC_COUNT, mDynamicShortcutCount); 2004 ShortcutService.writeAttr(out, ShortcutService.ATTR_CALL_COUNT, mApiCallCount); 2005 ShortcutService.writeAttr(out, ShortcutService.ATTR_LAST_RESET, mLastResetTime); 2006 2007 final int size = mShortcuts.size(); 2008 for (int j = 0; j < size; j++) { 2009 saveShortcut(out, mShortcuts.valueAt(j)); 2010 } 2011 2012 out.endTag(null, ShortcutService.TAG_PACKAGE); 2013 } 2014 2015 private static void saveShortcut(XmlSerializer out, ShortcutInfo si) 2016 throws IOException, XmlPullParserException { 2017 out.startTag(null, ShortcutService.TAG_SHORTCUT); 2018 ShortcutService.writeAttr(out, ShortcutService.ATTR_ID, si.getId()); 2019 // writeAttr(out, "package", si.getPackageName()); // not needed 2020 ShortcutService.writeAttr(out, ShortcutService.ATTR_ACTIVITY, si.getActivityComponent()); 2021 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 2022 ShortcutService.writeAttr(out, ShortcutService.ATTR_TITLE, si.getTitle()); 2023 ShortcutService.writeAttr(out, ShortcutService.ATTR_INTENT, si.getIntentNoExtras()); 2024 ShortcutService.writeAttr(out, ShortcutService.ATTR_WEIGHT, si.getWeight()); 2025 ShortcutService.writeAttr(out, ShortcutService.ATTR_TIMESTAMP, 2026 si.getLastChangedTimestamp()); 2027 ShortcutService.writeAttr(out, ShortcutService.ATTR_FLAGS, si.getFlags()); 2028 ShortcutService.writeAttr(out, ShortcutService.ATTR_ICON_RES, si.getIconResourceId()); 2029 ShortcutService.writeAttr(out, ShortcutService.ATTR_BITMAP_PATH, si.getBitmapPath()); 2030 2031 ShortcutService.writeTagExtra(out, ShortcutService.TAG_INTENT_EXTRAS, 2032 si.getIntentPersistableExtras()); 2033 ShortcutService.writeTagExtra(out, ShortcutService.TAG_EXTRAS, si.getExtras()); 2034 2035 out.endTag(null, ShortcutService.TAG_SHORTCUT); 2036 } 2037 2038 public static PackageShortcuts loadFromXml(XmlPullParser parser, int userId) 2039 throws IOException, XmlPullParserException { 2040 2041 final String packageName = ShortcutService.parseStringAttribute(parser, 2042 ShortcutService.ATTR_NAME); 2043 2044 final PackageShortcuts ret = new PackageShortcuts(userId, packageName); 2045 2046 ret.mDynamicShortcutCount = 2047 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_DYNAMIC_COUNT); 2048 ret.mApiCallCount = 2049 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_CALL_COUNT); 2050 ret.mLastResetTime = 2051 ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_LAST_RESET); 2052 2053 final int outerDepth = parser.getDepth(); 2054 int type; 2055 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 2056 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 2057 if (type != XmlPullParser.START_TAG) { 2058 continue; 2059 } 2060 final int depth = parser.getDepth(); 2061 final String tag = parser.getName(); 2062 switch (tag) { 2063 case ShortcutService.TAG_SHORTCUT: 2064 final ShortcutInfo si = parseShortcut(parser, packageName); 2065 2066 // Don't use addShortcut(), we don't need to save the icon. 2067 ret.mShortcuts.put(si.getId(), si); 2068 continue; 2069 } 2070 throw ShortcutService.throwForInvalidTag(depth, tag); 2071 } 2072 return ret; 2073 } 2074 2075 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName) 2076 throws IOException, XmlPullParserException { 2077 String id; 2078 ComponentName activityComponent; 2079 // Icon icon; 2080 String title; 2081 Intent intent; 2082 PersistableBundle intentPersistableExtras = null; 2083 int weight; 2084 PersistableBundle extras = null; 2085 long lastChangedTimestamp; 2086 int flags; 2087 int iconRes; 2088 String bitmapPath; 2089 2090 id = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_ID); 2091 activityComponent = ShortcutService.parseComponentNameAttribute(parser, 2092 ShortcutService.ATTR_ACTIVITY); 2093 title = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_TITLE); 2094 intent = ShortcutService.parseIntentAttribute(parser, ShortcutService.ATTR_INTENT); 2095 weight = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_WEIGHT); 2096 lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser, 2097 ShortcutService.ATTR_TIMESTAMP); 2098 flags = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_FLAGS); 2099 iconRes = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_ICON_RES); 2100 bitmapPath = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_BITMAP_PATH); 2101 2102 final int outerDepth = parser.getDepth(); 2103 int type; 2104 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 2105 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 2106 if (type != XmlPullParser.START_TAG) { 2107 continue; 2108 } 2109 final int depth = parser.getDepth(); 2110 final String tag = parser.getName(); 2111 if (ShortcutService.DEBUG_LOAD) { 2112 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 2113 depth, type, tag)); 2114 } 2115 switch (tag) { 2116 case ShortcutService.TAG_INTENT_EXTRAS: 2117 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 2118 continue; 2119 case ShortcutService.TAG_EXTRAS: 2120 extras = PersistableBundle.restoreFromXml(parser); 2121 continue; 2122 } 2123 throw ShortcutService.throwForInvalidTag(depth, tag); 2124 } 2125 return new ShortcutInfo( 2126 id, packageName, activityComponent, /* icon =*/ null, title, intent, 2127 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 2128 iconRes, bitmapPath); 2129 } 2130} 2131