ShortcutService.java revision 6f7362d92573e4ae693bc513dca586d6a4eb087b
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.content.ComponentName; 22import android.content.Context; 23import android.content.Intent; 24import android.content.pm.IShortcutService; 25import android.content.pm.LauncherApps; 26import android.content.pm.LauncherApps.ShortcutQuery; 27import android.content.pm.PackageManager; 28import android.content.pm.PackageManager.NameNotFoundException; 29import android.content.pm.ParceledListSlice; 30import android.content.pm.ShortcutInfo; 31import android.content.pm.ShortcutServiceInternal; 32import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; 33import android.graphics.drawable.Icon; 34import android.os.Binder; 35import android.os.Bundle; 36import android.os.Environment; 37import android.os.Handler; 38import android.os.PersistableBundle; 39import android.os.Process; 40import android.os.RemoteException; 41import android.os.ResultReceiver; 42import android.os.ShellCommand; 43import android.os.UserHandle; 44import android.text.TextUtils; 45import android.text.format.Time; 46import android.util.ArrayMap; 47import android.util.ArraySet; 48import android.util.AtomicFile; 49import android.util.Slog; 50import android.util.SparseArray; 51import android.util.Xml; 52 53import com.android.internal.annotations.GuardedBy; 54import com.android.internal.annotations.VisibleForTesting; 55import com.android.internal.os.BackgroundThread; 56import com.android.internal.util.FastXmlSerializer; 57import com.android.internal.util.Preconditions; 58import com.android.server.LocalServices; 59import com.android.server.SystemService; 60 61import libcore.io.IoUtils; 62 63import org.xmlpull.v1.XmlPullParser; 64import org.xmlpull.v1.XmlPullParserException; 65import org.xmlpull.v1.XmlSerializer; 66 67import java.io.File; 68import java.io.FileDescriptor; 69import java.io.FileInputStream; 70import java.io.FileNotFoundException; 71import java.io.FileOutputStream; 72import java.io.IOException; 73import java.io.PrintWriter; 74import java.net.URISyntaxException; 75import java.nio.charset.StandardCharsets; 76import java.util.ArrayList; 77import java.util.List; 78import java.util.function.Predicate; 79 80/** 81 * TODO: 82 * - Make save async 83 * 84 * - Add Bitmap support 85 * 86 * - Implement updateShortcuts 87 * 88 * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res 89 * 90 * - Pinned per each launcher package (multiple launchers) 91 * 92 * - Dev option to reset all counts for QA (for now use "adb shell cmd shortcut reset-throttling") 93 * 94 * - Load config from settings 95 */ 96public class ShortcutService extends IShortcutService.Stub { 97 private static final String TAG = "ShortcutService"; 98 99 private static final boolean DEBUG = true; // STOPSHIP if true 100 private static final boolean DEBUG_LOAD = true; // STOPSHIP if true 101 102 private static final int DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day 103 private static final int DEFAULT_MAX_DAILY_UPDATES = 10; 104 private static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5; 105 106 private static final int SAVE_DELAY_MS = 5000; // in milliseconds. 107 108 @VisibleForTesting 109 static final String FILENAME_BASE_STATE = "shortcut_service.xml"; 110 111 @VisibleForTesting 112 static final String DIRECTORY_PER_USER = "shortcut_service"; 113 114 @VisibleForTesting 115 static final String FILENAME_USER_PACKAGES = "shortcuts.xml"; 116 117 private static final String DIRECTORY_BITMAPS = "bitmaps"; 118 119 private static final String TAG_ROOT = "root"; 120 private static final String TAG_LAST_RESET_TIME = "last_reset_time"; 121 private static final String ATTR_VALUE = "value"; 122 123 private final Context mContext; 124 125 private final Object mLock = new Object(); 126 127 private final Handler mHandler; 128 129 @GuardedBy("mLock") 130 private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1); 131 132 @GuardedBy("mLock") 133 private long mRawLastResetTime; 134 135 /** 136 * All the information relevant to shortcuts from a single package (per-user). 137 * 138 * TODO Move the persisting code to this class. 139 */ 140 private static class PackageShortcuts { 141 /** 142 * All the shortcuts from the package, keyed on IDs. 143 */ 144 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); 145 146 /** 147 * # of dynamic shortcuts. 148 */ 149 private int mDynamicShortcutCount = 0; 150 151 /** 152 * # of times the package has called rate-limited APIs. 153 */ 154 private int mApiCallCountInner; 155 156 /** 157 * When {@link #mApiCallCountInner} was reset last time. 158 */ 159 private long mLastResetTime; 160 161 /** 162 * @return the all shortcuts. Note DO NOT add/remove or touch the flags of the result 163 * directly, which would cause {@link #mDynamicShortcutCount} to be out of sync. 164 */ 165 @GuardedBy("mLock") 166 public ArrayMap<String, ShortcutInfo> getShortcuts() { 167 return mShortcuts; 168 } 169 170 /** 171 * Add a shortcut, or update one with the same ID, with taking over existing flags. 172 * 173 * It checks the max number of dynamic shortcuts. 174 */ 175 @GuardedBy("mLock") 176 public void updateShortcutWithCapping(@NonNull ShortcutService s, 177 @NonNull ShortcutInfo newShortcut) { 178 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); 179 180 int oldFlags = 0; 181 int newDynamicCount = mDynamicShortcutCount; 182 183 if (oldShortcut != null) { 184 oldFlags = oldShortcut.getFlags(); 185 if (oldShortcut.isDynamic()) { 186 newDynamicCount--; 187 } 188 } 189 if (newShortcut.isDynamic()) { 190 newDynamicCount++; 191 } 192 // Make sure there's still room. 193 s.enforceMaxDynamicShortcuts(newDynamicCount); 194 195 // Okay, make it dynamic and add. 196 newShortcut.addFlags(oldFlags); 197 198 mShortcuts.put(newShortcut.getId(), newShortcut); 199 mDynamicShortcutCount = newDynamicCount; 200 } 201 202 @GuardedBy("mLock") 203 public void deleteAllDynamicShortcuts() { 204 ArrayList<String> removeList = null; // Lazily initialize. 205 206 for (int i = mShortcuts.size() - 1; i >= 0; i--) { 207 final ShortcutInfo si = mShortcuts.valueAt(i); 208 209 if (!si.isDynamic()) { 210 continue; 211 } 212 if (si.isPinned()) { 213 // Still pinned, so don't remove; just make it non-dynamic. 214 si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 215 } else { 216 if (removeList == null) { 217 removeList = new ArrayList<>(); 218 } 219 removeList.add(si.getId()); 220 } 221 } 222 if (removeList != null) { 223 for (int i = removeList.size() - 1 ; i >= 0; i--) { 224 mShortcuts.remove(removeList.get(i)); 225 } 226 } 227 mDynamicShortcutCount = 0; 228 } 229 230 @GuardedBy("mLock") 231 public void deleteDynamicWithId(@NonNull String shortcutId) { 232 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); 233 234 if (oldShortcut == null) { 235 return; 236 } 237 if (oldShortcut.isDynamic()) { 238 mDynamicShortcutCount--; 239 } 240 if (oldShortcut.isPinned()) { 241 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); 242 } else { 243 mShortcuts.remove(shortcutId); 244 } 245 } 246 247 @GuardedBy("mLock") 248 public void pinAll(List<String> shortcutIds) { 249 for (int i = shortcutIds.size() - 1; i >= 0; i--) { 250 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i)); 251 if (shortcut != null) { 252 shortcut.addFlags(ShortcutInfo.FLAG_PINNED); 253 } 254 } 255 } 256 257 /** 258 * Number of calls that the caller has made, since the last reset. 259 */ 260 @GuardedBy("mLock") 261 public int getApiCallCount(@NonNull ShortcutService s) { 262 final long last = s.getLastResetTimeLocked(); 263 264 // If not reset yet, then reset. 265 if (mLastResetTime < last) { 266 mApiCallCountInner = 0; 267 mLastResetTime = last; 268 } 269 return mApiCallCountInner; 270 } 271 272 /** 273 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCountInner} 274 * and return true. Otherwise just return false. 275 */ 276 @GuardedBy("mLock") 277 public boolean tryApiCall(@NonNull ShortcutService s) { 278 if (getApiCallCount(s) >= s.mMaxDailyUpdates) { 279 return false; 280 } 281 mApiCallCountInner++; 282 return true; 283 } 284 285 @GuardedBy("mLock") 286 public void resetRateLimitingForCommandLine() { 287 mApiCallCountInner = 0; 288 mLastResetTime = 0; 289 } 290 291 /** 292 * Find all shortcuts that match {@code query}. 293 */ 294 @GuardedBy("mLock") 295 public void findAll(@NonNull List<ShortcutInfo> result, 296 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) { 297 for (int i = 0; i < mShortcuts.size(); i++) { 298 final ShortcutInfo si = mShortcuts.valueAt(i); 299 if (query == null || query.test(si)) { 300 result.add(si.clone(cloneFlag)); 301 } 302 } 303 } 304 } 305 306 /** 307 * User ID -> package name -> list of ShortcutInfos. 308 */ 309 @GuardedBy("mLock") 310 private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts = 311 new SparseArray<>(); 312 313 /** 314 * Max number of dynamic shortcuts that each application can have at a time. 315 */ 316 @GuardedBy("mLock") 317 private int mMaxDynamicShortcuts; 318 319 /** 320 * Max number of updating API calls that each application can make a day. 321 */ 322 @GuardedBy("mLock") 323 private int mMaxDailyUpdates; 324 325 /** 326 * Actual throttling-reset interval. By default it's a day. 327 */ 328 @GuardedBy("mLock") 329 private long mResetInterval; 330 331 public ShortcutService(Context context) { 332 mContext = Preconditions.checkNotNull(context); 333 LocalServices.addService(ShortcutServiceInternal.class, new LocalService()); 334 mHandler = new Handler(BackgroundThread.get().getLooper()); 335 } 336 337 /** 338 * System service lifecycle. 339 */ 340 public static final class Lifecycle extends SystemService { 341 final ShortcutService mService; 342 343 public Lifecycle(Context context) { 344 super(context); 345 mService = new ShortcutService(context); 346 } 347 348 @Override 349 public void onStart() { 350 publishBinderService(Context.SHORTCUT_SERVICE, mService); 351 } 352 353 @Override 354 public void onBootPhase(int phase) { 355 mService.onBootPhase(phase); 356 } 357 358 @Override 359 public void onCleanupUser(int userHandle) { 360 synchronized (mService.mLock) { 361 mService.onCleanupUserInner(userHandle); 362 } 363 } 364 365 @Override 366 public void onStartUser(int userId) { 367 synchronized (mService.mLock) { 368 mService.onStartUserLocked(userId); 369 } 370 } 371 } 372 373 /** lifecycle event */ 374 void onBootPhase(int phase) { 375 if (DEBUG) { 376 Slog.d(TAG, "onBootPhase: " + phase); 377 } 378 switch (phase) { 379 case SystemService.PHASE_LOCK_SETTINGS_READY: 380 initialize(); 381 break; 382 } 383 } 384 385 /** lifecycle event */ 386 void onStartUserLocked(int userId) { 387 // Preload 388 getUserShortcutsLocked(userId); 389 } 390 391 /** lifecycle event */ 392 void onCleanupUserInner(int userId) { 393 // Unload 394 mShortcuts.delete(userId); 395 } 396 397 /** Return the base state file name */ 398 private AtomicFile getBaseStateFile() { 399 final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE); 400 path.mkdirs(); 401 return new AtomicFile(path); 402 } 403 404 /** 405 * Init the instance. (load the state file, etc) 406 */ 407 private void initialize() { 408 synchronized (mLock) { 409 injectLoadConfigurationLocked(); 410 loadBaseStateLocked(); 411 } 412 } 413 414 // Test overrides it to inject different values. 415 @VisibleForTesting 416 void injectLoadConfigurationLocked() { 417 mResetInterval = DEFAULT_RESET_INTERVAL_SEC * 1000L; 418 mMaxDailyUpdates = DEFAULT_MAX_DAILY_UPDATES; 419 mMaxDynamicShortcuts = DEFAULT_MAX_SHORTCUTS_PER_APP; 420 } 421 422 // === Persistings === 423 424 @Nullable 425 private String parseStringAttribute(XmlPullParser parser, String attribute) { 426 return parser.getAttributeValue(null, attribute); 427 } 428 429 private long parseLongAttribute(XmlPullParser parser, String attribute) { 430 final String value = parseStringAttribute(parser, attribute); 431 if (TextUtils.isEmpty(value)) { 432 return 0; 433 } 434 try { 435 return Long.parseLong(value); 436 } catch (NumberFormatException e) { 437 Slog.e(TAG, "Error parsing long " + value); 438 return 0; 439 } 440 } 441 442 @Nullable 443 private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) { 444 final String value = parseStringAttribute(parser, attribute); 445 if (TextUtils.isEmpty(value)) { 446 return null; 447 } 448 return ComponentName.unflattenFromString(value); 449 } 450 451 @Nullable 452 private Intent parseIntentAttribute(XmlPullParser parser, String attribute) { 453 final String value = parseStringAttribute(parser, attribute); 454 if (TextUtils.isEmpty(value)) { 455 return null; 456 } 457 try { 458 return Intent.parseUri(value, /* flags =*/ 0); 459 } catch (URISyntaxException e) { 460 Slog.e(TAG, "Error parsing intent", e); 461 return null; 462 } 463 } 464 465 private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException { 466 if (TextUtils.isEmpty(value)) return; 467 468 out.startTag(null, tag); 469 out.attribute(null, ATTR_VALUE, value); 470 out.endTag(null, tag); 471 } 472 473 private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException { 474 writeTagValue(out, tag, Long.toString(value)); 475 } 476 477 private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle) 478 throws IOException, XmlPullParserException { 479 if (bundle == null) return; 480 481 out.startTag(null, tag); 482 bundle.saveToXml(out); 483 out.endTag(null, tag); 484 } 485 486 private void writeAttr(XmlSerializer out, String name, String value) throws IOException { 487 if (TextUtils.isEmpty(value)) return; 488 489 out.attribute(null, name, value); 490 } 491 492 private void writeAttr(XmlSerializer out, String name, long value) throws IOException { 493 writeAttr(out, name, String.valueOf(value)); 494 } 495 496 private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException { 497 if (comp == null) return; 498 writeAttr(out, name, comp.flattenToString()); 499 } 500 501 private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException { 502 if (intent == null) return; 503 504 writeAttr(out, name, intent.toUri(/* flags =*/ 0)); 505 } 506 507 @VisibleForTesting 508 void saveBaseStateLocked() { 509 final AtomicFile file = getBaseStateFile(); 510 if (DEBUG) { 511 Slog.i(TAG, "Saving to " + file.getBaseFile()); 512 } 513 514 FileOutputStream outs = null; 515 try { 516 outs = file.startWrite(); 517 518 // Write to XML 519 XmlSerializer out = new FastXmlSerializer(); 520 out.setOutput(outs, StandardCharsets.UTF_8.name()); 521 out.startDocument(null, true); 522 out.startTag(null, TAG_ROOT); 523 524 // Body. 525 writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime); 526 527 // Epilogue. 528 out.endTag(null, TAG_ROOT); 529 out.endDocument(); 530 531 // Close. 532 file.finishWrite(outs); 533 } catch (IOException e) { 534 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 535 file.failWrite(outs); 536 } 537 } 538 539 private void loadBaseStateLocked() { 540 mRawLastResetTime = 0; 541 542 final AtomicFile file = getBaseStateFile(); 543 if (DEBUG) { 544 Slog.i(TAG, "Loading from " + file.getBaseFile()); 545 } 546 try (FileInputStream in = file.openRead()) { 547 XmlPullParser parser = Xml.newPullParser(); 548 parser.setInput(in, StandardCharsets.UTF_8.name()); 549 550 int type; 551 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 552 if (type != XmlPullParser.START_TAG) { 553 continue; 554 } 555 final int depth = parser.getDepth(); 556 // Check the root tag 557 final String tag = parser.getName(); 558 if (depth == 1) { 559 if (!TAG_ROOT.equals(tag)) { 560 Slog.e(TAG, "Invalid root tag: " + tag); 561 return; 562 } 563 continue; 564 } 565 // Assume depth == 2 566 switch (tag) { 567 case TAG_LAST_RESET_TIME: 568 mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE); 569 break; 570 default: 571 Slog.e(TAG, "Invalid tag: " + tag); 572 break; 573 } 574 } 575 } catch (FileNotFoundException e) { 576 // Use the default 577 } catch (IOException|XmlPullParserException e) { 578 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 579 580 mRawLastResetTime = 0; 581 } 582 // Adjust the last reset time. 583 getLastResetTimeLocked(); 584 } 585 586 private void saveUserLocked(@UserIdInt int userId) { 587 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); 588 if (DEBUG) { 589 Slog.i(TAG, "Saving to " + path); 590 } 591 path.mkdirs(); 592 final AtomicFile file = new AtomicFile(path); 593 FileOutputStream outs = null; 594 try { 595 outs = file.startWrite(); 596 597 // Write to XML 598 XmlSerializer out = new FastXmlSerializer(); 599 out.setOutput(outs, StandardCharsets.UTF_8.name()); 600 out.startDocument(null, true); 601 out.startTag(null, TAG_ROOT); 602 603 final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId); 604 605 // Body. 606 for (int i = 0; i < packages.size(); i++) { 607 final String packageName = packages.keyAt(i); 608 final PackageShortcuts shortcuts = packages.valueAt(i); 609 610 // TODO Move this to PackageShortcuts. 611 612 out.startTag(null, "package"); 613 614 writeAttr(out, "name", packageName); 615 writeAttr(out, "dynamic-count", shortcuts.mDynamicShortcutCount); 616 writeAttr(out, "call-count", shortcuts.mApiCallCountInner); 617 writeAttr(out, "last-reset", shortcuts.mLastResetTime); 618 619 final int size = shortcuts.getShortcuts().size(); 620 for (int j = 0; j < size; j++) { 621 saveShortcut(out, shortcuts.getShortcuts().valueAt(j)); 622 } 623 624 out.endTag(null, "package"); 625 } 626 627 // Epilogue. 628 out.endTag(null, TAG_ROOT); 629 out.endDocument(); 630 631 // Close. 632 file.finishWrite(outs); 633 } catch (IOException|XmlPullParserException e) { 634 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); 635 file.failWrite(outs); 636 } 637 } 638 639 private void saveShortcut(XmlSerializer out, ShortcutInfo si) 640 throws IOException, XmlPullParserException { 641 out.startTag(null, "shortcut"); 642 writeAttr(out, "id", si.getId()); 643 // writeAttr(out, "package", si.getPackageName()); // not needed 644 writeAttr(out, "activity", si.getActivityComponent()); 645 // writeAttr(out, "icon", si.getIcon()); // We don't save it. 646 writeAttr(out, "title", si.getTitle()); 647 writeAttr(out, "intent", si.getIntent()); 648 writeAttr(out, "weight", si.getWeight()); 649 writeAttr(out, "timestamp", si.getLastChangedTimestamp()); 650 writeAttr(out, "flags", si.getFlags()); 651 writeAttr(out, "icon-res", si.getIconResourceId()); 652 writeAttr(out, "bitmap-path", si.getBitmapPath()); 653 654 writeTagExtra(out, "intent-extras", si.getIntentPersistableExtras()); 655 writeTagExtra(out, "extras", si.getExtras()); 656 657 out.endTag(null, "shortcut"); 658 } 659 660 private static IOException throwForInvalidTag(int depth, String tag) throws IOException { 661 throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth)); 662 } 663 664 @Nullable 665 private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) { 666 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES); 667 if (DEBUG) { 668 Slog.i(TAG, "Loading from " + path); 669 } 670 path.mkdirs(); 671 final AtomicFile file = new AtomicFile(path); 672 673 final FileInputStream in; 674 try { 675 in = file.openRead(); 676 } catch (FileNotFoundException e) { 677 if (DEBUG) { 678 Slog.i(TAG, "Not found " + path); 679 } 680 return null; 681 } 682 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>(); 683 try { 684 XmlPullParser parser = Xml.newPullParser(); 685 parser.setInput(in, StandardCharsets.UTF_8.name()); 686 687 String packageName = null; 688 PackageShortcuts shortcuts = null; 689 690 int type; 691 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 692 if (type != XmlPullParser.START_TAG) { 693 continue; 694 } 695 final int depth = parser.getDepth(); 696 697 // TODO Move some of this to PackageShortcuts. 698 699 final String tag = parser.getName(); 700 if (DEBUG_LOAD) { 701 Slog.d(TAG, String.format("depth=%d type=%d name=%s", 702 depth, type, tag)); 703 } 704 switch (depth) { 705 case 1: { 706 if (TAG_ROOT.equals(tag)) { 707 continue; 708 } 709 break; 710 } 711 case 2: { 712 switch (tag) { 713 case "package": 714 packageName = parseStringAttribute(parser, "name"); 715 shortcuts = new PackageShortcuts(); 716 ret.put(packageName, shortcuts); 717 718 shortcuts.mDynamicShortcutCount = 719 (int) parseLongAttribute(parser, "dynamic-count"); 720 shortcuts.mApiCallCountInner = 721 (int) parseLongAttribute(parser, "call-count"); 722 shortcuts.mLastResetTime = parseLongAttribute(parser, "last-reset"); 723 continue; 724 } 725 break; 726 } 727 case 3: { 728 switch (tag) { 729 case "shortcut": 730 final ShortcutInfo si = parseShortcut(parser, packageName); 731 shortcuts.mShortcuts.put(si.getId(), si); 732 continue; 733 } 734 break; 735 } 736 } 737 throwForInvalidTag(depth, tag); 738 } 739 return ret; 740 } catch (IOException|XmlPullParserException e) { 741 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); 742 return null; 743 } finally { 744 IoUtils.closeQuietly(in); 745 } 746 } 747 748 private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName) 749 throws IOException, XmlPullParserException { 750 String id; 751 ComponentName activityComponent; 752 Icon icon; 753 String title; 754 Intent intent; 755 PersistableBundle intentPersistableExtras = null; 756 int weight; 757 PersistableBundle extras = null; 758 long lastChangedTimestamp; 759 int flags; 760 int iconRes; 761 String bitmapPath; 762 763 id = parseStringAttribute(parser, "id"); 764 activityComponent = parseComponentNameAttribute(parser, "activity"); 765 title = parseStringAttribute(parser, "title"); 766 intent = parseIntentAttribute(parser, "intent"); 767 weight = (int) parseLongAttribute(parser, "weight"); 768 lastChangedTimestamp = (int) parseLongAttribute(parser, "timestamp"); 769 flags = (int) parseLongAttribute(parser, "flags"); 770 iconRes = (int) parseLongAttribute(parser, "icon-res"); 771 bitmapPath = parseStringAttribute(parser, "bitmap-path"); 772 773 final int outerDepth = parser.getDepth(); 774 int type; 775 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 776 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 777 if (type != XmlPullParser.START_TAG) { 778 continue; 779 } 780 final int depth = parser.getDepth(); 781 final String tag = parser.getName(); 782 if (DEBUG_LOAD) { 783 Slog.d(TAG, String.format(" depth=%d type=%d name=%s", 784 depth, type, tag)); 785 } 786 switch (tag) { 787 case "intent-extras": 788 intentPersistableExtras = PersistableBundle.restoreFromXml(parser); 789 continue; 790 case "extras": 791 extras = PersistableBundle.restoreFromXml(parser); 792 continue; 793 } 794 throw throwForInvalidTag(depth, tag); 795 } 796 return new ShortcutInfo( 797 id, packgeName, activityComponent, /* icon =*/ null, title, intent, 798 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags, 799 iconRes, bitmapPath); 800 } 801 802 // TODO Actually make it async. 803 private void scheduleSaveBaseState() { 804 synchronized (mLock) { 805 saveBaseStateLocked(); 806 } 807 } 808 809 // TODO Actually make it async. 810 private void scheduleSaveUser(@UserIdInt int userId) { 811 synchronized (mLock) { 812 saveUserLocked(userId); 813 } 814 } 815 816 /** Return the last reset time. */ 817 long getLastResetTimeLocked() { 818 updateTimes(); 819 return mRawLastResetTime; 820 } 821 822 /** Return the next reset time. */ 823 long getNextResetTimeLocked() { 824 updateTimes(); 825 return mRawLastResetTime + mResetInterval; 826 } 827 828 /** 829 * Update the last reset time. 830 */ 831 private void updateTimes() { 832 833 final long now = injectCurrentTimeMillis(); 834 835 final long prevLastResetTime = mRawLastResetTime; 836 837 if (mRawLastResetTime == 0) { // first launch. 838 // TODO Randomize?? 839 mRawLastResetTime = now; 840 } else if (now < mRawLastResetTime) { 841 // Clock rewound. 842 // TODO Randomize?? 843 mRawLastResetTime = now; 844 } else { 845 // TODO Do it properly. 846 while ((mRawLastResetTime + mResetInterval) <= now) { 847 mRawLastResetTime += mResetInterval; 848 } 849 } 850 if (prevLastResetTime != mRawLastResetTime) { 851 scheduleSaveBaseState(); 852 } 853 } 854 855 /** Return the per-user state. */ 856 @GuardedBy("mLock") 857 @NonNull 858 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) { 859 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId); 860 if (userPackages == null) { 861 userPackages = loadUserLocked(userId); 862 if (userPackages == null) { 863 userPackages = new ArrayMap<>(); 864 } 865 mShortcuts.put(userId, userPackages); 866 } 867 return userPackages; 868 } 869 870 /** Return the per-user per-package state. */ 871 @GuardedBy("mLock") 872 @NonNull 873 private PackageShortcuts getPackageShortcutsLocked( 874 @NonNull String packageName, @UserIdInt int userId) { 875 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId); 876 PackageShortcuts shortcuts = userPackages.get(packageName); 877 if (shortcuts == null) { 878 shortcuts = new PackageShortcuts(); 879 userPackages.put(packageName, shortcuts); 880 } 881 return shortcuts; 882 } 883 884 // === Caller validation === 885 886 private boolean isCallerSystem() { 887 final int callingUid = injectBinderCallingUid(); 888 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); 889 } 890 891 private boolean isCallerShell() { 892 final int callingUid = injectBinderCallingUid(); 893 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID; 894 } 895 896 private void enforceSystemOrShell() { 897 Preconditions.checkState(isCallerSystem() || isCallerShell(), 898 "Caller must be system or shell"); 899 } 900 901 private void enforceShell() { 902 Preconditions.checkState(isCallerShell(), "Caller must be shell"); 903 } 904 905 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { 906 Preconditions.checkStringNotEmpty(packageName, "packageName"); 907 908 if (isCallerSystem()) { 909 return; // no check 910 } 911 912 final int callingUid = injectBinderCallingUid(); 913 914 // Otherwise, make sure the arguments are valid. 915 if (UserHandle.getUserId(callingUid) != userId) { 916 throw new SecurityException("Invalid user-ID"); 917 } 918 verifyCallingPackage(packageName); 919 } 920 921 private void verifyCallingPackage(@NonNull String packageName) { 922 Preconditions.checkStringNotEmpty(packageName, "packageName"); 923 924 if (isCallerSystem()) { 925 return; // no check 926 } 927 928 if (injectGetPackageUid(packageName) == injectBinderCallingUid()) { 929 return; // Caller is valid. 930 } 931 throw new SecurityException("Caller UID= doesn't own " + packageName); 932 } 933 934 // Test overrides it. 935 int injectGetPackageUid(String packageName) { 936 try { 937 938 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info? 939 940 return mContext.getPackageManager().getPackageUid(packageName, 941 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE 942 | PackageManager.MATCH_UNINSTALLED_PACKAGES); 943 } catch (NameNotFoundException e) { 944 return -1; 945 } 946 } 947 948 /** 949 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. 950 */ 951 void enforceMaxDynamicShortcuts(int numShortcuts) { 952 if (numShortcuts > mMaxDynamicShortcuts) { 953 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); 954 } 955 } 956 957 /** 958 * - Sends a notification to LauncherApps 959 * - Write to file 960 */ 961 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) { 962 notifyListeners(packageName, userId); 963 scheduleSaveUser(userId); 964 } 965 966 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) { 967 final ArrayList<ShortcutChangeListener> copy; 968 final List<ShortcutInfo> shortcuts = new ArrayList<>(); 969 synchronized (mLock) { 970 copy = new ArrayList<>(mListeners); 971 972 getPackageShortcutsLocked(packageName, userId) 973 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); 974 } 975 for (int i = copy.size() - 1; i >= 0; i--) { 976 copy.get(i).onShortcutChanged(packageName, shortcuts, userId); 977 } 978 } 979 980 /** 981 * Clean up / validate an incoming shortcut. 982 * - Make sure all mandatory fields are set. 983 * - Make sure the intent's extras are persistable, and them to set 984 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras. 985 * - Clear flags. 986 */ 987 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut) { 988 Preconditions.checkNotNull(shortcut, "Null shortcut detected"); 989 if (shortcut.getActivityComponent() != null) { 990 Preconditions.checkState( 991 shortcut.getPackageName().equals( 992 shortcut.getActivityComponent().getPackageName()), 993 "Activity package name mismatch"); 994 } 995 996 shortcut.enforceMandatoryFields(); 997 998 final Intent intent = shortcut.getIntent(); 999 final Bundle intentExtras = intent.getExtras(); 1000 if (intentExtras != null && intentExtras.size() > 0) { 1001 intent.replaceExtras((Bundle) null); 1002 1003 // PersistableBundle's constructor will throw IllegalArgumentException if original 1004 // extras contain something not persistable. 1005 shortcut.setIntentPersistableExtras(new PersistableBundle(intentExtras)); 1006 } 1007 1008 // TODO Save the icon 1009 shortcut.setIcon(null); 1010 1011 shortcut.setFlags(0); 1012 } 1013 1014 // === APIs === 1015 1016 @Override 1017 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1018 @UserIdInt int userId) { 1019 verifyCaller(packageName, userId); 1020 1021 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1022 final int size = newShortcuts.size(); 1023 1024 synchronized (mLock) { 1025 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1026 1027 // Throttling. 1028 if (!ps.tryApiCall(this)) { 1029 return false; 1030 } 1031 enforceMaxDynamicShortcuts(size); 1032 1033 // Validate the shortcuts. 1034 for (int i = 0; i < size; i++) { 1035 fixUpIncomingShortcutInfo(newShortcuts.get(i)); 1036 } 1037 1038 // First, remove all un-pinned; dynamic shortcuts 1039 ps.deleteAllDynamicShortcuts(); 1040 1041 // Then, add/update all. We need to make sure to take over "pinned" flag. 1042 for (int i = 0; i < size; i++) { 1043 final ShortcutInfo newShortcut = newShortcuts.get(i); 1044 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1045 ps.updateShortcutWithCapping(this, newShortcut); 1046 } 1047 } 1048 userPackageChanged(packageName, userId); 1049 1050 return true; 1051 } 1052 1053 @Override 1054 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList, 1055 @UserIdInt int userId) { 1056 verifyCaller(packageName, userId); 1057 1058 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList(); 1059 1060 synchronized (mLock) { 1061 1062 if (true) { 1063 throw new RuntimeException("not implemented yet"); 1064 } 1065 1066 // TODO Similar to setDynamicShortcuts, but don't add new ones, and don't change flags. 1067 // Update non-null fields only. 1068 } 1069 userPackageChanged(packageName, userId); 1070 1071 return true; 1072 } 1073 1074 @Override 1075 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut, 1076 @UserIdInt int userId) { 1077 verifyCaller(packageName, userId); 1078 1079 synchronized (mLock) { 1080 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId); 1081 1082 // Throttling. 1083 if (!ps.tryApiCall(this)) { 1084 return false; 1085 } 1086 1087 // Validate the shortcut. 1088 fixUpIncomingShortcutInfo(newShortcut); 1089 1090 // Add it. 1091 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); 1092 ps.updateShortcutWithCapping(this, newShortcut); 1093 } 1094 userPackageChanged(packageName, userId); 1095 1096 return true; 1097 } 1098 1099 @Override 1100 public void deleteDynamicShortcut(String packageName, String shortcutId, 1101 @UserIdInt int userId) { 1102 verifyCaller(packageName, userId); 1103 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided"); 1104 1105 synchronized (mLock) { 1106 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(shortcutId); 1107 } 1108 userPackageChanged(packageName, userId); 1109 } 1110 1111 @Override 1112 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) { 1113 verifyCaller(packageName, userId); 1114 1115 synchronized (mLock) { 1116 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(); 1117 } 1118 userPackageChanged(packageName, userId); 1119 } 1120 1121 @Override 1122 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName, 1123 @UserIdInt int userId) { 1124 verifyCaller(packageName, userId); 1125 synchronized (mLock) { 1126 return getShortcutsWithQueryLocked( 1127 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1128 ShortcutInfo::isDynamic); 1129 } 1130 } 1131 1132 @Override 1133 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName, 1134 @UserIdInt int userId) { 1135 verifyCaller(packageName, userId); 1136 synchronized (mLock) { 1137 return getShortcutsWithQueryLocked( 1138 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, 1139 ShortcutInfo::isPinned); 1140 } 1141 } 1142 1143 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, 1144 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) { 1145 1146 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1147 1148 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags); 1149 1150 return new ParceledListSlice<>(ret); 1151 } 1152 1153 @Override 1154 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId) 1155 throws RemoteException { 1156 verifyCaller(packageName, userId); 1157 1158 return mMaxDynamicShortcuts; 1159 } 1160 1161 @Override 1162 public int getRemainingCallCount(String packageName, @UserIdInt int userId) { 1163 verifyCaller(packageName, userId); 1164 1165 synchronized (mLock) { 1166 return mMaxDailyUpdates 1167 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this); 1168 } 1169 } 1170 1171 @Override 1172 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { 1173 verifyCaller(packageName, userId); 1174 1175 synchronized (mLock) { 1176 return getNextResetTimeLocked(); 1177 } 1178 } 1179 1180 /** 1181 * Reset all throttling, for developer options and command line. Only system/shell can call it. 1182 */ 1183 @Override 1184 public void resetThrottling() { 1185 enforceSystemOrShell(); 1186 1187 resetThrottlingInner(); 1188 } 1189 1190 @VisibleForTesting 1191 void resetThrottlingInner() { 1192 synchronized (mLock) { 1193 mRawLastResetTime = injectCurrentTimeMillis(); 1194 } 1195 scheduleSaveBaseState(); 1196 } 1197 1198 /** 1199 * Entry point from {@link LauncherApps}. 1200 */ 1201 private class LocalService extends ShortcutServiceInternal { 1202 @Override 1203 public List<ShortcutInfo> getShortcuts( 1204 @NonNull String callingPackage, long changedSince, 1205 @Nullable String packageName, @Nullable ComponentName componentName, 1206 int queryFlags, int userId) { 1207 final ArrayList<ShortcutInfo> ret = new ArrayList<>(); 1208 final int cloneFlag = 1209 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0) 1210 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER 1211 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO; 1212 1213 synchronized (mLock) { 1214 if (packageName != null) { 1215 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags, 1216 userId, ret, cloneFlag); 1217 } else { 1218 final ArrayMap<String, PackageShortcuts> packages = 1219 getUserShortcutsLocked(userId); 1220 for (int i = 0; i < packages.size(); i++) { 1221 getShortcutsInnerLocked( 1222 packages.keyAt(i), 1223 changedSince, componentName, queryFlags, userId, ret, cloneFlag); 1224 } 1225 } 1226 } 1227 return ret; 1228 } 1229 1230 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince, 1231 @Nullable ComponentName componentName, int queryFlags, 1232 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) { 1233 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1234 (ShortcutInfo si) -> { 1235 if (si.getLastChangedTimestamp() < changedSince) { 1236 return false; 1237 } 1238 if (componentName != null 1239 && !componentName.equals(si.getActivityComponent())) { 1240 return false; 1241 } 1242 final boolean matchDynamic = 1243 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) 1244 && si.isDynamic(); 1245 final boolean matchPinned = 1246 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) 1247 && si.isPinned(); 1248 return matchDynamic || matchPinned; 1249 }, cloneFlag); 1250 } 1251 1252 @Override 1253 public List<ShortcutInfo> getShortcutInfo( 1254 @NonNull String callingPackage, 1255 @NonNull String packageName, @Nullable List<String> ids, int userId) { 1256 // Calling permission must be checked by LauncherAppsImpl. 1257 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1258 1259 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size()); 1260 final ArraySet<String> idSet = new ArraySet<>(ids); 1261 synchronized (mLock) { 1262 getPackageShortcutsLocked(packageName, userId).findAll(ret, 1263 (ShortcutInfo si) -> idSet.contains(si.getId()), 1264 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 1265 } 1266 return ret; 1267 } 1268 1269 @Override 1270 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName, 1271 @NonNull List<String> shortcutIds, int userId) { 1272 // Calling permission must be checked by LauncherAppsImpl. 1273 Preconditions.checkStringNotEmpty(packageName, "packageName"); 1274 Preconditions.checkNotNull(shortcutIds, "shortcutIds"); 1275 1276 synchronized (mLock) { 1277 getPackageShortcutsLocked(packageName, userId).pinAll(shortcutIds); 1278 } 1279 userPackageChanged(packageName, userId); 1280 } 1281 1282 @Override 1283 public Intent createShortcutIntent(@NonNull String callingPackage, 1284 @NonNull ShortcutInfo shortcut, int userId) { 1285 // Calling permission must be checked by LauncherAppsImpl. 1286 Preconditions.checkNotNull(shortcut, "shortcut"); 1287 1288 synchronized (mLock) { 1289 final ShortcutInfo fullShortcut = 1290 getPackageShortcutsLocked(shortcut.getPackageName(), userId) 1291 .getShortcuts().get(shortcut.getId()); 1292 if (fullShortcut == null) { 1293 return null; 1294 } else { 1295 final Intent intent = fullShortcut.getIntent(); 1296 final PersistableBundle extras = fullShortcut.getIntentPersistableExtras(); 1297 if (extras != null) { 1298 intent.replaceExtras(new Bundle(extras)); 1299 } 1300 1301 return intent; 1302 } 1303 } 1304 } 1305 1306 @Override 1307 public void addListener(@NonNull ShortcutChangeListener listener) { 1308 synchronized (mLock) { 1309 mListeners.add(Preconditions.checkNotNull(listener)); 1310 } 1311 } 1312 } 1313 1314 // === Dump === 1315 1316 @Override 1317 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1318 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) 1319 != PackageManager.PERMISSION_GRANTED) { 1320 pw.println("Permission Denial: can't dump UserManager from from pid=" 1321 + Binder.getCallingPid() 1322 + ", uid=" + Binder.getCallingUid() 1323 + " without permission " 1324 + android.Manifest.permission.DUMP); 1325 return; 1326 } 1327 dumpInner(pw); 1328 } 1329 1330 @VisibleForTesting 1331 void dumpInner(PrintWriter pw) { 1332 synchronized (mLock) { 1333 final long now = injectCurrentTimeMillis(); 1334 pw.print("Now: ["); 1335 pw.print(now); 1336 pw.print("] "); 1337 pw.print(formatTime(now)); 1338 pw.print(" Raw last reset: ["); 1339 pw.print(mRawLastResetTime); 1340 pw.print("] "); 1341 pw.print(formatTime(mRawLastResetTime)); 1342 1343 final long last = getLastResetTimeLocked(); 1344 final long next = getNextResetTimeLocked(); 1345 pw.print(" Last reset: ["); 1346 pw.print(last); 1347 pw.print("] "); 1348 pw.print(formatTime(last)); 1349 1350 pw.print(" Next reset: ["); 1351 pw.print(next); 1352 pw.print("] "); 1353 pw.print(formatTime(next)); 1354 pw.println(); 1355 1356 pw.println(); 1357 1358 for (int i = 0; i < mShortcuts.size(); i++) { 1359 dumpUserLocked(pw, mShortcuts.keyAt(i)); 1360 } 1361 1362 } 1363 } 1364 1365 private void dumpUserLocked(PrintWriter pw, int userId) { 1366 pw.print(" User: "); 1367 pw.print(userId); 1368 pw.println(); 1369 1370 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId); 1371 if (packages == null) { 1372 return; 1373 } 1374 for (int j = 0; j < packages.size(); j++) { 1375 dumpPackageLocked(pw, userId, packages.keyAt(j)); 1376 } 1377 pw.println(); 1378 } 1379 1380 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) { 1381 final PackageShortcuts shortcuts = mShortcuts.get(userId).get(packageName); 1382 if (shortcuts == null) { 1383 return; 1384 } 1385 1386 pw.print(" Package: "); 1387 pw.print(packageName); 1388 pw.println(); 1389 1390 pw.print(" Calls: "); 1391 pw.print(shortcuts.getApiCallCount(this)); 1392 pw.println(); 1393 1394 // This should be after getApiCallCount(), which may update it. 1395 pw.print(" Last reset: ["); 1396 pw.print(shortcuts.mLastResetTime); 1397 pw.print("] "); 1398 pw.print(formatTime(shortcuts.mLastResetTime)); 1399 pw.println(); 1400 1401 pw.println(" Shortcuts:"); 1402 final int size = shortcuts.getShortcuts().size(); 1403 for (int i = 0; i < size; i++) { 1404 pw.print(" "); 1405 pw.println(shortcuts.getShortcuts().valueAt(i).toInsecureString()); 1406 } 1407 } 1408 1409 private static String formatTime(long time) { 1410 Time tobj = new Time(); 1411 tobj.set(time); 1412 return tobj.format("%Y-%m-%d %H:%M:%S"); 1413 } 1414 1415 // === Shell support === 1416 1417 @Override 1418 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 1419 String[] args, ResultReceiver resultReceiver) throws RemoteException { 1420 1421 enforceShell(); 1422 1423 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver); 1424 } 1425 1426 /** 1427 * Handle "adb shell cmd". 1428 */ 1429 private class MyShellCommand extends ShellCommand { 1430 @Override 1431 public int onCommand(String cmd) { 1432 if (cmd == null) { 1433 return handleDefaultCommands(cmd); 1434 } 1435 final PrintWriter pw = getOutPrintWriter(); 1436 switch(cmd) { 1437 case "reset-package-throttling": 1438 return handleResetPackageThrottling(); 1439 case "reset-throttling": 1440 return handleResetThrottling(); 1441 default: 1442 return handleDefaultCommands(cmd); 1443 } 1444 } 1445 1446 @Override 1447 public void onHelp() { 1448 final PrintWriter pw = getOutPrintWriter(); 1449 pw.println("Usage: cmd shortcut COMMAND [options ...]"); 1450 pw.println(); 1451 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE"); 1452 pw.println(" Reset throttling for a package"); 1453 pw.println(); 1454 pw.println("cmd shortcut reset-throttling"); 1455 pw.println(" Reset throttling for all packages and users"); 1456 pw.println(); 1457 } 1458 1459 private int handleResetThrottling() { 1460 resetThrottling(); 1461 return 0; 1462 } 1463 1464 private int handleResetPackageThrottling() { 1465 final PrintWriter pw = getOutPrintWriter(); 1466 1467 int userId = UserHandle.USER_SYSTEM; 1468 String opt; 1469 while ((opt = getNextOption()) != null) { 1470 switch (opt) { 1471 case "--user": 1472 userId = UserHandle.parseUserArg(getNextArgRequired()); 1473 break; 1474 default: 1475 pw.println("Error: Unknown option: " + opt); 1476 return 1; 1477 } 1478 } 1479 final String packageName = getNextArgRequired(); 1480 1481 synchronized (mLock) { 1482 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine(); 1483 saveUserLocked(userId); 1484 } 1485 1486 return 0; 1487 } 1488 } 1489 1490 // === Unit test support === 1491 1492 // Injection point. 1493 long injectCurrentTimeMillis() { 1494 return System.currentTimeMillis(); 1495 } 1496 1497 // Injection point. 1498 int injectBinderCallingUid() { 1499 return getCallingUid(); 1500 } 1501 1502 File injectSystemDataPath() { 1503 return Environment.getDataSystemDirectory(); 1504 } 1505 1506 File injectUserDataPath(@UserIdInt int userId) { 1507 return new File(Environment.getDataSystemDeDirectory(userId), DIRECTORY_PER_USER); 1508 } 1509 1510 @VisibleForTesting 1511 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() { 1512 return mShortcuts; 1513 } 1514 1515 @VisibleForTesting 1516 void setMaxDynamicShortcutsForTest(int max) { 1517 mMaxDynamicShortcuts = max; 1518 } 1519 1520 @VisibleForTesting 1521 void setMaxDailyUpdatesForTest(int max) { 1522 mMaxDailyUpdates = max; 1523 } 1524 1525 @VisibleForTesting 1526 public void setResetIntervalForTest(long interval) { 1527 mResetInterval = interval; 1528 } 1529} 1530