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