ShortcutPackage.java revision eddbfecb8dd751161339a9ed16d07ce2e108a575
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.PackageInfo;
24import android.content.pm.ShortcutInfo;
25import android.os.PersistableBundle;
26import android.text.format.Formatter;
27import android.util.ArrayMap;
28import android.util.ArraySet;
29import android.util.Slog;
30
31import com.android.internal.annotations.VisibleForTesting;
32import com.android.internal.util.Preconditions;
33import com.android.internal.util.XmlUtils;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37import org.xmlpull.v1.XmlSerializer;
38
39import java.io.File;
40import java.io.IOException;
41import java.io.PrintWriter;
42import java.util.ArrayList;
43import java.util.List;
44import java.util.Set;
45import java.util.function.Predicate;
46
47/**
48 * Package information used by {@link ShortcutService}.
49 * User information used by {@link ShortcutService}.
50 *
51 * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
52 *
53 * TODO Max dynamic shortcuts cap should be per activity.
54 */
55class ShortcutPackage extends ShortcutPackageItem {
56    private static final String TAG = ShortcutService.TAG;
57
58    static final String TAG_ROOT = "package";
59    private static final String TAG_INTENT_EXTRAS = "intent-extras";
60    private static final String TAG_EXTRAS = "extras";
61    private static final String TAG_SHORTCUT = "shortcut";
62    private static final String TAG_CATEGORIES = "categories";
63
64    private static final String ATTR_NAME = "name";
65    private static final String ATTR_CALL_COUNT = "call-count";
66    private static final String ATTR_LAST_RESET = "last-reset";
67    private static final String ATTR_ID = "id";
68    private static final String ATTR_ACTIVITY = "activity";
69    private static final String ATTR_TITLE = "title";
70    private static final String ATTR_TITLE_RES_ID = "titleid";
71    private static final String ATTR_TEXT = "text";
72    private static final String ATTR_TEXT_RES_ID = "textid";
73    private static final String ATTR_DISABLED_MESSAGE = "dmessage";
74    private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
75    private static final String ATTR_INTENT = "intent";
76    private static final String ATTR_RANK = "rank";
77    private static final String ATTR_TIMESTAMP = "timestamp";
78    private static final String ATTR_FLAGS = "flags";
79    private static final String ATTR_ICON_RES = "icon-res";
80    private static final String ATTR_BITMAP_PATH = "bitmap-path";
81
82    private static final String NAME_CATEGORIES = "categories";
83
84    private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
85    private static final String ATTR_NAME_XMLUTILS = "name";
86
87    /**
88     * All the shortcuts from the package, keyed on IDs.
89     */
90    final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
91
92    /**
93     * # of times the package has called rate-limited APIs.
94     */
95    private int mApiCallCount;
96
97    /**
98     * When {@link #mApiCallCount} was reset last time.
99     */
100    private long mLastResetTime;
101
102    private final int mPackageUid;
103
104    private long mLastKnownForegroundElapsedTime;
105
106    private ShortcutPackage(ShortcutUser shortcutUser,
107            int packageUserId, String packageName, ShortcutPackageInfo spi) {
108        super(shortcutUser, packageUserId, packageName,
109                spi != null ? spi : ShortcutPackageInfo.newEmpty());
110
111        mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
112    }
113
114    public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
115        this(shortcutUser, packageUserId, packageName, null);
116    }
117
118    @Override
119    public int getOwnerUserId() {
120        // For packages, always owner user == package user.
121        return getPackageUserId();
122    }
123
124    public int getPackageUid() {
125        return mPackageUid;
126    }
127
128    /**
129     * Called when a shortcut is about to be published.  At this point we know the publisher package
130     * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so
131     * we do some initialization for the package.
132     */
133    private void ensurePackageVersionInfo() {
134        // Make sure we have the version code for the app.  We need the version code in
135        // handlePackageUpdated().
136        if (getPackageInfo().getVersionCode() < 0) {
137            final ShortcutService s = mShortcutUser.mService;
138
139            final PackageInfo pi = s.getPackageInfo(getPackageName(), getOwnerUserId());
140            if (pi != null) {
141                if (ShortcutService.DEBUG) {
142                    Slog.d(TAG, String.format("Package %s version = %d", getPackageName(),
143                            pi.versionCode));
144                }
145                getPackageInfo().updateVersionInfo(pi);
146                s.scheduleSaveUser(getOwnerUserId());
147            }
148        }
149    }
150
151    @Override
152    protected void onRestoreBlocked() {
153        // Can't restore due to version/signature mismatch.  Remove all shortcuts.
154        mShortcuts.clear();
155    }
156
157    @Override
158    protected void onRestored() {
159        // Because some launchers may not have been restored (e.g. allowBackup=false),
160        // we need to re-calculate the pinned shortcuts.
161        refreshPinnedFlags();
162    }
163
164    /**
165     * Note this does *not* provide a correct view to the calling launcher.
166     */
167    @Nullable
168    public ShortcutInfo findShortcutById(String id) {
169        return mShortcuts.get(id);
170    }
171
172    private void ensureNotImmutable(@Nullable ShortcutInfo shortcut) {
173        if (shortcut != null && shortcut.isImmutable()) {
174            throw new IllegalArgumentException(
175                    "Manifest shortcut ID=" + shortcut.getId()
176                            + " may not be manipulated via APIs");
177        }
178    }
179
180    private void ensureNotImmutable(@NonNull String id) {
181        ensureNotImmutable(mShortcuts.get(id));
182    }
183
184    public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds) {
185        for (int i = shortcutIds.size() - 1; i >= 0; i--) {
186            ensureNotImmutable(shortcutIds.get(i));
187        }
188    }
189
190    public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts) {
191        for (int i = shortcuts.size() - 1; i >= 0; i--) {
192            ensureNotImmutable(shortcuts.get(i).getId());
193        }
194    }
195
196    private ShortcutInfo deleteShortcutInner(@NonNull String id) {
197        final ShortcutInfo shortcut = mShortcuts.remove(id);
198        if (shortcut != null) {
199            mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut);
200            shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
201                    | ShortcutInfo.FLAG_MANIFEST);
202        }
203        return shortcut;
204    }
205
206    private void addShortcutInner(@NonNull ShortcutInfo newShortcut) {
207        deleteShortcutInner(newShortcut.getId());
208        mShortcutUser.mService.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
209        mShortcuts.put(newShortcut.getId(), newShortcut);
210    }
211
212    /**
213     * Add a shortcut, or update one with the same ID, with taking over existing flags.
214     *
215     * It checks the max number of dynamic shortcuts.
216     */
217    public void addOrUpdateDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
218
219        Preconditions.checkArgument(newShortcut.isEnabled(),
220                "add/setDynamicShortcuts() cannot publish disabled shortcuts");
221
222        ensurePackageVersionInfo();
223
224        newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
225
226        final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
227
228        final boolean wasPinned;
229
230        if (oldShortcut == null) {
231            wasPinned = false;
232        } else {
233            // It's an update case.
234            // Make sure the target is updatable. (i.e. should be mutable.)
235            oldShortcut.ensureUpdatableWith(newShortcut);
236
237            wasPinned = oldShortcut.isPinned();
238            if (!oldShortcut.isEnabled()) {
239                newShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
240            }
241        }
242
243        // TODO Check max dynamic count.
244        // mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount);
245
246        // Okay, make it dynamic and add.
247        if (wasPinned) {
248            newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
249        }
250
251        addShortcutInner(newShortcut);
252    }
253
254    /**
255     * Remove all shortcuts that aren't pinned nor dynamic.
256     */
257    private void removeOrphans() {
258        ArrayList<String> removeList = null; // Lazily initialize.
259
260        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
261            final ShortcutInfo si = mShortcuts.valueAt(i);
262
263            if (si.isAlive()) continue;
264
265            if (removeList == null) {
266                removeList = new ArrayList<>();
267            }
268            removeList.add(si.getId());
269        }
270        if (removeList != null) {
271            for (int i = removeList.size() - 1; i >= 0; i--) {
272                deleteShortcutInner(removeList.get(i));
273            }
274        }
275    }
276
277    /**
278     * Remove all dynamic shortcuts.
279     */
280    public void deleteAllDynamicShortcuts() {
281        boolean changed = false;
282        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
283            final ShortcutInfo si = mShortcuts.valueAt(i);
284            if (si.isDynamic()) {
285                changed = true;
286                si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
287            }
288        }
289        if (changed) {
290            removeOrphans();
291        }
292    }
293
294    /**
295     * Remove a dynamic shortcut by ID.
296     */
297    public void deleteDynamicWithId(@NonNull String shortcutId) {
298        deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
299    }
300
301    public void disableWithId(@NonNull String shortcutId, String disabledMessage,
302            int disabledMessageResId, boolean overrideImmutable) {
303        final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
304                overrideImmutable);
305
306        if (disabled != null) {
307            if (disabledMessage != null) {
308                disabled.setDisabledMessage(disabledMessage);
309            } else if (disabledMessageResId != 0) {
310                disabled.setDisabledMessageResId(disabledMessageResId);
311            }
312        }
313    }
314
315    @Nullable
316    private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable,
317            boolean overrideImmutable) {
318        final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
319
320        if (oldShortcut == null || !oldShortcut.isEnabled()) {
321            return null; // Doesn't exist or already disabled.
322        }
323        if (!overrideImmutable) {
324            ensureNotImmutable(oldShortcut);
325        }
326        if (oldShortcut.isPinned()) {
327            oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST);
328            if (disable) {
329                oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
330            }
331            return oldShortcut;
332        } else {
333            deleteShortcutInner(shortcutId);
334            return null;
335        }
336    }
337
338    public void enableWithId(@NonNull String shortcutId) {
339        final ShortcutInfo shortcut = mShortcuts.get(shortcutId);
340        if (shortcut != null) {
341            ensureNotImmutable(shortcut);
342            shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED);
343        }
344    }
345
346    /**
347     * Called after a launcher updates the pinned set.  For each shortcut in this package,
348     * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
349     *
350     * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
351     */
352    public void refreshPinnedFlags() {
353        // First, un-pin all shortcuts
354        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
355            mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
356        }
357
358        // Then, for the pinned set for each launcher, set the pin flag one by one.
359        mShortcutUser.mService.getUserShortcutsLocked(getPackageUserId())
360                .forAllLaunchers(launcherShortcuts -> {
361            final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
362                    getPackageName(), getPackageUserId());
363
364            if (pinned == null || pinned.size() == 0) {
365                return;
366            }
367            for (int i = pinned.size() - 1; i >= 0; i--) {
368                final String id = pinned.valueAt(i);
369                final ShortcutInfo si = mShortcuts.get(id);
370                if (si == null) {
371                    // This happens if a launcher pinned shortcuts from this package, then backup&
372                    // restored, but this package doesn't allow backing up.
373                    // In that case the launcher ends up having a dangling pinned shortcuts.
374                    // That's fine, when the launcher is restored, we'll fix it.
375                    continue;
376                }
377                si.addFlags(ShortcutInfo.FLAG_PINNED);
378            }
379        });
380
381        // Lastly, remove the ones that are no longer pinned nor dynamic.
382        removeOrphans();
383    }
384
385    /**
386     * Number of calls that the caller has made, since the last reset.
387     *
388     * <p>This takes care of the resetting the counter for foreground apps as well as after
389     * locale changes.
390     */
391    public int getApiCallCount() {
392        mShortcutUser.resetThrottlingIfNeeded();
393
394        final ShortcutService s = mShortcutUser.mService;
395
396        // Reset the counter if:
397        // - the package is in foreground now.
398        // - the package is *not* in foreground now, but was in foreground at some point
399        // since the previous time it had been.
400        if (s.isUidForegroundLocked(mPackageUid)
401                || mLastKnownForegroundElapsedTime
402                    < s.getUidLastForegroundElapsedTimeLocked(mPackageUid)) {
403            mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
404            resetRateLimiting();
405        }
406
407        // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
408        // but we just can't return 0 at this point, because we may have to update
409        // mLastResetTime.
410
411        final long last = s.getLastResetTimeLocked();
412
413        final long now = s.injectCurrentTimeMillis();
414        if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
415            Slog.w(TAG, "Clock rewound");
416            // Clock rewound.
417            mLastResetTime = now;
418            mApiCallCount = 0;
419            return mApiCallCount;
420        }
421
422        // If not reset yet, then reset.
423        if (mLastResetTime < last) {
424            if (ShortcutService.DEBUG) {
425                Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
426                        getPackageName(), mLastResetTime, now, last));
427            }
428            mApiCallCount = 0;
429            mLastResetTime = last;
430        }
431        return mApiCallCount;
432    }
433
434    /**
435     * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
436     * and return true.  Otherwise just return false.
437     *
438     * <p>This takes care of the resetting the counter for foreground apps as well as after
439     * locale changes, which is done internally by {@link #getApiCallCount}.
440     */
441    public boolean tryApiCall() {
442        final ShortcutService s = mShortcutUser.mService;
443
444        if (getApiCallCount() >= s.mMaxUpdatesPerInterval) {
445            return false;
446        }
447        mApiCallCount++;
448        s.scheduleSaveUser(getOwnerUserId());
449        return true;
450    }
451
452    public void resetRateLimiting() {
453        if (ShortcutService.DEBUG) {
454            Slog.d(TAG, "resetRateLimiting: " + getPackageName());
455        }
456        if (mApiCallCount > 0) {
457            mApiCallCount = 0;
458            mShortcutUser.mService.scheduleSaveUser(getOwnerUserId());
459        }
460    }
461
462    public void resetRateLimitingForCommandLineNoSaving() {
463        mApiCallCount = 0;
464        mLastResetTime = 0;
465    }
466
467    /**
468     * Find all shortcuts that match {@code query}.
469     */
470    public void findAll(@NonNull List<ShortcutInfo> result,
471            @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
472        findAll(result, query, cloneFlag, null, 0);
473    }
474
475    /**
476     * Find all shortcuts that match {@code query}.
477     *
478     * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
479     * by the calling launcher will not be included in the result, and also "isPinned" will be
480     * adjusted for the caller too.
481     */
482    public void findAll(@NonNull List<ShortcutInfo> result,
483            @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
484            @Nullable String callingLauncher, int launcherUserId) {
485        if (getPackageInfo().isShadow()) {
486            // Restored and the app not installed yet, so don't return any.
487            return;
488        }
489
490        final ShortcutService s = mShortcutUser.mService;
491
492        // Set of pinned shortcuts by the calling launcher.
493        final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
494                : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
495                    .getPinnedShortcutIds(getPackageName(), getPackageUserId());
496
497        for (int i = 0; i < mShortcuts.size(); i++) {
498            final ShortcutInfo si = mShortcuts.valueAt(i);
499
500            // Need to adjust PINNED flag depending on the caller.
501            // Basically if the caller is a launcher (callingLauncher != null) and the launcher
502            // isn't pinning it, then we need to clear PINNED for this caller.
503            final boolean isPinnedByCaller = (callingLauncher == null)
504                    || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
505
506            if (si.isFloating()) {
507                if (!isPinnedByCaller) {
508                    continue;
509                }
510            }
511            final ShortcutInfo clone = si.clone(cloneFlag);
512
513            // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
514            // since it may check isPinned.
515            if (!isPinnedByCaller) {
516                clone.clearFlags(ShortcutInfo.FLAG_PINNED);
517            }
518            if (query == null || query.test(clone)) {
519                result.add(clone);
520            }
521        }
522    }
523
524    public void resetThrottling() {
525        mApiCallCount = 0;
526    }
527
528    /**
529     * Return the filenames (excluding path names) of icon bitmap files from this package.
530     */
531    public ArraySet<String> getUsedBitmapFiles() {
532        final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size());
533
534        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
535            final ShortcutInfo si = mShortcuts.valueAt(i);
536            if (si.getBitmapPath() != null) {
537                usedFiles.add(getFileName(si.getBitmapPath()));
538            }
539        }
540        return usedFiles;
541    }
542
543    private static String getFileName(@NonNull String path) {
544        final int sep = path.lastIndexOf(File.separatorChar);
545        if (sep == -1) {
546            return path;
547        } else {
548            return path.substring(sep + 1);
549        }
550    }
551
552    /**
553     * Called when the package is updated or added.
554     *
555     * Add case:
556     * - Publish manifest shortcuts.
557     *
558     * Update case:
559     * - Re-publish manifest shortcuts.
560     * - If there are shortcuts with resources (icons or strings), update their timestamps.
561     *
562     * @return TRUE if any shortcuts have been changed.
563     */
564    public boolean handlePackageAddedOrUpdated(boolean isNewApp) {
565        final PackageInfo pi = mShortcutUser.mService.getPackageInfo(
566                getPackageName(), getPackageUserId());
567        if (pi == null) {
568            return false; // Shouldn't happen.
569        }
570
571        if (!isNewApp) {
572            // Make sure the version code or last update time has changed.
573            // Otherwise, nothing to do.
574            if (getPackageInfo().getVersionCode() >= pi.versionCode
575                    && getPackageInfo().getLastUpdateTime() >= pi.lastUpdateTime) {
576                return false;
577            }
578        }
579
580        // Now prepare to publish manifest shortcuts.
581        List<ShortcutInfo> newManifestShortcutList = null;
582        try {
583            newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
584                    getPackageName(), getPackageUserId());
585        } catch (IOException|XmlPullParserException e) {
586            Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
587        }
588        final int manifestShortcutSize = newManifestShortcutList == null ? 0
589                : newManifestShortcutList.size();
590        if (ShortcutService.DEBUG) {
591            Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)",
592                    getPackageName(), manifestShortcutSize));
593        }
594        if (isNewApp && (manifestShortcutSize == 0)) {
595            // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
596
597            // If it's an update, then it may already have manifest shortcuts, which need to be
598            // disabled.
599            return false;
600        }
601        if (ShortcutService.DEBUG) {
602            Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
603                    (isNewApp ? "added" : "updated"),
604                    getPackageInfo().getVersionCode(), pi.versionCode));
605        }
606
607        getPackageInfo().updateVersionInfo(pi);
608
609        final ShortcutService s = mShortcutUser.mService;
610
611        boolean changed = false;
612
613        // For existing shortcuts, update timestamps if they have any resources.
614        if (!isNewApp) {
615            for (int i = mShortcuts.size() - 1; i >= 0; i--) {
616                final ShortcutInfo si = mShortcuts.valueAt(i);
617
618                if (si.hasAnyResources()) {
619                    changed = true;
620                    si.setTimestamp(s.injectCurrentTimeMillis());
621                }
622            }
623        }
624
625        // (Re-)publish manifest shortcut.
626        changed |= publishManifestShortcuts(newManifestShortcutList);
627
628        if (changed) {
629            // This will send a notification to the launcher, and also save .
630            s.packageShortcutsChanged(getPackageName(), getPackageUserId());
631        } else {
632            // Still save the version code.
633            s.scheduleSaveUser(getPackageUserId());
634        }
635        return changed;
636    }
637
638    private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) {
639        if (ShortcutService.DEBUG) {
640            Slog.d(TAG, String.format(
641                    "Package %s: publishing manifest shortcuts", getPackageName()));
642        }
643        boolean changed = false;
644
645        // TODO: Check dynamic count
646
647        // TODO: Kick out dynamic if too many
648
649        // Keep the previous IDs.
650        ArraySet<String> toDisableList = null;
651        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
652            final ShortcutInfo si = mShortcuts.valueAt(i);
653
654            if (si.isManifestShortcut()) {
655                if (toDisableList == null) {
656                    toDisableList = new ArraySet<>();
657                }
658                toDisableList.add(si.getId());
659            }
660        }
661
662        // Publish new ones.
663        if (newManifestShortcutList != null) {
664            final int newListSize = newManifestShortcutList.size();
665
666            for (int i = 0; i < newListSize; i++) {
667                changed = true;
668
669                final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
670                final boolean newDisabled = !newShortcut.isEnabled();
671
672                final String id =  newShortcut.getId();
673                final ShortcutInfo oldShortcut = mShortcuts.get(id);
674
675                boolean wasPinned = false;
676
677                if (oldShortcut != null) {
678                    if (!oldShortcut.isOriginallyFromManifest()) {
679                        Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId()
680                                + " exists but is not from AndroidManifest.xml, not updating.");
681                        continue;
682                    }
683                    // Take over the pinned flag.
684                    if (oldShortcut.isPinned()) {
685                        wasPinned = true;
686                        newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
687                    }
688                }
689                if (newDisabled && !wasPinned) {
690                    // If the shortcut is disabled, and it was *not* pinned, then this
691                    // just doesn't have to be published.
692                    // Just keep it in toDisableList, so the previous one would be removed.
693                    continue;
694                }
695                // TODO: Check dynamic count
696
697                // Note even if enabled=false, we still need to update all fields, so do it
698                // regardless.
699                addShortcutInner(newShortcut); // This will clean up the old one too.
700
701                if (!newDisabled && toDisableList != null) {
702                    // Still alive, don't remove.
703                    toDisableList.remove(id);
704                }
705            }
706        }
707
708        // Disable the previous manifest shortcuts that are no longer in the manifest.
709        if (toDisableList != null) {
710            if (ShortcutService.DEBUG) {
711                Slog.d(TAG, String.format(
712                        "Package %s: disabling %d stale shortcuts", getPackageName(),
713                        toDisableList.size()));
714            }
715            for (int i = toDisableList.size() - 1; i >= 0; i--) {
716                changed = true;
717
718                final String id = toDisableList.valueAt(i);
719
720                disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0,
721                        /* overrideImmutable=*/ true);
722            }
723            removeOrphans();
724        }
725        return changed;
726    }
727
728    public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
729        pw.println();
730
731        pw.print(prefix);
732        pw.print("Package: ");
733        pw.print(getPackageName());
734        pw.print("  UID: ");
735        pw.print(mPackageUid);
736        pw.println();
737
738        pw.print(prefix);
739        pw.print("  ");
740        pw.print("Calls: ");
741        pw.print(getApiCallCount());
742        pw.println();
743
744        // getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
745        pw.print(prefix);
746        pw.print("  ");
747        pw.print("Last known FG: ");
748        pw.print(mLastKnownForegroundElapsedTime);
749        pw.println();
750
751        // This should be after getApiCallCount(), which may update it.
752        pw.print(prefix);
753        pw.print("  ");
754        pw.print("Last reset: [");
755        pw.print(mLastResetTime);
756        pw.print("] ");
757        pw.print(ShortcutService.formatTime(mLastResetTime));
758        pw.println();
759
760        getPackageInfo().dump(pw, prefix + "  ");
761        pw.println();
762
763        pw.print(prefix);
764        pw.println("  Shortcuts:");
765        long totalBitmapSize = 0;
766        final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
767        final int size = shortcuts.size();
768        for (int i = 0; i < size; i++) {
769            final ShortcutInfo si = shortcuts.valueAt(i);
770            pw.print(prefix);
771            pw.print("    ");
772            pw.println(si.toInsecureString());
773            if (si.getBitmapPath() != null) {
774                final long len = new File(si.getBitmapPath()).length();
775                pw.print(prefix);
776                pw.print("      ");
777                pw.print("bitmap size=");
778                pw.println(len);
779
780                totalBitmapSize += len;
781            }
782        }
783        pw.print(prefix);
784        pw.print("  ");
785        pw.print("Total bitmap size: ");
786        pw.print(totalBitmapSize);
787        pw.print(" (");
788        pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize));
789        pw.println(")");
790    }
791
792    @Override
793    public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
794            throws IOException, XmlPullParserException {
795        final int size = mShortcuts.size();
796
797        if (size == 0 && mApiCallCount == 0) {
798            return; // nothing to write.
799        }
800
801        out.startTag(null, TAG_ROOT);
802
803        ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
804        ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
805        ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
806        getPackageInfo().saveToXml(out);
807
808        for (int j = 0; j < size; j++) {
809            saveShortcut(out, mShortcuts.valueAt(j), forBackup);
810        }
811
812        out.endTag(null, TAG_ROOT);
813    }
814
815    private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
816            throws IOException, XmlPullParserException {
817        if (forBackup) {
818            if (!si.isPinned()) {
819                return; // Backup only pinned icons.
820            }
821        }
822        out.startTag(null, TAG_SHORTCUT);
823        ShortcutService.writeAttr(out, ATTR_ID, si.getId());
824        // writeAttr(out, "package", si.getPackageName()); // not needed
825        ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity());
826        // writeAttr(out, "icon", si.getIcon());  // We don't save it.
827        ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
828        ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
829        ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
830        ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
831        ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
832        ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
833                si.getDisabledMessageResourceId());
834        ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
835        ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
836        ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
837                si.getLastChangedTimestamp());
838        if (forBackup) {
839            // Don't write icon information.  Also drop the dynamic flag.
840            ShortcutService.writeAttr(out, ATTR_FLAGS,
841                    si.getFlags() &
842                            ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
843                            | ShortcutInfo.FLAG_DYNAMIC));
844        } else {
845            ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
846            ShortcutService.writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
847            ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
848        }
849
850        {
851            final Set<String> cat = si.getCategories();
852            if (cat != null && cat.size() > 0) {
853                out.startTag(null, TAG_CATEGORIES);
854                XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
855                        NAME_CATEGORIES, out);
856                out.endTag(null, TAG_CATEGORIES);
857            }
858        }
859
860        ShortcutService.writeTagExtra(out, TAG_INTENT_EXTRAS,
861                si.getIntentPersistableExtras());
862        ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
863
864        out.endTag(null, TAG_SHORTCUT);
865    }
866
867    public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
868            XmlPullParser parser, boolean fromBackup)
869            throws IOException, XmlPullParserException {
870
871        final String packageName = ShortcutService.parseStringAttribute(parser,
872                ATTR_NAME);
873
874        final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
875                shortcutUser.getUserId(), packageName);
876
877        ret.mApiCallCount =
878                ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
879        ret.mLastResetTime =
880                ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
881
882        final int outerDepth = parser.getDepth();
883        int type;
884        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
885                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
886            if (type != XmlPullParser.START_TAG) {
887                continue;
888            }
889            final int depth = parser.getDepth();
890            final String tag = parser.getName();
891            if (depth == outerDepth + 1) {
892                switch (tag) {
893                    case ShortcutPackageInfo.TAG_ROOT:
894                        ret.getPackageInfo().loadFromXml(parser, fromBackup);
895                        continue;
896                    case TAG_SHORTCUT:
897                        final ShortcutInfo si = parseShortcut(parser, packageName,
898                                shortcutUser.getUserId());
899
900                        // Don't use addShortcut(), we don't need to save the icon.
901                        ret.mShortcuts.put(si.getId(), si);
902                        continue;
903                }
904            }
905            ShortcutService.warnForInvalidTag(depth, tag);
906        }
907        return ret;
908    }
909
910    private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
911            @UserIdInt int userId) throws IOException, XmlPullParserException {
912        String id;
913        ComponentName activityComponent;
914        // Icon icon;
915        String title;
916        int titleResId;
917        String text;
918        int textResId;
919        String disabledMessage;
920        int disabledMessageResId;
921        Intent intent;
922        PersistableBundle intentPersistableExtras = null;
923        int rank;
924        PersistableBundle extras = null;
925        long lastChangedTimestamp;
926        int flags;
927        int iconRes;
928        String bitmapPath;
929        ArraySet<String> categories = null;
930
931        id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
932        activityComponent = ShortcutService.parseComponentNameAttribute(parser,
933                ATTR_ACTIVITY);
934        title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
935        titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
936        text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
937        textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
938        disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
939        disabledMessageResId = ShortcutService.parseIntAttribute(parser,
940                ATTR_DISABLED_MESSAGE_RES_ID);
941        intent = ShortcutService.parseIntentAttribute(parser, ATTR_INTENT);
942        rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
943        lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
944        flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
945        iconRes = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES);
946        bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
947
948        final int outerDepth = parser.getDepth();
949        int type;
950        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
951                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
952            if (type != XmlPullParser.START_TAG) {
953                continue;
954            }
955            final int depth = parser.getDepth();
956            final String tag = parser.getName();
957            if (ShortcutService.DEBUG_LOAD) {
958                Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
959                        depth, type, tag));
960            }
961            switch (tag) {
962                case TAG_INTENT_EXTRAS:
963                    intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
964                    continue;
965                case TAG_EXTRAS:
966                    extras = PersistableBundle.restoreFromXml(parser);
967                    continue;
968                case TAG_CATEGORIES:
969                    // This just contains string-array.
970                    continue;
971                case TAG_STRING_ARRAY_XMLUTILS:
972                    if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
973                            ATTR_NAME_XMLUTILS))) {
974                        final String[] ar = XmlUtils.readThisStringArrayXml(
975                                parser, TAG_STRING_ARRAY_XMLUTILS, null);
976                        categories = new ArraySet<>(ar.length);
977                        for (int i = 0; i < ar.length; i++) {
978                            categories.add(ar[i]);
979                        }
980                    }
981                    continue;
982            }
983            throw ShortcutService.throwForInvalidTag(depth, tag);
984        }
985
986        return new ShortcutInfo(
987                userId, id, packageName, activityComponent, /* icon =*/ null,
988                title, titleResId, text, textResId, disabledMessage, disabledMessageResId,
989                categories, intent,
990                intentPersistableExtras, rank, extras, lastChangedTimestamp, flags,
991                iconRes, bitmapPath);
992    }
993
994    @VisibleForTesting
995    List<ShortcutInfo> getAllShortcutsForTest() {
996        return new ArrayList<>(mShortcuts.values());
997    }
998}
999