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