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