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