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