ShortcutService.java revision 43204b8357d781f284037fb8b7b7050ed04a2103
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 String packageName, @NonNull String shortcutId, int userId) {
1285            // Calling permission must be checked by LauncherAppsImpl.
1286            Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1287            Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
1288
1289            synchronized (mLock) {
1290                final ShortcutInfo fullShortcut =
1291                        getPackageShortcutsLocked(packageName, userId)
1292                        .getShortcuts().get(shortcutId);
1293                if (fullShortcut == null) {
1294                    return null;
1295                } else {
1296                    final Intent intent = fullShortcut.getIntent();
1297                    final PersistableBundle extras = fullShortcut.getIntentPersistableExtras();
1298                    if (extras != null) {
1299                        intent.replaceExtras(new Bundle(extras));
1300                    }
1301
1302                    return intent;
1303                }
1304            }
1305        }
1306
1307        @Override
1308        public void addListener(@NonNull ShortcutChangeListener listener) {
1309            synchronized (mLock) {
1310                mListeners.add(Preconditions.checkNotNull(listener));
1311            }
1312        }
1313    }
1314
1315    // === Dump ===
1316
1317    @Override
1318    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1319        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1320                != PackageManager.PERMISSION_GRANTED) {
1321            pw.println("Permission Denial: can't dump UserManager from from pid="
1322                    + Binder.getCallingPid()
1323                    + ", uid=" + Binder.getCallingUid()
1324                    + " without permission "
1325                    + android.Manifest.permission.DUMP);
1326            return;
1327        }
1328        dumpInner(pw);
1329    }
1330
1331    @VisibleForTesting
1332    void dumpInner(PrintWriter pw) {
1333        synchronized (mLock) {
1334            final long now = injectCurrentTimeMillis();
1335            pw.print("Now: [");
1336            pw.print(now);
1337            pw.print("] ");
1338            pw.print(formatTime(now));
1339            pw.print("  Raw last reset: [");
1340            pw.print(mRawLastResetTime);
1341            pw.print("] ");
1342            pw.print(formatTime(mRawLastResetTime));
1343
1344            final long last = getLastResetTimeLocked();
1345            final long next = getNextResetTimeLocked();
1346            pw.print("  Last reset: [");
1347            pw.print(last);
1348            pw.print("] ");
1349            pw.print(formatTime(last));
1350
1351            pw.print("  Next reset: [");
1352            pw.print(next);
1353            pw.print("] ");
1354            pw.print(formatTime(next));
1355            pw.println();
1356
1357            pw.println();
1358
1359            for (int i = 0; i < mShortcuts.size(); i++) {
1360                dumpUserLocked(pw, mShortcuts.keyAt(i));
1361            }
1362
1363        }
1364    }
1365
1366    private void dumpUserLocked(PrintWriter pw, int userId) {
1367        pw.print("  User: ");
1368        pw.print(userId);
1369        pw.println();
1370
1371        final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId);
1372        if (packages == null) {
1373            return;
1374        }
1375        for (int j = 0; j < packages.size(); j++) {
1376            dumpPackageLocked(pw, userId, packages.keyAt(j));
1377        }
1378        pw.println();
1379    }
1380
1381    private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
1382        final PackageShortcuts shortcuts = mShortcuts.get(userId).get(packageName);
1383        if (shortcuts == null) {
1384            return;
1385        }
1386
1387        pw.print("    Package: ");
1388        pw.print(packageName);
1389        pw.println();
1390
1391        pw.print("      Calls: ");
1392        pw.print(shortcuts.getApiCallCount(this));
1393        pw.println();
1394
1395        // This should be after getApiCallCount(), which may update it.
1396        pw.print("      Last reset: [");
1397        pw.print(shortcuts.mLastResetTime);
1398        pw.print("] ");
1399        pw.print(formatTime(shortcuts.mLastResetTime));
1400        pw.println();
1401
1402        pw.println("      Shortcuts:");
1403        final int size = shortcuts.getShortcuts().size();
1404        for (int i = 0; i < size; i++) {
1405            pw.print("        ");
1406            pw.println(shortcuts.getShortcuts().valueAt(i).toInsecureString());
1407        }
1408    }
1409
1410    private static String formatTime(long time) {
1411        Time tobj = new Time();
1412        tobj.set(time);
1413        return tobj.format("%Y-%m-%d %H:%M:%S");
1414    }
1415
1416    // === Shell support ===
1417
1418    @Override
1419    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1420            String[] args, ResultReceiver resultReceiver) throws RemoteException {
1421
1422        enforceShell();
1423
1424        (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1425    }
1426
1427    /**
1428     * Handle "adb shell cmd".
1429     */
1430    private class MyShellCommand extends ShellCommand {
1431        @Override
1432        public int onCommand(String cmd) {
1433            if (cmd == null) {
1434                return handleDefaultCommands(cmd);
1435            }
1436            final PrintWriter pw = getOutPrintWriter();
1437            switch(cmd) {
1438                case "reset-package-throttling":
1439                    return handleResetPackageThrottling();
1440                case "reset-throttling":
1441                    return handleResetThrottling();
1442                default:
1443                    return handleDefaultCommands(cmd);
1444            }
1445        }
1446
1447        @Override
1448        public void onHelp() {
1449            final PrintWriter pw = getOutPrintWriter();
1450            pw.println("Usage: cmd shortcut COMMAND [options ...]");
1451            pw.println();
1452            pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1453            pw.println("    Reset throttling for a package");
1454            pw.println();
1455            pw.println("cmd shortcut reset-throttling");
1456            pw.println("    Reset throttling for all packages and users");
1457            pw.println();
1458        }
1459
1460        private int handleResetThrottling() {
1461            resetThrottling();
1462            return 0;
1463        }
1464
1465        private int handleResetPackageThrottling() {
1466            final PrintWriter pw = getOutPrintWriter();
1467
1468            int userId = UserHandle.USER_SYSTEM;
1469            String opt;
1470            while ((opt = getNextOption()) != null) {
1471                switch (opt) {
1472                    case "--user":
1473                        userId = UserHandle.parseUserArg(getNextArgRequired());
1474                        break;
1475                    default:
1476                        pw.println("Error: Unknown option: " + opt);
1477                        return 1;
1478                }
1479            }
1480            final String packageName = getNextArgRequired();
1481
1482            synchronized (mLock) {
1483                getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine();
1484                saveUserLocked(userId);
1485            }
1486
1487            return 0;
1488        }
1489    }
1490
1491    // === Unit test support ===
1492
1493    // Injection point.
1494    long injectCurrentTimeMillis() {
1495        return System.currentTimeMillis();
1496    }
1497
1498    // Injection point.
1499    int injectBinderCallingUid() {
1500        return getCallingUid();
1501    }
1502
1503    File injectSystemDataPath() {
1504        return Environment.getDataSystemDirectory();
1505    }
1506
1507    File injectUserDataPath(@UserIdInt int userId) {
1508        return new File(Environment.getDataSystemDeDirectory(userId), DIRECTORY_PER_USER);
1509    }
1510
1511    @VisibleForTesting
1512    SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() {
1513        return mShortcuts;
1514    }
1515
1516    @VisibleForTesting
1517    void setMaxDynamicShortcutsForTest(int max) {
1518        mMaxDynamicShortcuts = max;
1519    }
1520
1521    @VisibleForTesting
1522    void setMaxDailyUpdatesForTest(int max) {
1523        mMaxDailyUpdates = max;
1524    }
1525
1526    @VisibleForTesting
1527    public void setResetIntervalForTest(long interval) {
1528        mResetInterval = interval;
1529    }
1530}
1531