ShortcutPackage.java revision be73a8068ff3babe5b0a4f12656731ba8eea6149
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.Intent;
23import android.content.pm.ShortcutInfo;
24import android.os.PersistableBundle;
25import android.text.format.Formatter;
26import android.util.ArrayMap;
27import android.util.ArraySet;
28import android.util.Slog;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.XmlUtils;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35import org.xmlpull.v1.XmlSerializer;
36
37import java.io.File;
38import java.io.IOException;
39import java.io.PrintWriter;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.List;
43import java.util.Set;
44import java.util.function.Predicate;
45
46/**
47 * Package information used by {@link ShortcutService}.
48 */
49class ShortcutPackage extends ShortcutPackageItem {
50    private static final String TAG = ShortcutService.TAG;
51
52    static final String TAG_ROOT = "package";
53    private static final String TAG_INTENT_EXTRAS = "intent-extras";
54    private static final String TAG_EXTRAS = "extras";
55    private static final String TAG_SHORTCUT = "shortcut";
56    private static final String TAG_CATEGORIES = "categories";
57
58    private static final String ATTR_NAME = "name";
59    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
60    private static final String ATTR_CALL_COUNT = "call-count";
61    private static final String ATTR_LAST_RESET = "last-reset";
62    private static final String ATTR_ID = "id";
63    private static final String ATTR_ACTIVITY = "activity";
64    private static final String ATTR_TITLE = "title";
65    private static final String ATTR_TEXT = "text";
66    private static final String ATTR_INTENT = "intent";
67    private static final String ATTR_WEIGHT = "weight";
68    private static final String ATTR_TIMESTAMP = "timestamp";
69    private static final String ATTR_FLAGS = "flags";
70    private static final String ATTR_ICON_RES = "icon-res";
71    private static final String ATTR_BITMAP_PATH = "bitmap-path";
72
73    private static final String NAME_CATEGORIES = "categories";
74
75    private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
76    private static final String ATTR_NAME_XMLUTILS = "name";
77
78    /**
79     * All the shortcuts from the package, keyed on IDs.
80     */
81    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
82
83    /**
84     * # of dynamic shortcuts.
85     */
86    private int mDynamicShortcutCount = 0;
87
88    /**
89     * # of times the package has called rate-limited APIs.
90     */
91    private int mApiCallCount;
92
93    /**
94     * When {@link #mApiCallCount} was reset last time.
95     */
96    private long mLastResetTime;
97
98    private ShortcutPackage(int packageUserId, String packageName, ShortcutPackageInfo spi) {
99        super(packageUserId, packageName, spi != null ? spi : ShortcutPackageInfo.newEmpty());
100    }
101
102    public ShortcutPackage(int packageUserId, String packageName) {
103        this(packageUserId, packageName, null);
104    }
105
106    @Override
107    public int getOwnerUserId() {
108        // For packages, always owner user == package user.
109        return getPackageUserId();
110    }
111
112    @Override
113    protected void onRestoreBlocked(ShortcutService s) {
114        // Can't restore due to version/signature mismatch.  Remove all shortcuts.
115        mShortcuts.clear();
116    }
117
118    @Override
119    protected void onRestored(ShortcutService s) {
120        // Because some launchers may not have been restored (e.g. allowBackup=false),
121        // we need to re-calculate the pinned shortcuts.
122        refreshPinnedFlags(s);
123    }
124
125    /**
126     * Note this does *not* provide a correct view to the calling launcher.
127     */
128    @Nullable
129    public ShortcutInfo findShortcutById(String id) {
130        return mShortcuts.get(id);
131    }
132
133    private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
134            @NonNull String id) {
135        final ShortcutInfo shortcut = mShortcuts.remove(id);
136        if (shortcut != null) {
137            s.removeIcon(getPackageUserId(), shortcut);
138            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
139        }
140        return shortcut;
141    }
142
143    void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
144        deleteShortcut(s, newShortcut.getId());
145        s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
146        mShortcuts.put(newShortcut.getId(), newShortcut);
147    }
148
149    /**
150     * Add a shortcut, or update one with the same ID, with taking over existing flags.
151     *
152     * It checks the max number of dynamic shortcuts.
153     */
154    public void addDynamicShortcut(@NonNull ShortcutService s,
155            @NonNull ShortcutInfo newShortcut) {
156        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
157
158        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
159
160        final boolean wasPinned;
161        final int newDynamicCount;
162
163        if (oldShortcut == null) {
164            wasPinned = false;
165            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
166        } else {
167            wasPinned = oldShortcut.isPinned();
168            if (oldShortcut.isDynamic()) {
169                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
170            } else {
171                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
172            }
173        }
174
175        // Make sure there's still room.
176        s.enforceMaxDynamicShortcuts(newDynamicCount);
177
178        // Okay, make it dynamic and add.
179        if (wasPinned) {
180            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
181        }
182
183        addShortcut(s, newShortcut);
184        mDynamicShortcutCount = newDynamicCount;
185    }
186
187    /**
188     * Remove all shortcuts that aren't pinned nor dynamic.
189     */
190    private void removeOrphans(@NonNull ShortcutService s) {
191        ArrayList<String> removeList = null; // Lazily initialize.
192
193        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
194            final ShortcutInfo si = mShortcuts.valueAt(i);
195
196            if (si.isPinned() || si.isDynamic()) continue;
197
198            if (removeList == null) {
199                removeList = new ArrayList<>();
200            }
201            removeList.add(si.getId());
202        }
203        if (removeList != null) {
204            for (int i = removeList.size() - 1; i >= 0; i--) {
205                deleteShortcut(s, removeList.get(i));
206            }
207        }
208    }
209
210    /**
211     * Remove all dynamic shortcuts.
212     */
213    public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
214        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
215            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
216        }
217        removeOrphans(s);
218        mDynamicShortcutCount = 0;
219    }
220
221    /**
222     * Remove a dynamic shortcut by ID.
223     */
224    public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
225        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
226
227        if (oldShortcut == null) {
228            return;
229        }
230        if (oldShortcut.isDynamic()) {
231            mDynamicShortcutCount--;
232        }
233        if (oldShortcut.isPinned()) {
234            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
235        } else {
236            deleteShortcut(s, shortcutId);
237        }
238    }
239
240    /**
241     * Called after a launcher updates the pinned set.  For each shortcut in this package,
242     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
243     *
244     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
245     */
246    public void refreshPinnedFlags(@NonNull ShortcutService s) {
247        // First, un-pin all shortcuts
248        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
249            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
250        }
251
252        // Then, for the pinned set for each launcher, set the pin flag one by one.
253        final ArrayMap<ShortcutUser.PackageWithUser, ShortcutLauncher> launchers =
254                s.getUserShortcutsLocked(getPackageUserId()).getAllLaunchers();
255
256        for (int l = launchers.size() - 1; l >= 0; l--) {
257            // Note even if a launcher that hasn't been installed can still pin shortcuts.
258
259            final ShortcutLauncher launcherShortcuts = launchers.valueAt(l);
260            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
261                    getPackageName(), getPackageUserId());
262
263            if (pinned == null || pinned.size() == 0) {
264                continue;
265            }
266            for (int i = pinned.size() - 1; i >= 0; i--) {
267                final String id = pinned.valueAt(i);
268                final ShortcutInfo si = mShortcuts.get(id);
269                if (si == null) {
270                    // This happens if a launcher pinned shortcuts from this package, then backup&
271                    // restored, but this package doesn't allow backing up.
272                    // In that case the launcher ends up having a dangling pinned shortcuts.
273                    // That's fine, when the launcher is restored, we'll fix it.
274                    continue;
275                }
276                si.addFlags(ShortcutInfo.FLAG_PINNED);
277            }
278        }
279
280        // Lastly, remove the ones that are no longer pinned nor dynamic.
281        removeOrphans(s);
282    }
283
284    /**
285     * Number of calls that the caller has made, since the last reset.
286     */
287    public int getApiCallCount(@NonNull ShortcutService s) {
288        final long last = s.getLastResetTimeLocked();
289
290        final long now = s.injectCurrentTimeMillis();
291        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
292            Slog.w(TAG, "Clock rewound");
293            // Clock rewound.
294            mLastResetTime = now;
295            mApiCallCount = 0;
296            return mApiCallCount;
297        }
298
299        // If not reset yet, then reset.
300        if (mLastResetTime < last) {
301            if (ShortcutService.DEBUG) {
302                Slog.d(TAG, String.format("My last reset=%d, now=%d, last=%d: resetting",
303                        mLastResetTime, now, last));
304            }
305            mApiCallCount = 0;
306            mLastResetTime = last;
307        }
308        return mApiCallCount;
309    }
310
311    /**
312     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
313     * and return true.  Otherwise just return false.
314     */
315    public boolean tryApiCall(@NonNull ShortcutService s) {
316        if (getApiCallCount(s) >= s.mMaxUpdatesPerInterval) {
317            return false;
318        }
319        mApiCallCount++;
320        return true;
321    }
322
323    public void resetRateLimitingForCommandLine() {
324        mApiCallCount = 0;
325        mLastResetTime = 0;
326    }
327
328    /**
329     * Find all shortcuts that match {@code query}.
330     */
331    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
332            @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
333        findAll(s, result, query, cloneFlag, null, 0);
334    }
335
336    /**
337     * Find all shortcuts that match {@code query}.
338     *
339     * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
340     * by the calling launcher will not be included in the result, and also "isPinned" will be
341     * adjusted for the caller too.
342     */
343    public void findAll(@NonNull ShortcutService s, @NonNull List<ShortcutInfo> result,
344            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
345            @Nullable String callingLauncher, int launcherUserId) {
346        if (getPackageInfo().isShadow()) {
347            // Restored and the app not installed yet, so don't return any.
348            return;
349        }
350
351        // Set of pinned shortcuts by the calling launcher.
352        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
353                : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
354                    .getPinnedShortcutIds(getPackageName(), getPackageUserId());
355
356        for (int i = 0; i < mShortcuts.size(); i++) {
357            final ShortcutInfo si = mShortcuts.valueAt(i);
358
359            // If it's called by non-launcher (i.e. publisher, always include -> true.
360            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
361            // it.
362            final boolean isPinnedByCaller = (callingLauncher == null)
363                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
364            if (!si.isDynamic()) {
365                if (!si.isPinned()) {
366                    s.wtf("Shortcut not pinned: package " + getPackageName()
367                            + ", user=" + getPackageUserId() + ", id=" + si.getId());
368                    continue;
369                }
370                if (!isPinnedByCaller) {
371                    continue;
372                }
373            }
374            final ShortcutInfo clone = si.clone(cloneFlag);
375            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
376            // since it may check isPinned.
377            if (!isPinnedByCaller) {
378                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
379            }
380            if (query == null || query.test(clone)) {
381                result.add(clone);
382            }
383        }
384    }
385
386    public void resetThrottling() {
387        mApiCallCount = 0;
388    }
389
390    public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
391        pw.println();
392
393        pw.print(prefix);
394        pw.print("Package: ");
395        pw.print(getPackageName());
396        pw.println();
397
398        pw.print(prefix);
399        pw.print("  ");
400        pw.print("Calls: ");
401        pw.print(getApiCallCount(s));
402        pw.println();
403
404        // This should be after getApiCallCount(), which may update it.
405        pw.print(prefix);
406        pw.print("  ");
407        pw.print("Last reset: [");
408        pw.print(mLastResetTime);
409        pw.print("] ");
410        pw.print(s.formatTime(mLastResetTime));
411        pw.println();
412
413        getPackageInfo().dump(s, pw, prefix + "  ");
414        pw.println();
415
416        pw.println("      Shortcuts:");
417        long totalBitmapSize = 0;
418        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
419        final int size = shortcuts.size();
420        for (int i = 0; i < size; i++) {
421            final ShortcutInfo si = shortcuts.valueAt(i);
422            pw.print("        ");
423            pw.println(si.toInsecureString());
424            if (si.getBitmapPath() != null) {
425                final long len = new File(si.getBitmapPath()).length();
426                pw.print("          ");
427                pw.print("bitmap size=");
428                pw.println(len);
429
430                totalBitmapSize += len;
431            }
432        }
433        pw.print(prefix);
434        pw.print("  ");
435        pw.print("Total bitmap size: ");
436        pw.print(totalBitmapSize);
437        pw.print(" (");
438        pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
439        pw.println(")");
440    }
441
442    @Override
443    public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
444            throws IOException, XmlPullParserException {
445        final int size = mShortcuts.size();
446
447        if (size == 0 && mApiCallCount == 0) {
448            return; // nothing to write.
449        }
450
451        out.startTag(null, TAG_ROOT);
452
453        ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
454        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
455        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
456        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
457        getPackageInfo().saveToXml(out);
458
459        for (int j = 0; j < size; j++) {
460            saveShortcut(out, mShortcuts.valueAt(j), forBackup);
461        }
462
463        out.endTag(null, TAG_ROOT);
464    }
465
466    private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
467            throws IOException, XmlPullParserException {
468        if (forBackup) {
469            if (!si.isPinned()) {
470                return; // Backup only pinned icons.
471            }
472        }
473        out.startTag(null, TAG_SHORTCUT);
474        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
475        // writeAttr(out, "package", si.getPackageName()); // not needed
476        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
477        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
478        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
479        ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
480        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
481        ShortcutService.writeAttr(out, ATTR_WEIGHT, si.getWeight());
482        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
483                si.getLastChangedTimestamp());
484        if (forBackup) {
485            // Don't write icon information.  Also drop the dynamic flag.
486            ShortcutService.writeAttr(out, ATTR_FLAGS,
487                    si.getFlags() &
488                            ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
489                            | ShortcutInfo.FLAG_DYNAMIC));
490        } else {
491            ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
492            ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
493            ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
494        }
495
496        {
497            final Set<String> cat = si.getCategories();
498            if (cat != null && cat.size() > 0) {
499                out.startTag(null, TAG_CATEGORIES);
500                XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
501                        NAME_CATEGORIES, out);
502                out.endTag(null, TAG_CATEGORIES);
503            }
504        }
505
506        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
507                si.getIntentPersistableExtras());
508        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
509
510        out.endTag(null, TAG_SHORTCUT);
511    }
512
513    public static ShortcutPackage loadFromXml(ShortcutService s, XmlPullParser parser,
514            int ownerUserId, boolean fromBackup)
515            throws IOException, XmlPullParserException {
516
517        final String packageName = ShortcutService.parseStringAttribute(parser,
518                ATTR_NAME);
519
520        final ShortcutPackage ret = new ShortcutPackage(ownerUserId, packageName);
521
522        ret.mDynamicShortcutCount =
523                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
524        ret.mApiCallCount =
525                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
526        ret.mLastResetTime =
527                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
528
529        final int outerDepth = parser.getDepth();
530        int type;
531        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
532                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
533            if (type != XmlPullParser.START_TAG) {
534                continue;
535            }
536            final int depth = parser.getDepth();
537            final String tag = parser.getName();
538            if (depth == outerDepth + 1) {
539                switch (tag) {
540                    case ShortcutPackageInfo.TAG_ROOT:
541                        ret.getPackageInfo().loadFromXml(parser, fromBackup);
542                        continue;
543                    case TAG_SHORTCUT:
544                        final ShortcutInfo si = parseShortcut(parser, packageName, ownerUserId);
545
546                        // Don't use addShortcut(), we don't need to save the icon.
547                        ret.mShortcuts.put(si.getId(), si);
548                        continue;
549                }
550            }
551            ShortcutService.warnForInvalidTag(depth, tag);
552        }
553        return ret;
554    }
555
556    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
557            @UserIdInt int userId) throws IOException, XmlPullParserException {
558        String id;
559        ComponentName activityComponent;
560        // Icon icon;
561        String title;
562        String text;
563        Intent intent;
564        PersistableBundle intentPersistableExtras = null;
565        int weight;
566        PersistableBundle extras = null;
567        long lastChangedTimestamp;
568        int flags;
569        int iconRes;
570        String bitmapPath;
571        ArraySet<String> categories = null;
572
573        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
574        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
575                ATTR_ACTIVITY);
576        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
577        text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
578        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
579        weight = (int) ShortcutService.parseLongAttribute(parser, ATTR_WEIGHT);
580        lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
581        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
582        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
583        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
584
585        final int outerDepth = parser.getDepth();
586        int type;
587        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
588                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
589            if (type != XmlPullParser.START_TAG) {
590                continue;
591            }
592            final int depth = parser.getDepth();
593            final String tag = parser.getName();
594            if (ShortcutService.DEBUG_LOAD) {
595                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
596                        depth, type, tag));
597            }
598            switch (tag) {
599                case TAG_INTENT_EXTRAS:
600                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
601                    continue;
602                case TAG_EXTRAS:
603                    extras = PersistableBundle.restoreFromXml(parser);
604                    continue;
605                case TAG_CATEGORIES:
606                    // This just contains string-array.
607                    continue;
608                case TAG_STRING_ARRAY_XMLUTILS:
609                    if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
610                            ATTR_NAME_XMLUTILS))) {
611                        final String[] ar = XmlUtils.readThisStringArrayXml(
612                                parser, TAG_STRING_ARRAY_XMLUTILS, null);
613                        categories = new ArraySet<>(ar.length);
614                        for (int i = 0; i < ar.length; i++) {
615                            categories.add(ar[i]);
616                        }
617                    }
618                    continue;
619            }
620            throw ShortcutService.throwForInvalidTag(depth, tag);
621        }
622
623        return new ShortcutInfo(
624                userId, id, packageName, activityComponent, /* icon =*/ null, title, text,
625                categories, intent,
626                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
627                iconRes, bitmapPath);
628    }
629
630    @VisibleForTesting
631    List<ShortcutInfo> getAllShortcutsForTest() {
632        return new ArrayList<>(mShortcuts.values());
633    }
634}
635