ShortcutService.java revision 4362a66dba0b4cfa9fadb6c8af10c590e4ba880d
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 onStartUser(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        path.mkdirs();
872        final AtomicFile file = new AtomicFile(path);
873
874        final FileInputStream in;
875        try {
876            in = file.openRead();
877        } catch (FileNotFoundException e) {
878            if (DEBUG) {
879                Slog.i(TAG, "Not found " + path);
880            }
881            return null;
882        }
883        final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>();
884        try {
885            XmlPullParser parser = Xml.newPullParser();
886            parser.setInput(in, StandardCharsets.UTF_8.name());
887
888            String packageName = null;
889            PackageShortcuts shortcuts = null;
890
891            int type;
892            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
893                if (type != XmlPullParser.START_TAG) {
894                    continue;
895                }
896                final int depth = parser.getDepth();
897
898                // TODO Move some of this to PackageShortcuts.
899
900                final String tag = parser.getName();
901                if (DEBUG_LOAD) {
902                    Slog.d(TAG, String.format("depth=%d type=%d name=%s",
903                            depth, type, tag));
904                }
905                switch (depth) {
906                    case 1: {
907                        if (TAG_ROOT.equals(tag)) {
908                            continue;
909                        }
910                        break;
911                    }
912                    case 2: {
913                        switch (tag) {
914                            case TAG_PACKAGE:
915                                packageName = parseStringAttribute(parser, ATTR_NAME);
916                                shortcuts = new PackageShortcuts(userId, packageName);
917                                ret.put(packageName, shortcuts);
918
919                                shortcuts.mDynamicShortcutCount =
920                                        (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT);
921                                shortcuts.mApiCallCount =
922                                        (int) parseLongAttribute(parser, ATTR_CALL_COUNT);
923                                shortcuts.mLastResetTime = parseLongAttribute(parser,
924                                        ATTR_LAST_RESET);
925                                continue;
926                        }
927                        break;
928                    }
929                    case 3: {
930                        switch (tag) {
931                            case TAG_SHORTCUT:
932                                final ShortcutInfo si = parseShortcut(parser, packageName);
933
934                                // Don't use addShortcut(), we don't need to save the icon.
935                                shortcuts.mShortcuts.put(si.getId(), si);
936                                continue;
937                        }
938                        break;
939                    }
940                }
941                throwForInvalidTag(depth, tag);
942            }
943            return ret;
944        } catch (IOException|XmlPullParserException e) {
945            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
946            return null;
947        } finally {
948            IoUtils.closeQuietly(in);
949        }
950    }
951
952    private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName)
953            throws IOException, XmlPullParserException {
954        String id;
955        ComponentName activityComponent;
956        Icon icon;
957        String title;
958        Intent intent;
959        PersistableBundle intentPersistableExtras = null;
960        int weight;
961        PersistableBundle extras = null;
962        long lastChangedTimestamp;
963        int flags;
964        int iconRes;
965        String bitmapPath;
966
967        id = parseStringAttribute(parser, ATTR_ID);
968        activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY);
969        title = parseStringAttribute(parser, ATTR_TITLE);
970        intent = parseIntentAttribute(parser, ATTR_INTENT);
971        weight = (int) parseLongAttribute(parser, ATTR_WEIGHT);
972        lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP);
973        flags = (int) parseLongAttribute(parser, ATTR_FLAGS);
974        iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES);
975        bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH);
976
977        final int outerDepth = parser.getDepth();
978        int type;
979        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
980                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
981            if (type != XmlPullParser.START_TAG) {
982                continue;
983            }
984            final int depth = parser.getDepth();
985            final String tag = parser.getName();
986            if (DEBUG_LOAD) {
987                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
988                        depth, type, tag));
989            }
990            switch (tag) {
991                case TAG_INTENT_EXTRAS:
992                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
993                    continue;
994                case TAG_EXTRAS:
995                    extras = PersistableBundle.restoreFromXml(parser);
996                    continue;
997            }
998            throw throwForInvalidTag(depth, tag);
999        }
1000        return new ShortcutInfo(
1001                id, packgeName, activityComponent, /* icon =*/ null, title, intent,
1002                intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
1003                iconRes, bitmapPath);
1004    }
1005
1006    // TODO Actually make it async.
1007    private void scheduleSaveBaseState() {
1008        synchronized (mLock) {
1009            saveBaseStateLocked();
1010        }
1011    }
1012
1013    // TODO Actually make it async.
1014    private void scheduleSaveUser(@UserIdInt int userId) {
1015        synchronized (mLock) {
1016            saveUserLocked(userId);
1017        }
1018    }
1019
1020    /** Return the last reset time. */
1021    long getLastResetTimeLocked() {
1022        updateTimes();
1023        return mRawLastResetTime;
1024    }
1025
1026    /** Return the next reset time. */
1027    long getNextResetTimeLocked() {
1028        updateTimes();
1029        return mRawLastResetTime + mResetInterval;
1030    }
1031
1032    /**
1033     * Update the last reset time.
1034     */
1035    private void updateTimes() {
1036
1037        final long now = injectCurrentTimeMillis();
1038
1039        final long prevLastResetTime = mRawLastResetTime;
1040
1041        if (mRawLastResetTime == 0) { // first launch.
1042            // TODO Randomize??
1043            mRawLastResetTime = now;
1044        } else if (now < mRawLastResetTime) {
1045            // Clock rewound.
1046            // TODO Randomize??
1047            mRawLastResetTime = now;
1048        } else {
1049            // TODO Do it properly.
1050            while ((mRawLastResetTime + mResetInterval) <= now) {
1051                mRawLastResetTime += mResetInterval;
1052            }
1053        }
1054        if (prevLastResetTime != mRawLastResetTime) {
1055            scheduleSaveBaseState();
1056        }
1057    }
1058
1059    /** Return the per-user state. */
1060    @GuardedBy("mLock")
1061    @NonNull
1062    private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) {
1063        ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId);
1064        if (userPackages == null) {
1065            userPackages = loadUserLocked(userId);
1066            if (userPackages == null) {
1067                userPackages = new ArrayMap<>();
1068            }
1069            mShortcuts.put(userId, userPackages);
1070        }
1071        return userPackages;
1072    }
1073
1074    /** Return the per-user per-package state. */
1075    @GuardedBy("mLock")
1076    @NonNull
1077    private PackageShortcuts getPackageShortcutsLocked(
1078            @NonNull String packageName, @UserIdInt int userId) {
1079        final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId);
1080        PackageShortcuts shortcuts = userPackages.get(packageName);
1081        if (shortcuts == null) {
1082            shortcuts = new PackageShortcuts(userId, packageName);
1083            userPackages.put(packageName, shortcuts);
1084        }
1085        return shortcuts;
1086    }
1087
1088    // === Caller validation ===
1089
1090    void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
1091        if (shortcut.getBitmapPath() != null) {
1092            if (DEBUG) {
1093                Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
1094            }
1095            new File(shortcut.getBitmapPath()).delete();
1096
1097            shortcut.setBitmapPath(null);
1098            shortcut.setIconResourceId(0);
1099            shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
1100        }
1101    }
1102
1103    @VisibleForTesting
1104    static class FileOutputStreamWithPath extends FileOutputStream {
1105        private final File mFile;
1106
1107        public FileOutputStreamWithPath(File file) throws FileNotFoundException {
1108            super(file);
1109            mFile = file;
1110        }
1111
1112        public File getFile() {
1113            return mFile;
1114        }
1115    }
1116
1117    /**
1118     * Build the cached bitmap filename for a shortcut icon.
1119     *
1120     * The filename will be based on the ID, except certain characters will be escaped.
1121     */
1122    @VisibleForTesting
1123    FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
1124            throws IOException {
1125        final File packagePath = new File(getUserBitmapFilePath(userId),
1126                shortcut.getPackageName());
1127        if (!packagePath.isDirectory()) {
1128            packagePath.mkdirs();
1129            if (!packagePath.isDirectory()) {
1130                throw new IOException("Unable to create directory " + packagePath);
1131            }
1132            SELinux.restorecon(packagePath);
1133        }
1134
1135        final String baseName = String.valueOf(injectCurrentTimeMillis());
1136        for (int suffix = 0;; suffix++) {
1137            final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
1138            final File file = new File(packagePath, filename);
1139            if (!file.exists()) {
1140                if (DEBUG) {
1141                    Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
1142                }
1143                return new FileOutputStreamWithPath(file);
1144            }
1145        }
1146    }
1147
1148    void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
1149        if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
1150            return;
1151        }
1152
1153        final long token = Binder.clearCallingIdentity();
1154        try {
1155            // Clear icon info on the shortcut.
1156            shortcut.setIconResourceId(0);
1157            shortcut.setBitmapPath(null);
1158
1159            final Icon icon = shortcut.getIcon();
1160            if (icon == null) {
1161                return; // has no icon
1162            }
1163
1164            Bitmap bitmap = null;
1165            try {
1166                switch (icon.getType()) {
1167                    case Icon.TYPE_RESOURCE: {
1168                        injectValidateIconResPackage(shortcut, icon);
1169
1170                        shortcut.setIconResourceId(icon.getResId());
1171                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
1172                        return;
1173                    }
1174                    case Icon.TYPE_BITMAP: {
1175                        bitmap = icon.getBitmap();
1176                        break;
1177                    }
1178                    case Icon.TYPE_URI: {
1179                        final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
1180
1181                        try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
1182
1183                            bitmap = BitmapFactory.decodeStream(is);
1184
1185                        } catch (IOException e) {
1186                            Slog.e(TAG, "Unable to load icon from " + uri);
1187                            return;
1188                        }
1189                        break;
1190                    }
1191                    default:
1192                        // This shouldn't happen because we've already validated the icon, but
1193                        // just in case.
1194                        throw ShortcutInfo.getInvalidIconException();
1195                }
1196                if (bitmap == null) {
1197                    Slog.e(TAG, "Null bitmap detected");
1198                    return;
1199                }
1200                // Shrink and write to the file.
1201                File path = null;
1202                try {
1203                    final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
1204                    try {
1205                        path = out.getFile();
1206
1207                        shrinkBitmap(bitmap, mMaxIconDimension)
1208                                .compress(mIconPersistFormat, mIconPersistQuality, out);
1209
1210                        shortcut.setBitmapPath(out.getFile().getAbsolutePath());
1211                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
1212                    } finally {
1213                        IoUtils.closeQuietly(out);
1214                    }
1215                } catch (IOException|RuntimeException e) {
1216                    // STOPSHIP Change wtf to e
1217                    Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
1218                    if (path != null && path.exists()) {
1219                        path.delete();
1220                    }
1221                }
1222            } finally {
1223                if (bitmap != null) {
1224                    bitmap.recycle();
1225                }
1226                // Once saved, we won't use the original icon information, so null it out.
1227                shortcut.clearIcon();
1228            }
1229        } finally {
1230            Binder.restoreCallingIdentity(token);
1231        }
1232    }
1233
1234    // Unfortunately we can't do this check in unit tests because we fake creator package names,
1235    // so override in unit tests.
1236    // TODO CTS this case.
1237    void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
1238        if (!shortcut.getPackageName().equals(icon.getResPackage())) {
1239            throw new IllegalArgumentException(
1240                    "Icon resource must reside in shortcut owner package");
1241        }
1242    }
1243
1244    @VisibleForTesting
1245    static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
1246        // Original width/height.
1247        final int ow = in.getWidth();
1248        final int oh = in.getHeight();
1249        if ((ow <= maxSize) && (oh <= maxSize)) {
1250            if (DEBUG) {
1251                Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
1252            }
1253            return in;
1254        }
1255        final int longerDimension = Math.max(ow, oh);
1256
1257        // New width and height.
1258        final int nw = ow * maxSize / longerDimension;
1259        final int nh = oh * maxSize / longerDimension;
1260        if (DEBUG) {
1261            Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
1262                    ow, oh, nw, nh));
1263        }
1264
1265        final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
1266        final Canvas c = new Canvas(scaledBitmap);
1267
1268        final RectF dst = new RectF(0, 0, nw, nh);
1269
1270        c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
1271
1272        in.recycle();
1273
1274        return scaledBitmap;
1275    }
1276
1277    // === Caller validation ===
1278
1279    private boolean isCallerSystem() {
1280        final int callingUid = injectBinderCallingUid();
1281         return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
1282    }
1283
1284    private boolean isCallerShell() {
1285        final int callingUid = injectBinderCallingUid();
1286        return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
1287    }
1288
1289    private void enforceSystemOrShell() {
1290        Preconditions.checkState(isCallerSystem() || isCallerShell(),
1291                "Caller must be system or shell");
1292    }
1293
1294    private void enforceShell() {
1295        Preconditions.checkState(isCallerShell(), "Caller must be shell");
1296    }
1297
1298    private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) {
1299        Preconditions.checkStringNotEmpty(packageName, "packageName");
1300
1301        if (isCallerSystem()) {
1302            return; // no check
1303        }
1304
1305        final int callingUid = injectBinderCallingUid();
1306
1307        // Otherwise, make sure the arguments are valid.
1308        if (UserHandle.getUserId(callingUid) != userId) {
1309            throw new SecurityException("Invalid user-ID");
1310        }
1311        if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
1312            return; // Caller is valid.
1313        }
1314        throw new SecurityException("Caller UID= doesn't own " + packageName);
1315    }
1316
1317    // Test overrides it.
1318    int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
1319        try {
1320
1321            // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
1322
1323            return mContext.getPackageManager().getPackageUidAsUser(packageName,
1324                    PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
1325                            | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
1326        } catch (NameNotFoundException e) {
1327            return -1;
1328        }
1329    }
1330
1331    /**
1332     * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
1333     */
1334    void enforceMaxDynamicShortcuts(int numShortcuts) {
1335        if (numShortcuts > mMaxDynamicShortcuts) {
1336            throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
1337        }
1338    }
1339
1340    /**
1341     * - Sends a notification to LauncherApps
1342     * - Write to file
1343     */
1344    private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
1345        notifyListeners(packageName, userId);
1346        scheduleSaveUser(userId);
1347    }
1348
1349    private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
1350        final ArrayList<ShortcutChangeListener> copy;
1351        final List<ShortcutInfo> shortcuts = new ArrayList<>();
1352        synchronized (mLock) {
1353            copy = new ArrayList<>(mListeners);
1354
1355            getPackageShortcutsLocked(packageName, userId)
1356                    .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
1357        }
1358        for (int i = copy.size() - 1; i >= 0; i--) {
1359            copy.get(i).onShortcutChanged(packageName, shortcuts, userId);
1360        }
1361    }
1362
1363    /**
1364     * Clean up / validate an incoming shortcut.
1365     * - Make sure all mandatory fields are set.
1366     * - Make sure the intent's extras are persistable, and them to set
1367     *  {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
1368     * - Clear flags.
1369     *
1370     * TODO Detailed unit tests
1371     */
1372    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
1373        Preconditions.checkNotNull(shortcut, "Null shortcut detected");
1374        if (shortcut.getActivityComponent() != null) {
1375            Preconditions.checkState(
1376                    shortcut.getPackageName().equals(
1377                            shortcut.getActivityComponent().getPackageName()),
1378                    "Activity package name mismatch");
1379        }
1380
1381        if (!forUpdate) {
1382            shortcut.enforceMandatoryFields();
1383        }
1384        if (shortcut.getIcon() != null) {
1385            ShortcutInfo.validateIcon(shortcut.getIcon());
1386        }
1387
1388        validateForXml(shortcut.getId());
1389        validateForXml(shortcut.getTitle());
1390        validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
1391        validatePersistableBundleForXml(shortcut.getExtras());
1392
1393        shortcut.setFlags(0);
1394    }
1395
1396    // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
1397    // characters.
1398
1399    private static void validatePersistableBundleForXml(PersistableBundle b) {
1400        if (b == null || b.size() == 0) {
1401            return;
1402        }
1403        for (String key : b.keySet()) {
1404            validateForXml(key);
1405            final Object value = b.get(key);
1406            if (value == null) {
1407                continue;
1408            } else if (value instanceof String) {
1409                validateForXml((String) value);
1410            } else if (value instanceof String[]) {
1411                for (String v : (String[]) value) {
1412                    validateForXml(v);
1413                }
1414            } else if (value instanceof PersistableBundle) {
1415                validatePersistableBundleForXml((PersistableBundle) value);
1416            }
1417        }
1418    }
1419
1420    private static void validateForXml(String s) {
1421        if (TextUtils.isEmpty(s)) {
1422            return;
1423        }
1424        for (int i = s.length() - 1; i >= 0; i--) {
1425            if (!isAllowedInXml(s.charAt(i))) {
1426                throw new IllegalArgumentException("Unsupported character detected in: " + s);
1427            }
1428        }
1429    }
1430
1431    private static boolean isAllowedInXml(char c) {
1432        return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
1433    }
1434
1435    // === APIs ===
1436
1437    @Override
1438    public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1439            @UserIdInt int userId) {
1440        verifyCaller(packageName, userId);
1441
1442        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1443        final int size = newShortcuts.size();
1444
1445        synchronized (mLock) {
1446            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1447
1448            // Throttling.
1449            if (!ps.tryApiCall(this)) {
1450                return false;
1451            }
1452            enforceMaxDynamicShortcuts(size);
1453
1454            // Validate the shortcuts.
1455            for (int i = 0; i < size; i++) {
1456                fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
1457            }
1458
1459            // First, remove all un-pinned; dynamic shortcuts
1460            ps.deleteAllDynamicShortcuts(this);
1461
1462            // Then, add/update all.  We need to make sure to take over "pinned" flag.
1463            for (int i = 0; i < size; i++) {
1464                final ShortcutInfo newShortcut = newShortcuts.get(i);
1465                newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1466                ps.updateShortcutWithCapping(this, newShortcut);
1467            }
1468        }
1469        userPackageChanged(packageName, userId);
1470        return true;
1471    }
1472
1473    @Override
1474    public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1475            @UserIdInt int userId) {
1476        verifyCaller(packageName, userId);
1477
1478        final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1479        final int size = newShortcuts.size();
1480
1481        synchronized (mLock) {
1482            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1483
1484            // Throttling.
1485            if (!ps.tryApiCall(this)) {
1486                return false;
1487            }
1488
1489            for (int i = 0; i < size; i++) {
1490                final ShortcutInfo source = newShortcuts.get(i);
1491                fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
1492
1493                final ShortcutInfo target = ps.findShortcutById(source.getId());
1494                if (target != null) {
1495                    final boolean replacingIcon = (source.getIcon() != null);
1496                    if (replacingIcon) {
1497                        removeIcon(userId, target);
1498                    }
1499
1500                    target.copyNonNullFieldsFrom(source);
1501
1502                    if (replacingIcon) {
1503                        saveIconAndFixUpShortcut(userId, target);
1504                    }
1505                }
1506            }
1507        }
1508        userPackageChanged(packageName, userId);
1509
1510        return true;
1511    }
1512
1513    @Override
1514    public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut,
1515            @UserIdInt int userId) {
1516        verifyCaller(packageName, userId);
1517
1518        synchronized (mLock) {
1519            final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1520
1521            // Throttling.
1522            if (!ps.tryApiCall(this)) {
1523                return false;
1524            }
1525
1526            // Validate the shortcut.
1527            fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
1528
1529            // Add it.
1530            newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1531            ps.updateShortcutWithCapping(this, newShortcut);
1532        }
1533        userPackageChanged(packageName, userId);
1534
1535        return true;
1536    }
1537
1538    @Override
1539    public void deleteDynamicShortcut(String packageName, String shortcutId,
1540            @UserIdInt int userId) {
1541        verifyCaller(packageName, userId);
1542        Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
1543
1544        synchronized (mLock) {
1545            getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
1546        }
1547        userPackageChanged(packageName, userId);
1548    }
1549
1550    @Override
1551    public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) {
1552        verifyCaller(packageName, userId);
1553
1554        synchronized (mLock) {
1555            getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
1556        }
1557        userPackageChanged(packageName, userId);
1558    }
1559
1560    @Override
1561    public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName,
1562            @UserIdInt int userId) {
1563        verifyCaller(packageName, userId);
1564        synchronized (mLock) {
1565            return getShortcutsWithQueryLocked(
1566                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1567                    ShortcutInfo::isDynamic);
1568        }
1569    }
1570
1571    @Override
1572    public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
1573            @UserIdInt int userId) {
1574        verifyCaller(packageName, userId);
1575        synchronized (mLock) {
1576            return getShortcutsWithQueryLocked(
1577                    packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1578                    ShortcutInfo::isPinned);
1579        }
1580    }
1581
1582    private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName,
1583            @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) {
1584
1585        final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1586
1587        getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags);
1588
1589        return new ParceledListSlice<>(ret);
1590    }
1591
1592    @Override
1593    public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId)
1594            throws RemoteException {
1595        verifyCaller(packageName, userId);
1596
1597        return mMaxDynamicShortcuts;
1598    }
1599
1600    @Override
1601    public int getRemainingCallCount(String packageName, @UserIdInt int userId) {
1602        verifyCaller(packageName, userId);
1603
1604        synchronized (mLock) {
1605            return mMaxDailyUpdates
1606                    - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
1607        }
1608    }
1609
1610    @Override
1611    public long getRateLimitResetTime(String packageName, @UserIdInt int userId) {
1612        verifyCaller(packageName, userId);
1613
1614        synchronized (mLock) {
1615            return getNextResetTimeLocked();
1616        }
1617    }
1618
1619    @Override
1620    public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
1621        synchronized (mLock) {
1622            return mMaxIconDimension;
1623        }
1624    }
1625
1626    /**
1627     * Reset all throttling, for developer options and command line.  Only system/shell can call it.
1628     */
1629    @Override
1630    public void resetThrottling() {
1631        enforceSystemOrShell();
1632
1633        resetThrottlingInner();
1634    }
1635
1636    @VisibleForTesting
1637    void resetThrottlingInner() {
1638        synchronized (mLock) {
1639            mRawLastResetTime = injectCurrentTimeMillis();
1640        }
1641        scheduleSaveBaseState();
1642        Slog.i(TAG, "ShortcutManager: throttling counter reset");
1643    }
1644
1645    /**
1646     * Entry point from {@link LauncherApps}.
1647     */
1648    private class LocalService extends ShortcutServiceInternal {
1649        @Override
1650        public List<ShortcutInfo> getShortcuts(
1651                @NonNull String callingPackage, long changedSince,
1652                @Nullable String packageName, @Nullable ComponentName componentName,
1653                int queryFlags, int userId) {
1654            final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1655            final int cloneFlag =
1656                    ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0)
1657                            ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER
1658                            : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO;
1659
1660            synchronized (mLock) {
1661                if (packageName != null) {
1662                    getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags,
1663                            userId, ret, cloneFlag);
1664                } else {
1665                    final ArrayMap<String, PackageShortcuts> packages =
1666                            getUserShortcutsLocked(userId);
1667                    for (int i = packages.size() - 1; i >= 0; i--) {
1668                        getShortcutsInnerLocked(
1669                                packages.keyAt(i),
1670                                changedSince, componentName, queryFlags, userId, ret, cloneFlag);
1671                    }
1672                }
1673            }
1674            return ret;
1675        }
1676
1677        private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince,
1678                @Nullable ComponentName componentName, int queryFlags,
1679                int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) {
1680            getPackageShortcutsLocked(packageName, userId).findAll(ret,
1681                    (ShortcutInfo si) -> {
1682                        if (si.getLastChangedTimestamp() < changedSince) {
1683                            return false;
1684                        }
1685                        if (componentName != null
1686                                && !componentName.equals(si.getActivityComponent())) {
1687                            return false;
1688                        }
1689                        final boolean matchDynamic =
1690                                ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
1691                                && si.isDynamic();
1692                        final boolean matchPinned =
1693                                ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
1694                                        && si.isPinned();
1695                        return matchDynamic || matchPinned;
1696                    }, cloneFlag);
1697        }
1698
1699        @Override
1700        public List<ShortcutInfo> getShortcutInfo(
1701                @NonNull String callingPackage,
1702                @NonNull String packageName, @Nullable List<String> ids, int userId) {
1703            // Calling permission must be checked by LauncherAppsImpl.
1704            Preconditions.checkStringNotEmpty(packageName, "packageName");
1705
1706            final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size());
1707            final ArraySet<String> idSet = new ArraySet<>(ids);
1708            synchronized (mLock) {
1709                getPackageShortcutsLocked(packageName, userId).findAll(ret,
1710                        (ShortcutInfo si) -> idSet.contains(si.getId()),
1711                        ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
1712            }
1713            return ret;
1714        }
1715
1716        @Override
1717        public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName,
1718                @NonNull List<String> shortcutIds, int userId) {
1719            // Calling permission must be checked by LauncherAppsImpl.
1720            Preconditions.checkStringNotEmpty(packageName, "packageName");
1721            Preconditions.checkNotNull(shortcutIds, "shortcutIds");
1722
1723            synchronized (mLock) {
1724                getPackageShortcutsLocked(packageName, userId).replacePinned(
1725                        ShortcutService.this, callingPackage, shortcutIds);
1726            }
1727            userPackageChanged(packageName, userId);
1728        }
1729
1730        @Override
1731        public Intent createShortcutIntent(@NonNull String callingPackage,
1732                @NonNull String packageName, @NonNull String shortcutId, int userId) {
1733            // Calling permission must be checked by LauncherAppsImpl.
1734            Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1735            Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
1736
1737            synchronized (mLock) {
1738                final ShortcutInfo fullShortcut =
1739                        getPackageShortcutsLocked(packageName, userId)
1740                        .findShortcutById(shortcutId);
1741                return fullShortcut == null ? null : fullShortcut.getIntent();
1742            }
1743        }
1744
1745        @Override
1746        public void addListener(@NonNull ShortcutChangeListener listener) {
1747            synchronized (mLock) {
1748                mListeners.add(Preconditions.checkNotNull(listener));
1749            }
1750        }
1751
1752        @Override
1753        public int getShortcutIconResId(@NonNull String callingPackage,
1754                @NonNull ShortcutInfo shortcut, int userId) {
1755            Preconditions.checkNotNull(shortcut, "shortcut");
1756
1757            synchronized (mLock) {
1758                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1759                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1760                return (shortcutInfo != null && shortcutInfo.hasIconResource())
1761                        ? shortcutInfo.getIconResourceId() : 0;
1762            }
1763        }
1764
1765        @Override
1766        public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
1767                @NonNull ShortcutInfo shortcut, int userId) {
1768            Preconditions.checkNotNull(shortcut, "shortcut");
1769
1770            synchronized (mLock) {
1771                final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1772                        shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1773                if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
1774                    return null;
1775                }
1776                try {
1777                    return ParcelFileDescriptor.open(
1778                            new File(shortcutInfo.getBitmapPath()),
1779                            ParcelFileDescriptor.MODE_READ_ONLY);
1780                } catch (FileNotFoundException e) {
1781                    Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
1782                    return null;
1783                }
1784            }
1785        }
1786    }
1787
1788    // === Dump ===
1789
1790    @Override
1791    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1792        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1793                != PackageManager.PERMISSION_GRANTED) {
1794            pw.println("Permission Denial: can't dump UserManager from from pid="
1795                    + Binder.getCallingPid()
1796                    + ", uid=" + Binder.getCallingUid()
1797                    + " without permission "
1798                    + android.Manifest.permission.DUMP);
1799            return;
1800        }
1801        dumpInner(pw);
1802    }
1803
1804    @VisibleForTesting
1805    void dumpInner(PrintWriter pw) {
1806        synchronized (mLock) {
1807            final long now = injectCurrentTimeMillis();
1808            pw.print("Now: [");
1809            pw.print(now);
1810            pw.print("] ");
1811            pw.print(formatTime(now));
1812
1813            pw.print("  Raw last reset: [");
1814            pw.print(mRawLastResetTime);
1815            pw.print("] ");
1816            pw.print(formatTime(mRawLastResetTime));
1817
1818            final long last = getLastResetTimeLocked();
1819            pw.print("  Last reset: [");
1820            pw.print(last);
1821            pw.print("] ");
1822            pw.print(formatTime(last));
1823
1824            final long next = getNextResetTimeLocked();
1825            pw.print("  Next reset: [");
1826            pw.print(next);
1827            pw.print("] ");
1828            pw.print(formatTime(next));
1829            pw.println();
1830
1831            pw.print("  Max icon dim: ");
1832            pw.print(mMaxIconDimension);
1833            pw.print("  Icon format: ");
1834            pw.print(mIconPersistFormat);
1835            pw.print("  Icon quality: ");
1836            pw.print(mIconPersistQuality);
1837            pw.println();
1838
1839            pw.println();
1840
1841            for (int i = 0; i < mShortcuts.size(); i++) {
1842                dumpUserLocked(pw, mShortcuts.keyAt(i));
1843            }
1844        }
1845    }
1846
1847    private void dumpUserLocked(PrintWriter pw, int userId) {
1848        pw.print("  User: ");
1849        pw.print(userId);
1850        pw.println();
1851
1852        final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId);
1853        if (packages == null) {
1854            return;
1855        }
1856        for (int j = 0; j < packages.size(); j++) {
1857            dumpPackageLocked(pw, userId, packages.keyAt(j));
1858        }
1859        pw.println();
1860    }
1861
1862    private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
1863        final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName);
1864        if (packageShortcuts == null) {
1865            return;
1866        }
1867
1868        pw.print("    Package: ");
1869        pw.print(packageName);
1870        pw.println();
1871
1872        pw.print("      Calls: ");
1873        pw.print(packageShortcuts.getApiCallCount(this));
1874        pw.println();
1875
1876        // This should be after getApiCallCount(), which may update it.
1877        pw.print("      Last reset: [");
1878        pw.print(packageShortcuts.mLastResetTime);
1879        pw.print("] ");
1880        pw.print(formatTime(packageShortcuts.mLastResetTime));
1881        pw.println();
1882
1883        pw.println("      Shortcuts:");
1884        long totalBitmapSize = 0;
1885        final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
1886        final int size = shortcuts.size();
1887        for (int i = 0; i < size; i++) {
1888            final ShortcutInfo si = shortcuts.valueAt(i);
1889            pw.print("        ");
1890            pw.println(si.toInsecureString());
1891            if (si.hasIconFile()) {
1892                final long len = new File(si.getBitmapPath()).length();
1893                pw.print("          ");
1894                pw.print("bitmap size=");
1895                pw.println(len);
1896
1897                totalBitmapSize += len;
1898            }
1899        }
1900        pw.print("      Total bitmap size: ");
1901        pw.print(totalBitmapSize);
1902        pw.print(" (");
1903        pw.print(Formatter.formatFileSize(mContext, totalBitmapSize));
1904        pw.println(")");
1905    }
1906
1907    private static String formatTime(long time) {
1908        Time tobj = new Time();
1909        tobj.set(time);
1910        return tobj.format("%Y-%m-%d %H:%M:%S");
1911    }
1912
1913    // === Shell support ===
1914
1915    @Override
1916    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1917            String[] args, ResultReceiver resultReceiver) throws RemoteException {
1918
1919        enforceShell();
1920
1921        (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1922    }
1923
1924    /**
1925     * Handle "adb shell cmd".
1926     */
1927    private class MyShellCommand extends ShellCommand {
1928        @Override
1929        public int onCommand(String cmd) {
1930            if (cmd == null) {
1931                return handleDefaultCommands(cmd);
1932            }
1933            final PrintWriter pw = getOutPrintWriter();
1934            int ret = 1;
1935            switch (cmd) {
1936                case "reset-package-throttling":
1937                    ret = handleResetPackageThrottling();
1938                    break;
1939                case "reset-throttling":
1940                    ret = handleResetThrottling();
1941                    break;
1942                case "override-config":
1943                    ret = handleOverrideConfig();
1944                    break;
1945                case "reset-config":
1946                    ret = handleResetConfig();
1947                    break;
1948                default:
1949                    return handleDefaultCommands(cmd);
1950            }
1951            if (ret == 0) {
1952                pw.println("Success");
1953            }
1954            return ret;
1955        }
1956
1957        @Override
1958        public void onHelp() {
1959            final PrintWriter pw = getOutPrintWriter();
1960            pw.println("Usage: cmd shortcut COMMAND [options ...]");
1961            pw.println();
1962            pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1963            pw.println("    Reset throttling for a package");
1964            pw.println();
1965            pw.println("cmd shortcut reset-throttling");
1966            pw.println("    Reset throttling for all packages and users");
1967            pw.println();
1968            pw.println("cmd shortcut override-config CONFIG");
1969            pw.println("    Override the configuration for testing (will last until reboot)");
1970            pw.println();
1971            pw.println("cmd shortcut reset-config");
1972            pw.println("    Reset the configuration set with \"update-config\"");
1973            pw.println();
1974        }
1975
1976        private int handleResetThrottling() {
1977            resetThrottling();
1978            return 0;
1979        }
1980
1981        private int handleResetPackageThrottling() {
1982            final PrintWriter pw = getOutPrintWriter();
1983
1984            int userId = UserHandle.USER_SYSTEM;
1985            String opt;
1986            while ((opt = getNextOption()) != null) {
1987                switch (opt) {
1988                    case "--user":
1989                        userId = UserHandle.parseUserArg(getNextArgRequired());
1990                        break;
1991                    default:
1992                        pw.println("Error: Unknown option: " + opt);
1993                        return 1;
1994                }
1995            }
1996            final String packageName = getNextArgRequired();
1997
1998            synchronized (mLock) {
1999                getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine();
2000                saveUserLocked(userId);
2001            }
2002
2003            return 0;
2004        }
2005
2006        private int handleOverrideConfig() {
2007            final PrintWriter pw = getOutPrintWriter();
2008            final String config = getNextArgRequired();
2009
2010            synchronized (mLock) {
2011                if (!updateConfigurationLocked(config)) {
2012                    pw.println("override-config failed.  See logcat for details.");
2013                    return 1;
2014                }
2015            }
2016            return 0;
2017        }
2018
2019        private int handleResetConfig() {
2020            synchronized (mLock) {
2021                loadConfigurationLocked();
2022            }
2023            return 0;
2024        }
2025    }
2026
2027    // === Unit test support ===
2028
2029    // Injection point.
2030    long injectCurrentTimeMillis() {
2031        return System.currentTimeMillis();
2032    }
2033
2034    // Injection point.
2035    int injectBinderCallingUid() {
2036        return getCallingUid();
2037    }
2038
2039    File injectSystemDataPath() {
2040        return Environment.getDataSystemDirectory();
2041    }
2042
2043    File injectUserDataPath(@UserIdInt int userId) {
2044        return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
2045    }
2046
2047    @VisibleForTesting
2048    boolean injectIsLowRamDevice() {
2049        return ActivityManager.isLowRamDeviceStatic();
2050    }
2051
2052    File getUserBitmapFilePath(@UserIdInt int userId) {
2053        return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
2054    }
2055
2056    @VisibleForTesting
2057    SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() {
2058        return mShortcuts;
2059    }
2060
2061    @VisibleForTesting
2062    int getMaxDynamicShortcutsForTest() {
2063        return mMaxDynamicShortcuts;
2064    }
2065
2066    @VisibleForTesting
2067    int getMaxDailyUpdatesForTest() {
2068        return mMaxDailyUpdates;
2069    }
2070
2071    @VisibleForTesting
2072    long getResetIntervalForTest() {
2073        return mResetInterval;
2074    }
2075
2076    @VisibleForTesting
2077    int getMaxIconDimensionForTest() {
2078        return mMaxIconDimension;
2079    }
2080
2081    @VisibleForTesting
2082    CompressFormat getIconPersistFormatForTest() {
2083        return mIconPersistFormat;
2084    }
2085
2086    @VisibleForTesting
2087    int getIconPersistQualityForTest() {
2088        return mIconPersistQuality;
2089    }
2090}
2091