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