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