ShortcutPackage.java revision 20c95f854e54b71caa49f0efe07d47d1e6afd435
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.content.ComponentName;
22import android.content.Intent;
23import android.content.pm.ShortcutInfo;
24import android.os.PersistableBundle;
25import android.text.format.Formatter;
26import android.util.ArrayMap;
27import android.util.ArraySet;
28import android.util.Slog;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.XmlUtils;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35import org.xmlpull.v1.XmlSerializer;
36
37import java.io.File;
38import java.io.IOException;
39import java.io.PrintWriter;
40import java.util.ArrayList;
41import java.util.List;
42import java.util.Set;
43import java.util.function.Predicate;
44
45/**
46 * Package information used by {@link ShortcutService}.
47 */
48class ShortcutPackage extends ShortcutPackageItem {
49    private static final String TAG = ShortcutService.TAG;
50
51    static final String TAG_ROOT = "package";
52    private static final String TAG_INTENT_EXTRAS = "intent-extras";
53    private static final String TAG_EXTRAS = "extras";
54    private static final String TAG_SHORTCUT = "shortcut";
55    private static final String TAG_CATEGORIES = "categories";
56
57    private static final String ATTR_NAME = "name";
58    private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
59    private static final String ATTR_CALL_COUNT = "call-count";
60    private static final String ATTR_LAST_RESET = "last-reset";
61    private static final String ATTR_ID = "id";
62    private static final String ATTR_ACTIVITY = "activity";
63    private static final String ATTR_TITLE = "title";
64    private static final String ATTR_TITLE_RES_ID = "titleid";
65    private static final String ATTR_TEXT = "text";
66    private static final String ATTR_TEXT_RES_ID = "textid";
67    private static final String ATTR_DISABLED_MESSAGE = "dmessage";
68    private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
69    private static final String ATTR_INTENT = "intent";
70    private static final String ATTR_RANK = "rank";
71    private static final String ATTR_TIMESTAMP = "timestamp";
72    private static final String ATTR_FLAGS = "flags";
73    private static final String ATTR_ICON_RES = "icon-res";
74    private static final String ATTR_BITMAP_PATH = "bitmap-path";
75
76    private static final String NAME_CATEGORIES = "categories";
77
78    private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
79    private static final String ATTR_NAME_XMLUTILS = "name";
80
81    /**
82     * All the shortcuts from the package, keyed on IDs.
83     */
84    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
85
86    /**
87     * # of dynamic shortcuts.
88     */
89    private int mDynamicShortcutCount = 0;
90
91    /**
92     * # of times the package has called rate-limited APIs.
93     */
94    private int mApiCallCount;
95
96    /**
97     * When {@link #mApiCallCount} was reset last time.
98     */
99    private long mLastResetTime;
100
101    private final int mPackageUid;
102
103    private long mLastKnownForegroundElapsedTime;
104
105    private ShortcutPackage(ShortcutUser shortcutUser,
106            int packageUserId, String packageName, ShortcutPackageInfo spi) {
107        super(shortcutUser, packageUserId, packageName,
108                spi != null ? spi : ShortcutPackageInfo.newEmpty());
109
110        mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
111    }
112
113    public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
114        this(shortcutUser, packageUserId, packageName, null);
115    }
116
117    @Override
118    public int getOwnerUserId() {
119        // For packages, always owner user == package user.
120        return getPackageUserId();
121    }
122
123    public int getPackageUid() {
124        return mPackageUid;
125    }
126
127    /**
128     * Called when a shortcut is about to be published.  At this point we know the publisher package
129     * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so
130     * we do some initialization for the package.
131     */
132    private void onShortcutPublish() {
133        // Make sure we have the version code for the app.  We need the version code in
134        // handlePackageUpdated().
135        if (getPackageInfo().getVersionCode() < 0) {
136            final ShortcutService s = mShortcutUser.mService;
137
138            final int versionCode = s.getApplicationVersionCode(getPackageName(), getOwnerUserId());
139            if (ShortcutService.DEBUG) {
140                Slog.d(TAG, String.format("Package %s version = %d", getPackageName(),
141                        versionCode));
142            }
143            if (versionCode >= 0) {
144                getPackageInfo().setVersionCode(versionCode);
145                s.scheduleSaveUser(getOwnerUserId());
146            }
147        }
148    }
149
150    @Override
151    protected void onRestoreBlocked() {
152        // Can't restore due to version/signature mismatch.  Remove all shortcuts.
153        mShortcuts.clear();
154    }
155
156    @Override
157    protected void onRestored() {
158        // Because some launchers may not have been restored (e.g. allowBackup=false),
159        // we need to re-calculate the pinned shortcuts.
160        refreshPinnedFlags();
161    }
162
163    /**
164     * Note this does *not* provide a correct view to the calling launcher.
165     */
166    @Nullable
167    public ShortcutInfo findShortcutById(String id) {
168        return mShortcuts.get(id);
169    }
170
171    private ShortcutInfo deleteShortcut(@NonNull String id) {
172        final ShortcutInfo shortcut = mShortcuts.remove(id);
173        if (shortcut != null) {
174            mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut);
175            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
176        }
177        return shortcut;
178    }
179
180    void addShortcut(@NonNull ShortcutInfo newShortcut) {
181        deleteShortcut(newShortcut.getId());
182        mShortcutUser.mService.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
183        mShortcuts.put(newShortcut.getId(), newShortcut);
184    }
185
186    /**
187     * Add a shortcut, or update one with the same ID, with taking over existing flags.
188     *
189     * It checks the max number of dynamic shortcuts.
190     */
191    public void addDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
192
193        onShortcutPublish();
194
195        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
196
197        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
198
199        final boolean wasPinned;
200        final int newDynamicCount;
201
202        if (oldShortcut == null) {
203            wasPinned = false;
204            newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
205        } else {
206            wasPinned = oldShortcut.isPinned();
207            if (oldShortcut.isDynamic()) {
208                newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut.
209            } else {
210                newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut.
211            }
212        }
213
214        // Make sure there's still room.
215        mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount);
216
217        // Okay, make it dynamic and add.
218        if (wasPinned) {
219            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
220        }
221
222        addShortcut(newShortcut);
223        mDynamicShortcutCount = newDynamicCount;
224    }
225
226    /**
227     * Remove all shortcuts that aren't pinned nor dynamic.
228     */
229    private void removeOrphans() {
230        ArrayList<String> removeList = null; // Lazily initialize.
231
232        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
233            final ShortcutInfo si = mShortcuts.valueAt(i);
234
235            if (si.isPinned() || si.isDynamic()) continue;
236
237            if (removeList == null) {
238                removeList = new ArrayList<>();
239            }
240            removeList.add(si.getId());
241        }
242        if (removeList != null) {
243            for (int i = removeList.size() - 1; i >= 0; i--) {
244                deleteShortcut(removeList.get(i));
245            }
246        }
247    }
248
249    /**
250     * Remove all dynamic shortcuts.
251     */
252    public void deleteAllDynamicShortcuts() {
253        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
254            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
255        }
256        removeOrphans();
257        mDynamicShortcutCount = 0;
258    }
259
260    /**
261     * Remove a dynamic shortcut by ID.
262     */
263    public void deleteDynamicWithId(@NonNull String shortcutId) {
264        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
265
266        if (oldShortcut == null) {
267            return;
268        }
269        if (oldShortcut.isDynamic()) {
270            mDynamicShortcutCount--;
271        }
272        if (oldShortcut.isPinned()) {
273            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
274        } else {
275            deleteShortcut(shortcutId);
276        }
277    }
278
279    /**
280     * Called after a launcher updates the pinned set.  For each shortcut in this package,
281     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
282     *
283     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
284     */
285    public void refreshPinnedFlags() {
286        // First, un-pin all shortcuts
287        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
288            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
289        }
290
291        // Then, for the pinned set for each launcher, set the pin flag one by one.
292        mShortcutUser.mService.getUserShortcutsLocked(getPackageUserId())
293                .forAllLaunchers(launcherShortcuts -> {
294            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
295                    getPackageName(), getPackageUserId());
296
297            if (pinned == null || pinned.size() == 0) {
298                return;
299            }
300            for (int i = pinned.size() - 1; i >= 0; i--) {
301                final String id = pinned.valueAt(i);
302                final ShortcutInfo si = mShortcuts.get(id);
303                if (si == null) {
304                    // This happens if a launcher pinned shortcuts from this package, then backup&
305                    // restored, but this package doesn't allow backing up.
306                    // In that case the launcher ends up having a dangling pinned shortcuts.
307                    // That's fine, when the launcher is restored, we'll fix it.
308                    continue;
309                }
310                si.addFlags(ShortcutInfo.FLAG_PINNED);
311            }
312        });
313
314        // Lastly, remove the ones that are no longer pinned nor dynamic.
315        removeOrphans();
316    }
317
318    /**
319     * Number of calls that the caller has made, since the last reset.
320     *
321     * <p>This takes care of the resetting the counter for foreground apps as well as after
322     * locale changes.
323     */
324    public int getApiCallCount() {
325        mShortcutUser.resetThrottlingIfNeeded();
326
327        final ShortcutService s = mShortcutUser.mService;
328
329        // Reset the counter if:
330        // - the package is in foreground now.
331        // - the package is *not* in foreground now, but was in foreground at some point
332        // since the previous time it had been.
333        if (s.isUidForegroundLocked(mPackageUid)
334                || mLastKnownForegroundElapsedTime
335                    < s.getUidLastForegroundElapsedTimeLocked(mPackageUid)) {
336            mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
337            resetRateLimiting();
338        }
339
340        // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
341        // but we just can't return 0 at this point, because we may have to update
342        // mLastResetTime.
343
344        final long last = s.getLastResetTimeLocked();
345
346        final long now = s.injectCurrentTimeMillis();
347        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
348            Slog.w(TAG, "Clock rewound");
349            // Clock rewound.
350            mLastResetTime = now;
351            mApiCallCount = 0;
352            return mApiCallCount;
353        }
354
355        // If not reset yet, then reset.
356        if (mLastResetTime < last) {
357            if (ShortcutService.DEBUG) {
358                Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
359                        getPackageName(), mLastResetTime, now, last));
360            }
361            mApiCallCount = 0;
362            mLastResetTime = last;
363        }
364        return mApiCallCount;
365    }
366
367    /**
368     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
369     * and return true.  Otherwise just return false.
370     *
371     * <p>This takes care of the resetting the counter for foreground apps as well as after
372     * locale changes, which is done internally by {@link #getApiCallCount}.
373     */
374    public boolean tryApiCall() {
375        final ShortcutService s = mShortcutUser.mService;
376
377        if (getApiCallCount() >= s.mMaxUpdatesPerInterval) {
378            return false;
379        }
380        mApiCallCount++;
381        s.scheduleSaveUser(getOwnerUserId());
382        return true;
383    }
384
385    public void resetRateLimiting() {
386        if (ShortcutService.DEBUG) {
387            Slog.d(TAG, "resetRateLimiting: " + getPackageName());
388        }
389        if (mApiCallCount > 0) {
390            mApiCallCount = 0;
391            mShortcutUser.mService.scheduleSaveUser(getOwnerUserId());
392        }
393    }
394
395    public void resetRateLimitingForCommandLineNoSaving() {
396        mApiCallCount = 0;
397        mLastResetTime = 0;
398    }
399
400    /**
401     * Find all shortcuts that match {@code query}.
402     */
403    public void findAll(@NonNull List<ShortcutInfo> result,
404            @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
405        findAll(result, query, cloneFlag, null, 0);
406    }
407
408    /**
409     * Find all shortcuts that match {@code query}.
410     *
411     * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
412     * by the calling launcher will not be included in the result, and also "isPinned" will be
413     * adjusted for the caller too.
414     */
415    public void findAll(@NonNull List<ShortcutInfo> result,
416            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
417            @Nullable String callingLauncher, int launcherUserId) {
418        if (getPackageInfo().isShadow()) {
419            // Restored and the app not installed yet, so don't return any.
420            return;
421        }
422
423        final ShortcutService s = mShortcutUser.mService;
424
425        // Set of pinned shortcuts by the calling launcher.
426        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
427                : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
428                    .getPinnedShortcutIds(getPackageName(), getPackageUserId());
429
430        for (int i = 0; i < mShortcuts.size(); i++) {
431            final ShortcutInfo si = mShortcuts.valueAt(i);
432
433            // If it's called by non-launcher (i.e. publisher, always include -> true.
434            // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned
435            // it.
436            final boolean isPinnedByCaller = (callingLauncher == null)
437                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
438            if (!si.isDynamic()) {
439                if (!si.isPinned()) {
440                    s.wtf("Shortcut not pinned: package " + getPackageName()
441                            + ", user=" + getPackageUserId() + ", id=" + si.getId());
442                    continue;
443                }
444                if (!isPinnedByCaller) {
445                    continue;
446                }
447            }
448            final ShortcutInfo clone = si.clone(cloneFlag);
449            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
450            // since it may check isPinned.
451            if (!isPinnedByCaller) {
452                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
453            }
454            if (query == null || query.test(clone)) {
455                result.add(clone);
456            }
457        }
458    }
459
460    public void resetThrottling() {
461        mApiCallCount = 0;
462    }
463
464    /**
465     * Return the filenames (excluding path names) of icon bitmap files from this package.
466     */
467    public ArraySet<String> getUsedBitmapFiles() {
468        final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size());
469
470        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
471            final ShortcutInfo si = mShortcuts.valueAt(i);
472            if (si.getBitmapPath() != null) {
473                usedFiles.add(getFileName(si.getBitmapPath()));
474            }
475        }
476        return usedFiles;
477    }
478
479    private static String getFileName(@NonNull String path) {
480        final int sep = path.lastIndexOf(File.separatorChar);
481        if (sep == -1) {
482            return path;
483        } else {
484            return path.substring(sep + 1);
485        }
486    }
487
488    /**
489     * Called when the package is updated.  If there are shortcuts with resource icons, update
490     * their timestamps.
491     */
492    public void handlePackageUpdated(int newVersionCode) {
493        if (getPackageInfo().getVersionCode() >= newVersionCode) {
494            // Version hasn't changed; nothing to do.
495            return;
496        }
497        if (ShortcutService.DEBUG) {
498            Slog.d(TAG, String.format("Package %s updated, version %d -> %d", getPackageName(),
499                    getPackageInfo().getVersionCode(), newVersionCode));
500        }
501
502        getPackageInfo().setVersionCode(newVersionCode);
503
504        final ShortcutService s = mShortcutUser.mService;
505
506        boolean changed = false;
507        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
508            final ShortcutInfo si = mShortcuts.valueAt(i);
509
510            if (si.hasAnyResources()) {
511                changed = true;
512                si.setTimestamp(s.injectCurrentTimeMillis());
513            }
514        }
515        if (changed) {
516            // This will send a notification to the launcher, and also save .
517            s.packageShortcutsChanged(getPackageName(), getPackageUserId());
518        } else {
519            // Still save the version code.
520            s.scheduleSaveUser(getPackageUserId());
521        }
522    }
523
524    public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
525        pw.println();
526
527        pw.print(prefix);
528        pw.print("Package: ");
529        pw.print(getPackageName());
530        pw.print("  UID: ");
531        pw.print(mPackageUid);
532        pw.println();
533
534        pw.print(prefix);
535        pw.print("  ");
536        pw.print("Calls: ");
537        pw.print(getApiCallCount());
538        pw.println();
539
540        // getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
541        pw.print(prefix);
542        pw.print("  ");
543        pw.print("Last known FG: ");
544        pw.print(mLastKnownForegroundElapsedTime);
545        pw.println();
546
547        // This should be after getApiCallCount(), which may update it.
548        pw.print(prefix);
549        pw.print("  ");
550        pw.print("Last reset: [");
551        pw.print(mLastResetTime);
552        pw.print("] ");
553        pw.print(ShortcutService.formatTime(mLastResetTime));
554        pw.println();
555
556        getPackageInfo().dump(pw, prefix + "  ");
557        pw.println();
558
559        pw.print(prefix);
560        pw.println("  Shortcuts:");
561        long totalBitmapSize = 0;
562        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
563        final int size = shortcuts.size();
564        for (int i = 0; i < size; i++) {
565            final ShortcutInfo si = shortcuts.valueAt(i);
566            pw.print(prefix);
567            pw.print("    ");
568            pw.println(si.toInsecureString());
569            if (si.getBitmapPath() != null) {
570                final long len = new File(si.getBitmapPath()).length();
571                pw.print(prefix);
572                pw.print("      ");
573                pw.print("bitmap size=");
574                pw.println(len);
575
576                totalBitmapSize += len;
577            }
578        }
579        pw.print(prefix);
580        pw.print("  ");
581        pw.print("Total bitmap size: ");
582        pw.print(totalBitmapSize);
583        pw.print(" (");
584        pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize));
585        pw.println(")");
586    }
587
588    @Override
589    public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
590            throws IOException, XmlPullParserException {
591        final int size = mShortcuts.size();
592
593        if (size == 0 && mApiCallCount == 0) {
594            return; // nothing to write.
595        }
596
597        out.startTag(null, TAG_ROOT);
598
599        ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
600        ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
601        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
602        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
603        getPackageInfo().saveToXml(out);
604
605        for (int j = 0; j < size; j++) {
606            saveShortcut(out, mShortcuts.valueAt(j), forBackup);
607        }
608
609        out.endTag(null, TAG_ROOT);
610    }
611
612    private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
613            throws IOException, XmlPullParserException {
614        if (forBackup) {
615            if (!si.isPinned()) {
616                return; // Backup only pinned icons.
617            }
618        }
619        out.startTag(null, TAG_SHORTCUT);
620        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
621        // writeAttr(out, "package", si.getPackageName()); // not needed
622        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
623        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
624        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
625        ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
626        ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
627        ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
628        ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
629        ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID, si.getDisabledMessageResId());
630        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
631        ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
632        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
633                si.getLastChangedTimestamp());
634        if (forBackup) {
635            // Don't write icon information.  Also drop the dynamic flag.
636            ShortcutService.writeAttr(out, ATTR_FLAGS,
637                    si.getFlags() &
638                            ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
639                            | ShortcutInfo.FLAG_DYNAMIC));
640        } else {
641            ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
642            ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
643            ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
644        }
645
646        {
647            final Set<String> cat = si.getCategories();
648            if (cat != null && cat.size() > 0) {
649                out.startTag(null, TAG_CATEGORIES);
650                XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
651                        NAME_CATEGORIES, out);
652                out.endTag(null, TAG_CATEGORIES);
653            }
654        }
655
656        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
657                si.getIntentPersistableExtras());
658        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
659
660        out.endTag(null, TAG_SHORTCUT);
661    }
662
663    public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
664            XmlPullParser parser, boolean fromBackup)
665            throws IOException, XmlPullParserException {
666
667        final String packageName = ShortcutService.parseStringAttribute(parser,
668                ATTR_NAME);
669
670        final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
671                shortcutUser.getUserId(), packageName);
672
673        ret.mDynamicShortcutCount =
674                ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT);
675        ret.mApiCallCount =
676                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
677        ret.mLastResetTime =
678                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
679
680        final int outerDepth = parser.getDepth();
681        int type;
682        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
683                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
684            if (type != XmlPullParser.START_TAG) {
685                continue;
686            }
687            final int depth = parser.getDepth();
688            final String tag = parser.getName();
689            if (depth == outerDepth + 1) {
690                switch (tag) {
691                    case ShortcutPackageInfo.TAG_ROOT:
692                        ret.getPackageInfo().loadFromXml(parser, fromBackup);
693                        continue;
694                    case TAG_SHORTCUT:
695                        final ShortcutInfo si = parseShortcut(parser, packageName,
696                                shortcutUser.getUserId());
697
698                        // Don't use addShortcut(), we don't need to save the icon.
699                        ret.mShortcuts.put(si.getId(), si);
700                        continue;
701                }
702            }
703            ShortcutService.warnForInvalidTag(depth, tag);
704        }
705        return ret;
706    }
707
708    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
709            @UserIdInt int userId) throws IOException, XmlPullParserException {
710        String id;
711        ComponentName activityComponent;
712        // Icon icon;
713        String title;
714        int titleResId;
715        String text;
716        int textResId;
717        String disabledMessage;
718        int disabledMessageResId;
719        Intent intent;
720        PersistableBundle intentPersistableExtras = null;
721        int rank;
722        PersistableBundle extras = null;
723        long lastChangedTimestamp;
724        int flags;
725        int iconRes;
726        String bitmapPath;
727        ArraySet<String> categories = null;
728
729        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
730        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
731                ATTR_ACTIVITY);
732        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
733        titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
734        text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
735        textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
736        disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
737        disabledMessageResId = ShortcutService.parseIntAttribute(parser,
738                ATTR_DISABLED_MESSAGE_RES_ID);
739        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
740        rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
741        lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
742        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
743        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
744        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
745
746        final int outerDepth = parser.getDepth();
747        int type;
748        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
749                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
750            if (type != XmlPullParser.START_TAG) {
751                continue;
752            }
753            final int depth = parser.getDepth();
754            final String tag = parser.getName();
755            if (ShortcutService.DEBUG_LOAD) {
756                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
757                        depth, type, tag));
758            }
759            switch (tag) {
760                case TAG_INTENT_EXTRAS:
761                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
762                    continue;
763                case TAG_EXTRAS:
764                    extras = PersistableBundle.restoreFromXml(parser);
765                    continue;
766                case TAG_CATEGORIES:
767                    // This just contains string-array.
768                    continue;
769                case TAG_STRING_ARRAY_XMLUTILS:
770                    if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
771                            ATTR_NAME_XMLUTILS))) {
772                        final String[] ar = XmlUtils.readThisStringArrayXml(
773                                parser, TAG_STRING_ARRAY_XMLUTILS, null);
774                        categories = new ArraySet<>(ar.length);
775                        for (int i = 0; i < ar.length; i++) {
776                            categories.add(ar[i]);
777                        }
778                    }
779                    continue;
780            }
781            throw ShortcutService.throwForInvalidTag(depth, tag);
782        }
783
784        return new ShortcutInfo(
785                userId, id, packageName, activityComponent, /* icon =*/ null,
786                title, titleResId, text, textResId, disabledMessage, disabledMessageResId,
787                categories, intent,
788                intentPersistableExtras, rank, extras, lastChangedTimestamp, flags,
789                iconRes, bitmapPath);
790    }
791
792    @VisibleForTesting
793    List<ShortcutInfo> getAllShortcutsForTest() {
794        return new ArrayList<>(mShortcuts.values());
795    }
796}
797