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