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  */
16 package com.android.server.pm;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.annotation.UserIdInt;
21 import android.content.ComponentName;
22 import android.content.Intent;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.ShortcutInfo;
25 import android.content.res.Resources;
26 import android.os.PersistableBundle;
27 import android.text.format.Formatter;
28 import android.util.ArrayMap;
29 import android.util.ArraySet;
30 import android.util.Log;
31 import android.util.Slog;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.internal.util.Preconditions;
35 import com.android.internal.util.XmlUtils;
36 import com.android.server.pm.ShortcutService.DumpFilter;
37 import com.android.server.pm.ShortcutService.ShortcutOperation;
38 import com.android.server.pm.ShortcutService.Stats;
39 
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 import org.xmlpull.v1.XmlSerializer;
45 
46 import java.io.File;
47 import java.io.IOException;
48 import java.io.PrintWriter;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.Comparator;
52 import java.util.List;
53 import java.util.Set;
54 import java.util.function.Predicate;
55 
56 /**
57  * Package information used by {@link ShortcutService}.
58  * User information used by {@link ShortcutService}.
59  *
60  * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
61  */
62 class ShortcutPackage extends ShortcutPackageItem {
63     private static final String TAG = ShortcutService.TAG;
64     private static final String TAG_VERIFY = ShortcutService.TAG + ".verify";
65 
66     static final String TAG_ROOT = "package";
67     private static final String TAG_INTENT_EXTRAS_LEGACY = "intent-extras";
68     private static final String TAG_INTENT = "intent";
69     private static final String TAG_EXTRAS = "extras";
70     private static final String TAG_SHORTCUT = "shortcut";
71     private static final String TAG_CATEGORIES = "categories";
72 
73     private static final String ATTR_NAME = "name";
74     private static final String ATTR_CALL_COUNT = "call-count";
75     private static final String ATTR_LAST_RESET = "last-reset";
76     private static final String ATTR_ID = "id";
77     private static final String ATTR_ACTIVITY = "activity";
78     private static final String ATTR_TITLE = "title";
79     private static final String ATTR_TITLE_RES_ID = "titleid";
80     private static final String ATTR_TITLE_RES_NAME = "titlename";
81     private static final String ATTR_TEXT = "text";
82     private static final String ATTR_TEXT_RES_ID = "textid";
83     private static final String ATTR_TEXT_RES_NAME = "textname";
84     private static final String ATTR_DISABLED_MESSAGE = "dmessage";
85     private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
86     private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
87     private static final String ATTR_DISABLED_REASON = "disabled-reason";
88     private static final String ATTR_INTENT_LEGACY = "intent";
89     private static final String ATTR_INTENT_NO_EXTRA = "intent-base";
90     private static final String ATTR_RANK = "rank";
91     private static final String ATTR_TIMESTAMP = "timestamp";
92     private static final String ATTR_FLAGS = "flags";
93     private static final String ATTR_ICON_RES_ID = "icon-res";
94     private static final String ATTR_ICON_RES_NAME = "icon-resname";
95     private static final String ATTR_BITMAP_PATH = "bitmap-path";
96 
97     private static final String NAME_CATEGORIES = "categories";
98 
99     private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
100     private static final String ATTR_NAME_XMLUTILS = "name";
101 
102     private static final String KEY_DYNAMIC = "dynamic";
103     private static final String KEY_MANIFEST = "manifest";
104     private static final String KEY_PINNED = "pinned";
105     private static final String KEY_BITMAPS = "bitmaps";
106     private static final String KEY_BITMAP_BYTES = "bitmapBytes";
107 
108     /**
109      * All the shortcuts from the package, keyed on IDs.
110      */
111     final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
112 
113     /**
114      * # of times the package has called rate-limited APIs.
115      */
116     private int mApiCallCount;
117 
118     /**
119      * When {@link #mApiCallCount} was reset last time.
120      */
121     private long mLastResetTime;
122 
123     private final int mPackageUid;
124 
125     private long mLastKnownForegroundElapsedTime;
126 
ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi)127     private ShortcutPackage(ShortcutUser shortcutUser,
128             int packageUserId, String packageName, ShortcutPackageInfo spi) {
129         super(shortcutUser, packageUserId, packageName,
130                 spi != null ? spi : ShortcutPackageInfo.newEmpty());
131 
132         mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
133     }
134 
ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName)135     public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
136         this(shortcutUser, packageUserId, packageName, null);
137     }
138 
139     @Override
getOwnerUserId()140     public int getOwnerUserId() {
141         // For packages, always owner user == package user.
142         return getPackageUserId();
143     }
144 
getPackageUid()145     public int getPackageUid() {
146         return mPackageUid;
147     }
148 
149     @Nullable
getPackageResources()150     public Resources getPackageResources() {
151         return mShortcutUser.mService.injectGetResourcesForApplicationAsUser(
152                 getPackageName(), getPackageUserId());
153     }
154 
getShortcutCount()155     public int getShortcutCount() {
156         return mShortcuts.size();
157     }
158 
159     @Override
canRestoreAnyVersion()160     protected boolean canRestoreAnyVersion() {
161         return false;
162     }
163 
164     @Override
onRestored(int restoreBlockReason)165     protected void onRestored(int restoreBlockReason) {
166         // Shortcuts have been restored.
167         // - Unshadow all shortcuts.
168         // - Set disabled reason.
169         // - Disable if needed.
170         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
171             ShortcutInfo si = mShortcuts.valueAt(i);
172             si.clearFlags(ShortcutInfo.FLAG_SHADOW);
173 
174             si.setDisabledReason(restoreBlockReason);
175             if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
176                 si.addFlags(ShortcutInfo.FLAG_DISABLED);
177             }
178         }
179         // Because some launchers may not have been restored (e.g. allowBackup=false),
180         // we need to re-calculate the pinned shortcuts.
181         refreshPinnedFlags();
182     }
183 
184     /**
185      * Note this does *not* provide a correct view to the calling launcher.
186      */
187     @Nullable
findShortcutById(String id)188     public ShortcutInfo findShortcutById(String id) {
189         return mShortcuts.get(id);
190     }
191 
isShortcutExistsAndInvisibleToPublisher(String id)192     public boolean isShortcutExistsAndInvisibleToPublisher(String id) {
193         ShortcutInfo si = findShortcutById(id);
194         return si != null && !si.isVisibleToPublisher();
195     }
196 
isShortcutExistsAndVisibleToPublisher(String id)197     public boolean isShortcutExistsAndVisibleToPublisher(String id) {
198         ShortcutInfo si = findShortcutById(id);
199         return si != null && si.isVisibleToPublisher();
200     }
201 
ensureNotImmutable(@ullable ShortcutInfo shortcut, boolean ignoreInvisible)202     private void ensureNotImmutable(@Nullable ShortcutInfo shortcut, boolean ignoreInvisible) {
203         if (shortcut != null && shortcut.isImmutable()
204                 && (!ignoreInvisible || shortcut.isVisibleToPublisher())) {
205             throw new IllegalArgumentException(
206                     "Manifest shortcut ID=" + shortcut.getId()
207                             + " may not be manipulated via APIs");
208         }
209     }
210 
ensureNotImmutable(@onNull String id, boolean ignoreInvisible)211     public void ensureNotImmutable(@NonNull String id, boolean ignoreInvisible) {
212         ensureNotImmutable(mShortcuts.get(id), ignoreInvisible);
213     }
214 
ensureImmutableShortcutsNotIncludedWithIds(@onNull List<String> shortcutIds, boolean ignoreInvisible)215     public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds,
216             boolean ignoreInvisible) {
217         for (int i = shortcutIds.size() - 1; i >= 0; i--) {
218             ensureNotImmutable(shortcutIds.get(i), ignoreInvisible);
219         }
220     }
221 
ensureImmutableShortcutsNotIncluded(@onNull List<ShortcutInfo> shortcuts, boolean ignoreInvisible)222     public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts,
223             boolean ignoreInvisible) {
224         for (int i = shortcuts.size() - 1; i >= 0; i--) {
225             ensureNotImmutable(shortcuts.get(i).getId(), ignoreInvisible);
226         }
227     }
228 
229     /**
230      * Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible.
231      */
forceDeleteShortcutInner(@onNull String id)232     private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) {
233         final ShortcutInfo shortcut = mShortcuts.remove(id);
234         if (shortcut != null) {
235             mShortcutUser.mService.removeIconLocked(shortcut);
236             shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
237                     | ShortcutInfo.FLAG_MANIFEST);
238         }
239         return shortcut;
240     }
241 
242     /**
243      * Force replace a shortcut. If there's already a shortcut with the same ID, it'll be removed,
244      * even if it's invisible.
245      */
forceReplaceShortcutInner(@onNull ShortcutInfo newShortcut)246     private void forceReplaceShortcutInner(@NonNull ShortcutInfo newShortcut) {
247         final ShortcutService s = mShortcutUser.mService;
248 
249         forceDeleteShortcutInner(newShortcut.getId());
250 
251         // Extract Icon and update the icon res ID and the bitmap path.
252         s.saveIconAndFixUpShortcutLocked(newShortcut);
253         s.fixUpShortcutResourceNamesAndValues(newShortcut);
254         mShortcuts.put(newShortcut.getId(), newShortcut);
255     }
256 
257     /**
258      * Add a shortcut. If there's already a one with the same ID, it'll be removed, even if it's
259      * invisible.
260      *
261      * It checks the max number of dynamic shortcuts.
262      */
addOrReplaceDynamicShortcut(@onNull ShortcutInfo newShortcut)263     public void addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
264 
265         Preconditions.checkArgument(newShortcut.isEnabled(),
266                 "add/setDynamicShortcuts() cannot publish disabled shortcuts");
267 
268         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
269 
270         final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
271 
272         final boolean wasPinned;
273 
274         if (oldShortcut == null) {
275             wasPinned = false;
276         } else {
277             // It's an update case.
278             // Make sure the target is updatable. (i.e. should be mutable.)
279             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
280 
281             wasPinned = oldShortcut.isPinned();
282         }
283 
284         // If it was originally pinned, the new one should be pinned too.
285         if (wasPinned) {
286             newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
287         }
288 
289         forceReplaceShortcutInner(newShortcut);
290     }
291 
292     /**
293      * Remove all shortcuts that aren't pinned nor dynamic.
294      */
removeOrphans()295     private void removeOrphans() {
296         ArrayList<String> removeList = null; // Lazily initialize.
297 
298         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
299             final ShortcutInfo si = mShortcuts.valueAt(i);
300 
301             if (si.isAlive()) continue;
302 
303             if (removeList == null) {
304                 removeList = new ArrayList<>();
305             }
306             removeList.add(si.getId());
307         }
308         if (removeList != null) {
309             for (int i = removeList.size() - 1; i >= 0; i--) {
310                 forceDeleteShortcutInner(removeList.get(i));
311             }
312         }
313     }
314 
315     /**
316      * Remove all dynamic shortcuts.
317      */
deleteAllDynamicShortcuts(boolean ignoreInvisible)318     public void deleteAllDynamicShortcuts(boolean ignoreInvisible) {
319         final long now = mShortcutUser.mService.injectCurrentTimeMillis();
320 
321         boolean changed = false;
322         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
323             final ShortcutInfo si = mShortcuts.valueAt(i);
324             if (si.isDynamic() && (!ignoreInvisible || si.isVisibleToPublisher())) {
325                 changed = true;
326 
327                 si.setTimestamp(now);
328                 si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
329                 si.setRank(0); // It may still be pinned, so clear the rank.
330             }
331         }
332         if (changed) {
333             removeOrphans();
334         }
335     }
336 
337     /**
338      * Remove a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
339      * is pinned, it'll remain as a pinned shortcut, and is still enabled.
340      *
341      * @return true if it's actually removed because it wasn't pinned, or false if it's still
342      * pinned.
343      */
deleteDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible)344     public boolean deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) {
345         final ShortcutInfo removed = deleteOrDisableWithId(
346                 shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible,
347                 ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
348         return removed == null;
349     }
350 
351     /**
352      * Disable a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
353      * is pinned, it'll remain as a pinned shortcut, but will be disabled.
354      *
355      * @return true if it's actually removed because it wasn't pinned, or false if it's still
356      * pinned.
357      */
disableDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible, int disabledReason)358     private boolean disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible,
359             int disabledReason) {
360         final ShortcutInfo disabled = deleteOrDisableWithId(
361                 shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, ignoreInvisible,
362                 disabledReason);
363         return disabled == null;
364     }
365 
366     /**
367      * Disable a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
368      * is pinned, it'll remain as a pinned shortcut but will be disabled.
369      */
disableWithId(@onNull String shortcutId, String disabledMessage, int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)370     public void disableWithId(@NonNull String shortcutId, String disabledMessage,
371             int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible,
372             int disabledReason) {
373         final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
374                 overrideImmutable, ignoreInvisible, disabledReason);
375 
376         if (disabled != null) {
377             if (disabledMessage != null) {
378                 disabled.setDisabledMessage(disabledMessage);
379             } else if (disabledMessageResId != 0) {
380                 disabled.setDisabledMessageResId(disabledMessageResId);
381 
382                 mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled);
383             }
384         }
385     }
386 
387     @Nullable
deleteOrDisableWithId(@onNull String shortcutId, boolean disable, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)388     private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable,
389             boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) {
390         Preconditions.checkState(
391                 (disable == (disabledReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED)),
392                 "disable and disabledReason disagree: " + disable + " vs " + disabledReason);
393         final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
394 
395         if (oldShortcut == null || !oldShortcut.isEnabled()
396                 && (ignoreInvisible && !oldShortcut.isVisibleToPublisher())) {
397             return null; // Doesn't exist or already disabled.
398         }
399         if (!overrideImmutable) {
400             ensureNotImmutable(oldShortcut, /*ignoreInvisible=*/ true);
401         }
402         if (oldShortcut.isPinned()) {
403 
404             oldShortcut.setRank(0);
405             oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST);
406             if (disable) {
407                 oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED);
408                 // Do not overwrite the disabled reason if one is alreay set.
409                 if (oldShortcut.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
410                     oldShortcut.setDisabledReason(disabledReason);
411                 }
412             }
413             oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis());
414 
415             // See ShortcutRequestPinProcessor.directPinShortcut().
416             if (mShortcutUser.mService.isDummyMainActivity(oldShortcut.getActivity())) {
417                 oldShortcut.setActivity(null);
418             }
419 
420             return oldShortcut;
421         } else {
422             forceDeleteShortcutInner(shortcutId);
423             return null;
424         }
425     }
426 
enableWithId(@onNull String shortcutId)427     public void enableWithId(@NonNull String shortcutId) {
428         final ShortcutInfo shortcut = mShortcuts.get(shortcutId);
429         if (shortcut != null) {
430             ensureNotImmutable(shortcut, /*ignoreInvisible=*/ true);
431             shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED);
432             shortcut.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
433         }
434     }
435 
updateInvisibleShortcutForPinRequestWith(@onNull ShortcutInfo shortcut)436     public void updateInvisibleShortcutForPinRequestWith(@NonNull ShortcutInfo shortcut) {
437         final ShortcutInfo source = mShortcuts.get(shortcut.getId());
438         Preconditions.checkNotNull(source);
439 
440         mShortcutUser.mService.validateShortcutForPinRequest(shortcut);
441 
442         shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
443 
444         forceReplaceShortcutInner(shortcut);
445 
446         adjustRanks();
447     }
448 
449     /**
450      * Called after a launcher updates the pinned set.  For each shortcut in this package,
451      * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
452      *
453      * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
454      */
refreshPinnedFlags()455     public void refreshPinnedFlags() {
456         // First, un-pin all shortcuts
457         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
458             mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
459         }
460 
461         // Then, for the pinned set for each launcher, set the pin flag one by one.
462         mShortcutUser.forAllLaunchers(launcherShortcuts -> {
463             final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
464                     getPackageName(), getPackageUserId());
465 
466             if (pinned == null || pinned.size() == 0) {
467                 return;
468             }
469             for (int i = pinned.size() - 1; i >= 0; i--) {
470                 final String id = pinned.valueAt(i);
471                 final ShortcutInfo si = mShortcuts.get(id);
472                 if (si == null) {
473                     // This happens if a launcher pinned shortcuts from this package, then backup&
474                     // restored, but this package doesn't allow backing up.
475                     // In that case the launcher ends up having a dangling pinned shortcuts.
476                     // That's fine, when the launcher is restored, we'll fix it.
477                     continue;
478                 }
479                 si.addFlags(ShortcutInfo.FLAG_PINNED);
480             }
481         });
482 
483         // Lastly, remove the ones that are no longer pinned nor dynamic.
484         removeOrphans();
485     }
486 
487     /**
488      * Number of calls that the caller has made, since the last reset.
489      *
490      * <p>This takes care of the resetting the counter for foreground apps as well as after
491      * locale changes.
492      */
getApiCallCount(boolean unlimited)493     public int getApiCallCount(boolean unlimited) {
494         final ShortcutService s = mShortcutUser.mService;
495 
496         // Reset the counter if:
497         // - the package is in foreground now.
498         // - the package is *not* in foreground now, but was in foreground at some point
499         // since the previous time it had been.
500         if (s.isUidForegroundLocked(mPackageUid)
501                 || (mLastKnownForegroundElapsedTime
502                     < s.getUidLastForegroundElapsedTimeLocked(mPackageUid))
503                 || unlimited) {
504             mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
505             resetRateLimiting();
506         }
507 
508         // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
509         // but we just can't return 0 at this point, because we may have to update
510         // mLastResetTime.
511 
512         final long last = s.getLastResetTimeLocked();
513 
514         final long now = s.injectCurrentTimeMillis();
515         if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
516             Slog.w(TAG, "Clock rewound");
517             // Clock rewound.
518             mLastResetTime = now;
519             mApiCallCount = 0;
520             return mApiCallCount;
521         }
522 
523         // If not reset yet, then reset.
524         if (mLastResetTime < last) {
525             if (ShortcutService.DEBUG) {
526                 Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
527                         getPackageName(), mLastResetTime, now, last));
528             }
529             mApiCallCount = 0;
530             mLastResetTime = last;
531         }
532         return mApiCallCount;
533     }
534 
535     /**
536      * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
537      * and return true.  Otherwise just return false.
538      *
539      * <p>This takes care of the resetting the counter for foreground apps as well as after
540      * locale changes, which is done internally by {@link #getApiCallCount}.
541      */
tryApiCall(boolean unlimited)542     public boolean tryApiCall(boolean unlimited) {
543         final ShortcutService s = mShortcutUser.mService;
544 
545         if (getApiCallCount(unlimited) >= s.mMaxUpdatesPerInterval) {
546             return false;
547         }
548         mApiCallCount++;
549         s.scheduleSaveUser(getOwnerUserId());
550         return true;
551     }
552 
resetRateLimiting()553     public void resetRateLimiting() {
554         if (ShortcutService.DEBUG) {
555             Slog.d(TAG, "resetRateLimiting: " + getPackageName());
556         }
557         if (mApiCallCount > 0) {
558             mApiCallCount = 0;
559             mShortcutUser.mService.scheduleSaveUser(getOwnerUserId());
560         }
561     }
562 
resetRateLimitingForCommandLineNoSaving()563     public void resetRateLimitingForCommandLineNoSaving() {
564         mApiCallCount = 0;
565         mLastResetTime = 0;
566     }
567 
568     /**
569      * Find all shortcuts that match {@code query}.
570      */
findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> query, int cloneFlag)571     public void findAll(@NonNull List<ShortcutInfo> result,
572             @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
573         findAll(result, query, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false);
574     }
575 
576     /**
577      * Find all shortcuts that match {@code query}.
578      *
579      * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
580      * by the calling launcher will not be included in the result, and also "isPinned" will be
581      * adjusted for the caller too.
582      */
findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> query, int cloneFlag, @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher)583     public void findAll(@NonNull List<ShortcutInfo> result,
584             @Nullable Predicate<ShortcutInfo> query, int cloneFlag,
585             @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) {
586         if (getPackageInfo().isShadow()) {
587             // Restored and the app not installed yet, so don't return any.
588             return;
589         }
590 
591         final ShortcutService s = mShortcutUser.mService;
592 
593         // Set of pinned shortcuts by the calling launcher.
594         final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
595                 : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
596                     .getPinnedShortcutIds(getPackageName(), getPackageUserId());
597 
598         for (int i = 0; i < mShortcuts.size(); i++) {
599             final ShortcutInfo si = mShortcuts.valueAt(i);
600 
601             // Need to adjust PINNED flag depending on the caller.
602             // Basically if the caller is a launcher (callingLauncher != null) and the launcher
603             // isn't pinning it, then we need to clear PINNED for this caller.
604             final boolean isPinnedByCaller = (callingLauncher == null)
605                     || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
606 
607             if (!getPinnedByAnyLauncher) {
608                 if (si.isFloating()) {
609                     if (!isPinnedByCaller) {
610                         continue;
611                     }
612                 }
613             }
614             final ShortcutInfo clone = si.clone(cloneFlag);
615 
616             // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
617             // since it may check isPinned.
618             // However, if getPinnedByAnyLauncher is set, we do it after the test.
619             if (!getPinnedByAnyLauncher) {
620                 if (!isPinnedByCaller) {
621                     clone.clearFlags(ShortcutInfo.FLAG_PINNED);
622                 }
623             }
624             if (query == null || query.test(clone)) {
625                 if (!isPinnedByCaller) {
626                     clone.clearFlags(ShortcutInfo.FLAG_PINNED);
627                 }
628                 result.add(clone);
629             }
630         }
631     }
632 
resetThrottling()633     public void resetThrottling() {
634         mApiCallCount = 0;
635     }
636 
637     /**
638      * Return the filenames (excluding path names) of icon bitmap files from this package.
639      */
getUsedBitmapFiles()640     public ArraySet<String> getUsedBitmapFiles() {
641         final ArraySet<String> usedFiles = new ArraySet<>(mShortcuts.size());
642 
643         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
644             final ShortcutInfo si = mShortcuts.valueAt(i);
645             if (si.getBitmapPath() != null) {
646                 usedFiles.add(getFileName(si.getBitmapPath()));
647             }
648         }
649         return usedFiles;
650     }
651 
getFileName(@onNull String path)652     private static String getFileName(@NonNull String path) {
653         final int sep = path.lastIndexOf(File.separatorChar);
654         if (sep == -1) {
655             return path;
656         } else {
657             return path.substring(sep + 1);
658         }
659     }
660 
661     /**
662      * @return false if any of the target activities are no longer enabled.
663      */
areAllActivitiesStillEnabled()664     private boolean areAllActivitiesStillEnabled() {
665         if (mShortcuts.size() == 0) {
666             return true;
667         }
668         final ShortcutService s = mShortcutUser.mService;
669 
670         // Normally the number of target activities is 1 or so, so no need to use a complex
671         // structure like a set.
672         final ArrayList<ComponentName> checked = new ArrayList<>(4);
673 
674         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
675             final ShortcutInfo si = mShortcuts.valueAt(i);
676             final ComponentName activity = si.getActivity();
677 
678             if (checked.contains(activity)) {
679                 continue; // Already checked.
680             }
681             checked.add(activity);
682 
683             if ((activity != null)
684                     && !s.injectIsActivityEnabledAndExported(activity, getOwnerUserId())) {
685                 return false;
686             }
687         }
688         return true;
689     }
690 
691     /**
692      * Called when the package may be added or updated, or its activities may be disabled, and
693      * if so, rescan the package and do the necessary stuff.
694      *
695      * Add case:
696      * - Publish manifest shortcuts.
697      *
698      * Update case:
699      * - Re-publish manifest shortcuts.
700      * - If there are shortcuts with resources (icons or strings), update their timestamps.
701      * - Disable shortcuts whose target activities are disabled.
702      *
703      * @return TRUE if any shortcuts have been changed.
704      */
rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan)705     public boolean rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan) {
706         final ShortcutService s = mShortcutUser.mService;
707         final long start = s.getStatStartTime();
708 
709         final PackageInfo pi;
710         try {
711             pi = mShortcutUser.mService.getPackageInfo(
712                     getPackageName(), getPackageUserId());
713             if (pi == null) {
714                 return false; // Shouldn't happen.
715             }
716 
717             if (!isNewApp && !forceRescan) {
718                 // Return if the package hasn't changed, ie:
719                 // - version code hasn't change
720                 // - lastUpdateTime hasn't change
721                 // - all target activities are still enabled.
722 
723                 // Note, system apps timestamps do *not* change after OTAs.  (But they do
724                 // after an adb sync or a local flash.)
725                 // This means if a system app's version code doesn't change on an OTA,
726                 // we don't notice it's updated.  But that's fine since their version code *should*
727                 // really change on OTAs.
728                 if ((getPackageInfo().getVersionCode() == pi.getLongVersionCode())
729                         && (getPackageInfo().getLastUpdateTime() == pi.lastUpdateTime)
730                         && areAllActivitiesStillEnabled()) {
731                     return false;
732                 }
733             }
734         } finally {
735             s.logDurationStat(Stats.PACKAGE_UPDATE_CHECK, start);
736         }
737 
738         // Now prepare to publish manifest shortcuts.
739         List<ShortcutInfo> newManifestShortcutList = null;
740         try {
741             newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
742                     getPackageName(), getPackageUserId());
743         } catch (IOException|XmlPullParserException e) {
744             Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
745         }
746         final int manifestShortcutSize = newManifestShortcutList == null ? 0
747                 : newManifestShortcutList.size();
748         if (ShortcutService.DEBUG) {
749             Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)",
750                     getPackageName(), manifestShortcutSize));
751         }
752         if (isNewApp && (manifestShortcutSize == 0)) {
753             // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
754 
755             // If it's an update, then it may already have manifest shortcuts, which need to be
756             // disabled.
757             return false;
758         }
759         if (ShortcutService.DEBUG) {
760             Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
761                     (isNewApp ? "added" : "updated"),
762                     getPackageInfo().getVersionCode(), pi.getLongVersionCode()));
763         }
764 
765         getPackageInfo().updateFromPackageInfo(pi);
766         final long newVersionCode = getPackageInfo().getVersionCode();
767 
768         // See if there are any shortcuts that were prevented restoring because the app was of a
769         // lower version, and re-enable them.
770         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
771             final ShortcutInfo si = mShortcuts.valueAt(i);
772             if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) {
773                 continue;
774             }
775             if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) {
776                 if (ShortcutService.DEBUG) {
777                     Slog.d(TAG, String.format("Shortcut %s require version %s, still not restored.",
778                             si.getId(), getPackageInfo().getBackupSourceVersionCode()));
779                 }
780                 continue;
781             }
782             Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId()));
783             si.clearFlags(ShortcutInfo.FLAG_DISABLED);
784             si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
785         }
786 
787         // For existing shortcuts, update timestamps if they have any resources.
788         // Also check if shortcuts' activities are still main activities.  Otherwise, disable them.
789         if (!isNewApp) {
790             Resources publisherRes = null;
791 
792             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
793                 final ShortcutInfo si = mShortcuts.valueAt(i);
794 
795                 // Disable dynamic shortcuts whose target activity is gone.
796                 if (si.isDynamic()) {
797                     if (si.getActivity() == null) {
798                         // Note if it's dynamic, it must have a target activity, but b/36228253.
799                         s.wtf("null activity detected.");
800                         // TODO Maybe remove it?
801                     } else if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
802                         Slog.w(TAG, String.format(
803                                 "%s is no longer main activity. Disabling shorcut %s.",
804                                 getPackageName(), si.getId()));
805                         if (disableDynamicWithId(si.getId(), /*ignoreInvisible*/ false,
806                                 ShortcutInfo.DISABLED_REASON_APP_CHANGED)) {
807                             continue; // Actually removed.
808                         }
809                         // Still pinned, so fall-through and possibly update the resources.
810                     }
811                 }
812 
813                 if (si.hasAnyResources()) {
814                     if (!si.isOriginallyFromManifest()) {
815                         if (publisherRes == null) {
816                             publisherRes = getPackageResources();
817                             if (publisherRes == null) {
818                                 break; // Resources couldn't be loaded.
819                             }
820                         }
821 
822                         // If this shortcut is not from a manifest, then update all resource IDs
823                         // from resource names.  (We don't allow resource strings for
824                         // non-manifest at the moment, but icons can still be resources.)
825                         si.lookupAndFillInResourceIds(publisherRes);
826                     }
827                     si.setTimestamp(s.injectCurrentTimeMillis());
828                 }
829             }
830         }
831 
832         // (Re-)publish manifest shortcut.
833         publishManifestShortcuts(newManifestShortcutList);
834 
835         if (newManifestShortcutList != null) {
836             pushOutExcessShortcuts();
837         }
838 
839         s.verifyStates();
840 
841         // This will send a notification to the launcher, and also save .
842         s.packageShortcutsChanged(getPackageName(), getPackageUserId());
843         return true; // true means changed.
844     }
845 
publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList)846     private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) {
847         if (ShortcutService.DEBUG) {
848             Slog.d(TAG, String.format(
849                     "Package %s: publishing manifest shortcuts", getPackageName()));
850         }
851         boolean changed = false;
852 
853         // Keep the previous IDs.
854         ArraySet<String> toDisableList = null;
855         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
856             final ShortcutInfo si = mShortcuts.valueAt(i);
857 
858             if (si.isManifestShortcut()) {
859                 if (toDisableList == null) {
860                     toDisableList = new ArraySet<>();
861                 }
862                 toDisableList.add(si.getId());
863             }
864         }
865 
866         // Publish new ones.
867         if (newManifestShortcutList != null) {
868             final int newListSize = newManifestShortcutList.size();
869 
870             for (int i = 0; i < newListSize; i++) {
871                 changed = true;
872 
873                 final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
874                 final boolean newDisabled = !newShortcut.isEnabled();
875 
876                 final String id = newShortcut.getId();
877                 final ShortcutInfo oldShortcut = mShortcuts.get(id);
878 
879                 boolean wasPinned = false;
880 
881                 if (oldShortcut != null) {
882                     if (!oldShortcut.isOriginallyFromManifest()) {
883                         Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId()
884                                 + " exists but is not from AndroidManifest.xml, not updating.");
885                         continue;
886                     }
887                     // Take over the pinned flag.
888                     if (oldShortcut.isPinned()) {
889                         wasPinned = true;
890                         newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
891                     }
892                 }
893                 if (newDisabled && !wasPinned) {
894                     // If the shortcut is disabled, and it was *not* pinned, then this
895                     // just doesn't have to be published.
896                     // Just keep it in toDisableList, so the previous one would be removed.
897                     continue;
898                 }
899 
900                 // Note even if enabled=false, we still need to update all fields, so do it
901                 // regardless.
902                 forceReplaceShortcutInner(newShortcut); // This will clean up the old one too.
903 
904                 if (!newDisabled && toDisableList != null) {
905                     // Still alive, don't remove.
906                     toDisableList.remove(id);
907                 }
908             }
909         }
910 
911         // Disable the previous manifest shortcuts that are no longer in the manifest.
912         if (toDisableList != null) {
913             if (ShortcutService.DEBUG) {
914                 Slog.d(TAG, String.format(
915                         "Package %s: disabling %d stale shortcuts", getPackageName(),
916                         toDisableList.size()));
917             }
918             for (int i = toDisableList.size() - 1; i >= 0; i--) {
919                 changed = true;
920 
921                 final String id = toDisableList.valueAt(i);
922 
923                 disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0,
924                         /* overrideImmutable=*/ true, /*ignoreInvisible=*/ false,
925                         ShortcutInfo.DISABLED_REASON_APP_CHANGED);
926             }
927             removeOrphans();
928         }
929         adjustRanks();
930         return changed;
931     }
932 
933     /**
934      * For each target activity, make sure # of dynamic + manifest shortcuts <= max.
935      * If too many, we'll remove the dynamic with the lowest ranks.
936      */
pushOutExcessShortcuts()937     private boolean pushOutExcessShortcuts() {
938         final ShortcutService service = mShortcutUser.mService;
939         final int maxShortcuts = service.getMaxActivityShortcuts();
940 
941         boolean changed = false;
942 
943         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
944                 sortShortcutsToActivities();
945         for (int outer = all.size() - 1; outer >= 0; outer--) {
946             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
947             if (list.size() <= maxShortcuts) {
948                 continue;
949             }
950             // Sort by isManifestShortcut() and getRank().
951             Collections.sort(list, mShortcutTypeAndRankComparator);
952 
953             // Keep [0 .. max), and remove (as dynamic) [max .. size)
954             for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) {
955                 final ShortcutInfo shortcut = list.get(inner);
956 
957                 if (shortcut.isManifestShortcut()) {
958                     // This shouldn't happen -- excess shortcuts should all be non-manifest.
959                     // But just in case.
960                     service.wtf("Found manifest shortcuts in excess list.");
961                     continue;
962                 }
963                 deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true);
964             }
965         }
966 
967         return changed;
968     }
969 
970     /**
971      * To sort by isManifestShortcut() and getRank(). i.e.  manifest shortcuts come before
972      * non-manifest shortcuts, then sort by rank.
973      *
974      * This is used to decide which dynamic shortcuts to remove when an upgraded version has more
975      * manifest shortcuts than before and as a result we need to remove some of the dynamic
976      * shortcuts.  We sort manifest + dynamic shortcuts by this order, and remove the ones with
977      * the last ones.
978      *
979      * (Note the number of manifest shortcuts is always <= the max number, because if there are
980      * more, ShortcutParser would ignore the rest.)
981      */
982     final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a,
983             ShortcutInfo b) -> {
984         if (a.isManifestShortcut() && !b.isManifestShortcut()) {
985             return -1;
986         }
987         if (!a.isManifestShortcut() && b.isManifestShortcut()) {
988             return 1;
989         }
990         return Integer.compare(a.getRank(), b.getRank());
991     };
992 
993     /**
994      * Build a list of shortcuts for each target activity and return as a map. The result won't
995      * contain "floating" shortcuts because they don't belong on any activities.
996      */
sortShortcutsToActivities()997     private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() {
998         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts
999                 = new ArrayMap<>();
1000         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1001             final ShortcutInfo si = mShortcuts.valueAt(i);
1002             if (si.isFloating()) {
1003                 continue; // Ignore floating shortcuts, which are not tied to any activities.
1004             }
1005 
1006             final ComponentName activity = si.getActivity();
1007             if (activity == null) {
1008                 mShortcutUser.mService.wtf("null activity detected.");
1009                 continue;
1010             }
1011 
1012             ArrayList<ShortcutInfo> list = activitiesToShortcuts.get(activity);
1013             if (list == null) {
1014                 list = new ArrayList<>();
1015                 activitiesToShortcuts.put(activity, list);
1016             }
1017             list.add(si);
1018         }
1019         return activitiesToShortcuts;
1020     }
1021 
1022     /** Used by {@link #enforceShortcutCountsBeforeOperation} */
incrementCountForActivity(ArrayMap<ComponentName, Integer> counts, ComponentName cn, int increment)1023     private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts,
1024             ComponentName cn, int increment) {
1025         Integer oldValue = counts.get(cn);
1026         if (oldValue == null) {
1027             oldValue = 0;
1028         }
1029 
1030         counts.put(cn, oldValue + increment);
1031     }
1032 
1033     /**
1034      * Called by
1035      * {@link android.content.pm.ShortcutManager#setDynamicShortcuts},
1036      * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and
1037      * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing
1038      * the operation to make sure the operation wouldn't result in the target activities having
1039      * more than the allowed number of dynamic/manifest shortcuts.
1040      *
1041      * @param newList shortcut list passed to set, add or updateShortcuts().
1042      * @param operation add, set or update.
1043      * @throws IllegalArgumentException if the operation would result in going over the max
1044      *                                  shortcut count for any activity.
1045      */
enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList, @ShortcutOperation int operation)1046     public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList,
1047             @ShortcutOperation int operation) {
1048         final ShortcutService service = mShortcutUser.mService;
1049 
1050         // Current # of dynamic / manifest shortcuts for each activity.
1051         // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced
1052         // anyway.)
1053         final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4);
1054         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1055             final ShortcutInfo shortcut = mShortcuts.valueAt(i);
1056 
1057             if (shortcut.isManifestShortcut()) {
1058                 incrementCountForActivity(counts, shortcut.getActivity(), 1);
1059             } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) {
1060                 incrementCountForActivity(counts, shortcut.getActivity(), 1);
1061             }
1062         }
1063 
1064         for (int i = newList.size() - 1; i >= 0; i--) {
1065             final ShortcutInfo newShortcut = newList.get(i);
1066             final ComponentName newActivity = newShortcut.getActivity();
1067             if (newActivity == null) {
1068                 if (operation != ShortcutService.OPERATION_UPDATE) {
1069                     service.wtf("Activity must not be null at this point");
1070                     continue; // Just ignore this invalid case.
1071                 }
1072                 continue; // Activity can be null for update.
1073             }
1074 
1075             final ShortcutInfo original = mShortcuts.get(newShortcut.getId());
1076             if (original == null) {
1077                 if (operation == ShortcutService.OPERATION_UPDATE) {
1078                     continue; // When updating, ignore if there's no target.
1079                 }
1080                 // Add() or set(), and there's no existing shortcut with the same ID.  We're
1081                 // simply publishing (as opposed to updating) this shortcut, so just +1.
1082                 incrementCountForActivity(counts, newActivity, 1);
1083                 continue;
1084             }
1085             if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) {
1086                 // Updating floating shortcuts doesn't affect the count, so ignore.
1087                 continue;
1088             }
1089 
1090             // If it's add() or update(), then need to decrement for the previous activity.
1091             // Skip it for set() since it's already been taken care of by not counting the original
1092             // dynamic shortcuts in the first loop.
1093             if (operation != ShortcutService.OPERATION_SET) {
1094                 final ComponentName oldActivity = original.getActivity();
1095                 if (!original.isFloating()) {
1096                     incrementCountForActivity(counts, oldActivity, -1);
1097                 }
1098             }
1099             incrementCountForActivity(counts, newActivity, 1);
1100         }
1101 
1102         // Then make sure none of the activities have more than the max number of shortcuts.
1103         for (int i = counts.size() - 1; i >= 0; i--) {
1104             service.enforceMaxActivityShortcuts(counts.valueAt(i));
1105         }
1106     }
1107 
1108     /**
1109      * For all the text fields, refresh the string values if they're from resources.
1110      */
resolveResourceStrings()1111     public void resolveResourceStrings() {
1112         final ShortcutService s = mShortcutUser.mService;
1113         boolean changed = false;
1114 
1115         Resources publisherRes = null;
1116         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1117             final ShortcutInfo si = mShortcuts.valueAt(i);
1118 
1119             if (si.hasStringResources()) {
1120                 changed = true;
1121 
1122                 if (publisherRes == null) {
1123                     publisherRes = getPackageResources();
1124                     if (publisherRes == null) {
1125                         break; // Resources couldn't be loaded.
1126                     }
1127                 }
1128 
1129                 si.resolveResourceStrings(publisherRes);
1130                 si.setTimestamp(s.injectCurrentTimeMillis());
1131             }
1132         }
1133         if (changed) {
1134             s.packageShortcutsChanged(getPackageName(), getPackageUserId());
1135         }
1136     }
1137 
1138     /** Clears the implicit ranks for all shortcuts. */
clearAllImplicitRanks()1139     public void clearAllImplicitRanks() {
1140         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1141             final ShortcutInfo si = mShortcuts.valueAt(i);
1142             si.clearImplicitRankAndRankChangedFlag();
1143         }
1144     }
1145 
1146     /**
1147      * Used to sort shortcuts for rank auto-adjusting.
1148      */
1149     final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> {
1150         // First, sort by rank.
1151         int ret = Integer.compare(a.getRank(), b.getRank());
1152         if (ret != 0) {
1153             return ret;
1154         }
1155         // When ranks are tie, then prioritize the ones that have just been assigned new ranks.
1156         // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively,
1157         // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because
1158         // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set.
1159         // Similarly, updating s3's rank to 1 will insert it between s1 and s2.
1160         if (a.isRankChanged() != b.isRankChanged()) {
1161             return a.isRankChanged() ? -1 : 1;
1162         }
1163         // If they're still tie, sort by implicit rank -- i.e. preserve the order in which
1164         // they're passed to the API.
1165         ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank());
1166         if (ret != 0) {
1167             return ret;
1168         }
1169         // If they're still tie, just sort by their IDs.
1170         // This may happen with updateShortcuts() -- see
1171         // the testUpdateShortcuts_noManifestShortcuts() test.
1172         return a.getId().compareTo(b.getId());
1173     };
1174 
1175     /**
1176      * Re-calculate the ranks for all shortcuts.
1177      */
adjustRanks()1178     public void adjustRanks() {
1179         final ShortcutService s = mShortcutUser.mService;
1180         final long now = s.injectCurrentTimeMillis();
1181 
1182         // First, clear ranks for floating shortcuts.
1183         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1184             final ShortcutInfo si = mShortcuts.valueAt(i);
1185             if (si.isFloating()) {
1186                 if (si.getRank() != 0) {
1187                     si.setTimestamp(now);
1188                     si.setRank(0);
1189                 }
1190             }
1191         }
1192 
1193         // Then adjust ranks.  Ranks are unique for each activity, so we first need to sort
1194         // shortcuts to each activity.
1195         // Then sort the shortcuts within each activity with mShortcutRankComparator, and
1196         // assign ranks from 0.
1197         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
1198                 sortShortcutsToActivities();
1199         for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity.
1200             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
1201 
1202             // Sort by ranks and other signals.
1203             Collections.sort(list, mShortcutRankComparator);
1204 
1205             int rank = 0;
1206 
1207             final int size = list.size();
1208             for (int i = 0; i < size; i++) {
1209                 final ShortcutInfo si = list.get(i);
1210                 if (si.isManifestShortcut()) {
1211                     // Don't adjust ranks for manifest shortcuts.
1212                     continue;
1213                 }
1214                 // At this point, it must be dynamic.
1215                 if (!si.isDynamic()) {
1216                     s.wtf("Non-dynamic shortcut found.");
1217                     continue;
1218                 }
1219                 final int thisRank = rank++;
1220                 if (si.getRank() != thisRank) {
1221                     si.setTimestamp(now);
1222                     si.setRank(thisRank);
1223                 }
1224             }
1225         }
1226     }
1227 
1228     /** @return true if there's any shortcuts that are not manifest shortcuts. */
hasNonManifestShortcuts()1229     public boolean hasNonManifestShortcuts() {
1230         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1231             final ShortcutInfo si = mShortcuts.valueAt(i);
1232             if (!si.isDeclaredInManifest()) {
1233                 return true;
1234             }
1235         }
1236         return false;
1237     }
1238 
dump(@onNull PrintWriter pw, @NonNull String prefix, DumpFilter filter)1239     public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
1240         pw.println();
1241 
1242         pw.print(prefix);
1243         pw.print("Package: ");
1244         pw.print(getPackageName());
1245         pw.print("  UID: ");
1246         pw.print(mPackageUid);
1247         pw.println();
1248 
1249         pw.print(prefix);
1250         pw.print("  ");
1251         pw.print("Calls: ");
1252         pw.print(getApiCallCount(/*unlimited=*/ false));
1253         pw.println();
1254 
1255         // getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
1256         pw.print(prefix);
1257         pw.print("  ");
1258         pw.print("Last known FG: ");
1259         pw.print(mLastKnownForegroundElapsedTime);
1260         pw.println();
1261 
1262         // This should be after getApiCallCount(), which may update it.
1263         pw.print(prefix);
1264         pw.print("  ");
1265         pw.print("Last reset: [");
1266         pw.print(mLastResetTime);
1267         pw.print("] ");
1268         pw.print(ShortcutService.formatTime(mLastResetTime));
1269         pw.println();
1270 
1271         getPackageInfo().dump(pw, prefix + "  ");
1272         pw.println();
1273 
1274         pw.print(prefix);
1275         pw.println("  Shortcuts:");
1276         long totalBitmapSize = 0;
1277         final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
1278         final int size = shortcuts.size();
1279         for (int i = 0; i < size; i++) {
1280             final ShortcutInfo si = shortcuts.valueAt(i);
1281             pw.println(si.toDumpString(prefix + "    "));
1282             if (si.getBitmapPath() != null) {
1283                 final long len = new File(si.getBitmapPath()).length();
1284                 pw.print(prefix);
1285                 pw.print("      ");
1286                 pw.print("bitmap size=");
1287                 pw.println(len);
1288 
1289                 totalBitmapSize += len;
1290             }
1291         }
1292         pw.print(prefix);
1293         pw.print("  ");
1294         pw.print("Total bitmap size: ");
1295         pw.print(totalBitmapSize);
1296         pw.print(" (");
1297         pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize));
1298         pw.println(")");
1299     }
1300 
1301     @Override
dumpCheckin(boolean clear)1302     public JSONObject dumpCheckin(boolean clear) throws JSONException {
1303         final JSONObject result = super.dumpCheckin(clear);
1304 
1305         int numDynamic = 0;
1306         int numPinned = 0;
1307         int numManifest = 0;
1308         int numBitmaps = 0;
1309         long totalBitmapSize = 0;
1310 
1311         final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
1312         final int size = shortcuts.size();
1313         for (int i = 0; i < size; i++) {
1314             final ShortcutInfo si = shortcuts.valueAt(i);
1315 
1316             if (si.isDynamic()) numDynamic++;
1317             if (si.isDeclaredInManifest()) numManifest++;
1318             if (si.isPinned()) numPinned++;
1319 
1320             if (si.getBitmapPath() != null) {
1321                 numBitmaps++;
1322                 totalBitmapSize += new File(si.getBitmapPath()).length();
1323             }
1324         }
1325 
1326         result.put(KEY_DYNAMIC, numDynamic);
1327         result.put(KEY_MANIFEST, numManifest);
1328         result.put(KEY_PINNED, numPinned);
1329         result.put(KEY_BITMAPS, numBitmaps);
1330         result.put(KEY_BITMAP_BYTES, totalBitmapSize);
1331 
1332         // TODO Log update frequency too.
1333 
1334         return result;
1335     }
1336 
1337     @Override
saveToXml(@onNull XmlSerializer out, boolean forBackup)1338     public void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
1339             throws IOException, XmlPullParserException {
1340         final int size = mShortcuts.size();
1341 
1342         if (size == 0 && mApiCallCount == 0) {
1343             return; // nothing to write.
1344         }
1345 
1346         out.startTag(null, TAG_ROOT);
1347 
1348         ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
1349         ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
1350         ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
1351         getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
1352 
1353         for (int j = 0; j < size; j++) {
1354             saveShortcut(out, mShortcuts.valueAt(j), forBackup,
1355                     getPackageInfo().isBackupAllowed());
1356         }
1357 
1358         out.endTag(null, TAG_ROOT);
1359     }
1360 
saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup, boolean appSupportsBackup)1361     private void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup,
1362             boolean appSupportsBackup)
1363             throws IOException, XmlPullParserException {
1364 
1365         final ShortcutService s = mShortcutUser.mService;
1366 
1367         if (forBackup) {
1368             if (!(si.isPinned() && si.isEnabled())) {
1369                 // We only backup pinned shortcuts that are enabled.
1370                 // Note, this means, shortcuts that are restored but are blocked restore, e.g. due
1371                 // to a lower version code, will not be ported to a new device.
1372                 return;
1373             }
1374         }
1375         final boolean shouldBackupDetails =
1376                 !forBackup // It's not backup
1377                 || appSupportsBackup; // Or, it's a backup and app supports backup.
1378 
1379         // Note: at this point no shortcuts should have bitmaps pending save, but if they do,
1380         // just remove the bitmap.
1381         if (si.isIconPendingSave()) {
1382             s.removeIconLocked(si);
1383         }
1384         out.startTag(null, TAG_SHORTCUT);
1385         ShortcutService.writeAttr(out, ATTR_ID, si.getId());
1386         // writeAttr(out, "package", si.getPackageName()); // not needed
1387         ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity());
1388         // writeAttr(out, "icon", si.getIcon());  // We don't save it.
1389         ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
1390         ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
1391         ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName());
1392         ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
1393         ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
1394         ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName());
1395         if (shouldBackupDetails) {
1396             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
1397             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
1398                     si.getDisabledMessageResourceId());
1399             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME,
1400                     si.getDisabledMessageResName());
1401         }
1402         ShortcutService.writeAttr(out, ATTR_DISABLED_REASON, si.getDisabledReason());
1403         ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
1404                 si.getLastChangedTimestamp());
1405         if (forBackup) {
1406             // Don't write icon information.  Also drop the dynamic flag.
1407 
1408             int flags = si.getFlags() &
1409                     ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
1410                             | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE
1411                             | ShortcutInfo.FLAG_DYNAMIC);
1412             ShortcutService.writeAttr(out, ATTR_FLAGS, flags);
1413 
1414             // Set the publisher version code at every backup.
1415             final long packageVersionCode = getPackageInfo().getVersionCode();
1416             if (packageVersionCode == 0) {
1417                 s.wtf("Package version code should be available at this point.");
1418                 // However, 0 is a valid version code, so we just go ahead with it...
1419             }
1420         } else {
1421             // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored
1422             // as dynamic.
1423             ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
1424 
1425             ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
1426             ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId());
1427             ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName());
1428             ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
1429         }
1430 
1431         if (shouldBackupDetails) {
1432             {
1433                 final Set<String> cat = si.getCategories();
1434                 if (cat != null && cat.size() > 0) {
1435                     out.startTag(null, TAG_CATEGORIES);
1436                     XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
1437                             NAME_CATEGORIES, out);
1438                     out.endTag(null, TAG_CATEGORIES);
1439                 }
1440             }
1441             final Intent[] intentsNoExtras = si.getIntentsNoExtras();
1442             final PersistableBundle[] intentsExtras = si.getIntentPersistableExtrases();
1443             final int numIntents = intentsNoExtras.length;
1444             for (int i = 0; i < numIntents; i++) {
1445                 out.startTag(null, TAG_INTENT);
1446                 ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]);
1447                 ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]);
1448                 out.endTag(null, TAG_INTENT);
1449             }
1450 
1451             ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
1452         }
1453 
1454         out.endTag(null, TAG_SHORTCUT);
1455     }
1456 
loadFromXml(ShortcutService s, ShortcutUser shortcutUser, XmlPullParser parser, boolean fromBackup)1457     public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
1458             XmlPullParser parser, boolean fromBackup)
1459             throws IOException, XmlPullParserException {
1460 
1461         final String packageName = ShortcutService.parseStringAttribute(parser,
1462                 ATTR_NAME);
1463 
1464         final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
1465                 shortcutUser.getUserId(), packageName);
1466 
1467         ret.mApiCallCount =
1468                 ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
1469         ret.mLastResetTime =
1470                 ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
1471 
1472 
1473         final int outerDepth = parser.getDepth();
1474         int type;
1475         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
1476                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
1477             if (type != XmlPullParser.START_TAG) {
1478                 continue;
1479             }
1480             final int depth = parser.getDepth();
1481             final String tag = parser.getName();
1482             if (depth == outerDepth + 1) {
1483                 switch (tag) {
1484                     case ShortcutPackageInfo.TAG_ROOT:
1485                         ret.getPackageInfo().loadFromXml(parser, fromBackup);
1486 
1487                         continue;
1488                     case TAG_SHORTCUT:
1489                         final ShortcutInfo si = parseShortcut(parser, packageName,
1490                                 shortcutUser.getUserId(), fromBackup);
1491 
1492                         // Don't use addShortcut(), we don't need to save the icon.
1493                         ret.mShortcuts.put(si.getId(), si);
1494                         continue;
1495                 }
1496             }
1497             ShortcutService.warnForInvalidTag(depth, tag);
1498         }
1499         return ret;
1500     }
1501 
parseShortcut(XmlPullParser parser, String packageName, @UserIdInt int userId, boolean fromBackup)1502     private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName,
1503             @UserIdInt int userId, boolean fromBackup)
1504             throws IOException, XmlPullParserException {
1505         String id;
1506         ComponentName activityComponent;
1507         // Icon icon;
1508         String title;
1509         int titleResId;
1510         String titleResName;
1511         String text;
1512         int textResId;
1513         String textResName;
1514         String disabledMessage;
1515         int disabledMessageResId;
1516         String disabledMessageResName;
1517         int disabledReason;
1518         Intent intentLegacy;
1519         PersistableBundle intentPersistableExtrasLegacy = null;
1520         ArrayList<Intent> intents = new ArrayList<>();
1521         int rank;
1522         PersistableBundle extras = null;
1523         long lastChangedTimestamp;
1524         int flags;
1525         int iconResId;
1526         String iconResName;
1527         String bitmapPath;
1528         int backupVersionCode;
1529         ArraySet<String> categories = null;
1530 
1531         id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
1532         activityComponent = ShortcutService.parseComponentNameAttribute(parser,
1533                 ATTR_ACTIVITY);
1534         title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
1535         titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
1536         titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
1537         text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
1538         textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
1539         textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
1540         disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
1541         disabledMessageResId = ShortcutService.parseIntAttribute(parser,
1542                 ATTR_DISABLED_MESSAGE_RES_ID);
1543         disabledMessageResName = ShortcutService.parseStringAttribute(parser,
1544                 ATTR_DISABLED_MESSAGE_RES_NAME);
1545         disabledReason = ShortcutService.parseIntAttribute(parser, ATTR_DISABLED_REASON);
1546         intentLegacy = ShortcutService.parseIntentAttributeNoDefault(parser, ATTR_INTENT_LEGACY);
1547         rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
1548         lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
1549         flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
1550         iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID);
1551         iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME);
1552         bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
1553 
1554         final int outerDepth = parser.getDepth();
1555         int type;
1556         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
1557                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
1558             if (type != XmlPullParser.START_TAG) {
1559                 continue;
1560             }
1561             final int depth = parser.getDepth();
1562             final String tag = parser.getName();
1563             if (ShortcutService.DEBUG_LOAD) {
1564                 Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
1565                         depth, type, tag));
1566             }
1567             switch (tag) {
1568                 case TAG_INTENT_EXTRAS_LEGACY:
1569                     intentPersistableExtrasLegacy = PersistableBundle.restoreFromXml(parser);
1570                     continue;
1571                 case TAG_INTENT:
1572                     intents.add(parseIntent(parser));
1573                     continue;
1574                 case TAG_EXTRAS:
1575                     extras = PersistableBundle.restoreFromXml(parser);
1576                     continue;
1577                 case TAG_CATEGORIES:
1578                     // This just contains string-array.
1579                     continue;
1580                 case TAG_STRING_ARRAY_XMLUTILS:
1581                     if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
1582                             ATTR_NAME_XMLUTILS))) {
1583                         final String[] ar = XmlUtils.readThisStringArrayXml(
1584                                 parser, TAG_STRING_ARRAY_XMLUTILS, null);
1585                         categories = new ArraySet<>(ar.length);
1586                         for (int i = 0; i < ar.length; i++) {
1587                             categories.add(ar[i]);
1588                         }
1589                     }
1590                     continue;
1591             }
1592             throw ShortcutService.throwForInvalidTag(depth, tag);
1593         }
1594 
1595         if (intentLegacy != null) {
1596             // For the legacy file format which supported only one intent per shortcut.
1597             ShortcutInfo.setIntentExtras(intentLegacy, intentPersistableExtrasLegacy);
1598             intents.clear();
1599             intents.add(intentLegacy);
1600         }
1601 
1602 
1603         if ((disabledReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)
1604                 && ((flags & ShortcutInfo.FLAG_DISABLED) != 0)) {
1605             // We didn't used to have the disabled reason, so if a shortcut is disabled
1606             // and has no reason, we assume it was disabled by publisher.
1607             disabledReason = ShortcutInfo.DISABLED_REASON_BY_APP;
1608         }
1609 
1610         // All restored shortcuts are initially "shadow".
1611         if (fromBackup) {
1612             flags |= ShortcutInfo.FLAG_SHADOW;
1613         }
1614 
1615         return new ShortcutInfo(
1616                 userId, id, packageName, activityComponent, /* icon =*/ null,
1617                 title, titleResId, titleResName, text, textResId, textResName,
1618                 disabledMessage, disabledMessageResId, disabledMessageResName,
1619                 categories,
1620                 intents.toArray(new Intent[intents.size()]),
1621                 rank, extras, lastChangedTimestamp, flags,
1622                 iconResId, iconResName, bitmapPath, disabledReason);
1623     }
1624 
parseIntent(XmlPullParser parser)1625     private static Intent parseIntent(XmlPullParser parser)
1626             throws IOException, XmlPullParserException {
1627 
1628         Intent intent = ShortcutService.parseIntentAttribute(parser,
1629                 ATTR_INTENT_NO_EXTRA);
1630 
1631         final int outerDepth = parser.getDepth();
1632         int type;
1633         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
1634                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
1635             if (type != XmlPullParser.START_TAG) {
1636                 continue;
1637             }
1638             final int depth = parser.getDepth();
1639             final String tag = parser.getName();
1640             if (ShortcutService.DEBUG_LOAD) {
1641                 Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
1642                         depth, type, tag));
1643             }
1644             switch (tag) {
1645                 case TAG_EXTRAS:
1646                     ShortcutInfo.setIntentExtras(intent,
1647                             PersistableBundle.restoreFromXml(parser));
1648                     continue;
1649             }
1650             throw ShortcutService.throwForInvalidTag(depth, tag);
1651         }
1652         return intent;
1653     }
1654 
1655     @VisibleForTesting
getAllShortcutsForTest()1656     List<ShortcutInfo> getAllShortcutsForTest() {
1657         return new ArrayList<>(mShortcuts.values());
1658     }
1659 
1660     @Override
verifyStates()1661     public void verifyStates() {
1662         super.verifyStates();
1663 
1664         boolean failed = false;
1665 
1666         final ShortcutService s = mShortcutUser.mService;
1667 
1668         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
1669                 sortShortcutsToActivities();
1670 
1671         // Make sure each activity won't have more than max shortcuts.
1672         for (int outer = all.size() - 1; outer >= 0; outer--) {
1673             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
1674             if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) {
1675                 failed = true;
1676                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer)
1677                         + " has " + all.valueAt(outer).size() + " shortcuts.");
1678             }
1679 
1680             // Sort by rank.
1681             Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank()));
1682 
1683             // Split into two arrays for each kind.
1684             final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list);
1685             dynamicList.removeIf((si) -> !si.isDynamic());
1686 
1687             final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list);
1688             dynamicList.removeIf((si) -> !si.isManifestShortcut());
1689 
1690             verifyRanksSequential(dynamicList);
1691             verifyRanksSequential(manifestList);
1692         }
1693 
1694         // Verify each shortcut's status.
1695         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1696             final ShortcutInfo si = mShortcuts.valueAt(i);
1697             if (!(si.isDeclaredInManifest() || si.isDynamic() || si.isPinned())) {
1698                 failed = true;
1699                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1700                         + " is not manifest, dynamic or pinned.");
1701             }
1702             if (si.isDeclaredInManifest() && si.isDynamic()) {
1703                 failed = true;
1704                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1705                         + " is both dynamic and manifest at the same time.");
1706             }
1707             if (si.getActivity() == null && !si.isFloating()) {
1708                 failed = true;
1709                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1710                         + " has null activity, but not floating.");
1711             }
1712             if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) {
1713                 failed = true;
1714                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1715                         + " is not floating, but is disabled.");
1716             }
1717             if (si.isFloating() && si.getRank() != 0) {
1718                 failed = true;
1719                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1720                         + " is floating, but has rank=" + si.getRank());
1721             }
1722             if (si.getIcon() != null) {
1723                 failed = true;
1724                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1725                         + " still has an icon");
1726             }
1727             if (si.hasAdaptiveBitmap() && !si.hasIconFile()) {
1728                 failed = true;
1729                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1730                     + " has adaptive bitmap but was not saved to a file.");
1731             }
1732             if (si.hasIconFile() && si.hasIconResource()) {
1733                 failed = true;
1734                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1735                         + " has both resource and bitmap icons");
1736             }
1737             if (si.isEnabled()
1738                     != (si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)) {
1739                 failed = true;
1740                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1741                         + " isEnabled() and getDisabledReason() disagree: "
1742                         + si.isEnabled() + " vs " + si.getDisabledReason());
1743             }
1744             if ((si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER)
1745                     && (getPackageInfo().getBackupSourceVersionCode()
1746                     == ShortcutInfo.VERSION_CODE_UNKNOWN)) {
1747                 failed = true;
1748                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1749                         + " RESTORED_VERSION_LOWER with no backup source version code.");
1750             }
1751             if (s.isDummyMainActivity(si.getActivity())) {
1752                 failed = true;
1753                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1754                         + " has a dummy target activity");
1755             }
1756         }
1757 
1758         if (failed) {
1759             throw new IllegalStateException("See logcat for errors");
1760         }
1761     }
1762 
verifyRanksSequential(List<ShortcutInfo> list)1763     private boolean verifyRanksSequential(List<ShortcutInfo> list) {
1764         boolean failed = false;
1765 
1766         for (int i = 0; i < list.size(); i++) {
1767             final ShortcutInfo si = list.get(i);
1768             if (si.getRank() != i) {
1769                 failed = true;
1770                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
1771                         + " rank=" + si.getRank() + " but expected to be "+ i);
1772             }
1773         }
1774         return failed;
1775     }
1776 }
1777