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