• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.Person;
22 import android.app.usage.UsageStatsManagerInternal;
23 import android.content.ComponentName;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.LocusId;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.ShortcutInfo;
29 import android.content.pm.ShortcutManager;
30 import android.content.res.Resources;
31 import android.graphics.drawable.Icon;
32 import android.os.Binder;
33 import android.os.PersistableBundle;
34 import android.os.StrictMode;
35 import android.os.SystemClock;
36 import android.text.format.Formatter;
37 import android.util.ArrayMap;
38 import android.util.ArraySet;
39 import android.util.Log;
40 import android.util.Slog;
41 import android.util.Xml;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.os.BackgroundThread;
46 import com.android.internal.util.ArrayUtils;
47 import com.android.internal.util.CollectionUtils;
48 import com.android.internal.util.Preconditions;
49 import com.android.internal.util.XmlUtils;
50 import com.android.modules.utils.TypedXmlPullParser;
51 import com.android.modules.utils.TypedXmlSerializer;
52 import com.android.server.pm.ShortcutService.DumpFilter;
53 import com.android.server.pm.ShortcutService.ShortcutOperation;
54 import com.android.server.pm.ShortcutService.Stats;
55 
56 import org.json.JSONException;
57 import org.json.JSONObject;
58 import org.xmlpull.v1.XmlPullParser;
59 import org.xmlpull.v1.XmlPullParserException;
60 
61 import java.io.File;
62 import java.io.FileInputStream;
63 import java.io.IOException;
64 import java.io.PrintWriter;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collection;
68 import java.util.Collections;
69 import java.util.Comparator;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 import java.util.Set;
74 import java.util.concurrent.Executor;
75 import java.util.function.Consumer;
76 import java.util.function.Function;
77 import java.util.function.Predicate;
78 import java.util.stream.Collectors;
79 
80 /**
81  * Package information used by {@link ShortcutService}.
82  * User information used by {@link ShortcutService}.
83  */
84 class ShortcutPackage extends ShortcutPackageItem {
85     private static final String TAG = ShortcutService.TAG;
86     private static final String TAG_VERIFY = ShortcutService.TAG + ".verify";
87 
88     static final String TAG_ROOT = "package";
89     private static final String TAG_INTENT_EXTRAS_LEGACY = "intent-extras";
90     private static final String TAG_INTENT = "intent";
91     private static final String TAG_EXTRAS = "extras";
92     private static final String TAG_SHORTCUT = "shortcut";
93     private static final String TAG_SHARE_TARGET = "share-target";
94     private static final String TAG_CATEGORIES = "categories";
95     private static final String TAG_PERSON = "person";
96 
97     private static final String ATTR_NAME = "name";
98     private static final String ATTR_CALL_COUNT = "call-count";
99     private static final String ATTR_LAST_RESET = "last-reset";
100     private static final String ATTR_SCHEMA_VERSON = "schema-version";
101     private static final String ATTR_ID = "id";
102     private static final String ATTR_ACTIVITY = "activity";
103     private static final String ATTR_TITLE = "title";
104     private static final String ATTR_TITLE_RES_ID = "titleid";
105     private static final String ATTR_TITLE_RES_NAME = "titlename";
106     private static final String ATTR_TEXT = "text";
107     private static final String ATTR_TEXT_RES_ID = "textid";
108     private static final String ATTR_TEXT_RES_NAME = "textname";
109     private static final String ATTR_DISABLED_MESSAGE = "dmessage";
110     private static final String ATTR_DISABLED_MESSAGE_RES_ID = "dmessageid";
111     private static final String ATTR_DISABLED_MESSAGE_RES_NAME = "dmessagename";
112     private static final String ATTR_DISABLED_REASON = "disabled-reason";
113     private static final String ATTR_INTENT_LEGACY = "intent";
114     private static final String ATTR_INTENT_NO_EXTRA = "intent-base";
115     private static final String ATTR_RANK = "rank";
116     private static final String ATTR_TIMESTAMP = "timestamp";
117     private static final String ATTR_FLAGS = "flags";
118     private static final String ATTR_ICON_RES_ID = "icon-res";
119     private static final String ATTR_ICON_RES_NAME = "icon-resname";
120     private static final String ATTR_BITMAP_PATH = "bitmap-path";
121     private static final String ATTR_ICON_URI = "icon-uri";
122     private static final String ATTR_LOCUS_ID = "locus-id";
123     private static final String ATTR_SPLASH_SCREEN_THEME_NAME = "splash-screen-theme-name";
124 
125     private static final String ATTR_PERSON_NAME = "name";
126     private static final String ATTR_PERSON_URI = "uri";
127     private static final String ATTR_PERSON_KEY = "key";
128     private static final String ATTR_PERSON_IS_BOT = "is-bot";
129     private static final String ATTR_PERSON_IS_IMPORTANT = "is-important";
130 
131     private static final String NAME_CATEGORIES = "categories";
132     private static final String NAME_CAPABILITY = "capability";
133 
134     private static final String TAG_STRING_ARRAY_XMLUTILS = "string-array";
135     private static final String TAG_MAP_XMLUTILS = "map";
136     private static final String ATTR_NAME_XMLUTILS = "name";
137 
138     private static final String KEY_DYNAMIC = "dynamic";
139     private static final String KEY_MANIFEST = "manifest";
140     private static final String KEY_PINNED = "pinned";
141     private static final String KEY_BITMAPS = "bitmaps";
142     private static final String KEY_BITMAP_BYTES = "bitmapBytes";
143 
144     private final Executor mExecutor;
145 
146     /**
147      * An in-memory copy of shortcuts for this package that was loaded from xml, keyed on IDs.
148      */
149     @GuardedBy("mPackageItemLock")
150     private final ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
151 
152     /**
153      * All the share targets from the package
154      */
155     @GuardedBy("mPackageItemLock")
156     private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0);
157 
158     /**
159      * # of times the package has called rate-limited APIs.
160      */
161     private int mApiCallCount;
162 
163     /**
164      * When {@link #mApiCallCount} was reset last time.
165      */
166     private long mLastResetTime;
167 
168     private final int mPackageUid;
169 
170     private long mLastKnownForegroundElapsedTime;
171 
172     @GuardedBy("mPackageItemLock")
173     private long mLastReportedTime;
174 
ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi)175     private ShortcutPackage(ShortcutUser shortcutUser,
176             int packageUserId, String packageName, ShortcutPackageInfo spi) {
177         super(shortcutUser, packageUserId, packageName,
178                 spi != null ? spi : ShortcutPackageInfo.newEmpty());
179 
180         mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId);
181         mExecutor = BackgroundThread.getExecutor();
182     }
183 
ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName)184     public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) {
185         this(shortcutUser, packageUserId, packageName, null);
186     }
187 
188     @Override
getOwnerUserId()189     public int getOwnerUserId() {
190         // For packages, always owner user == package user.
191         return getPackageUserId();
192     }
193 
getPackageUid()194     public int getPackageUid() {
195         return mPackageUid;
196     }
197 
198     @Nullable
getPackageResources()199     public Resources getPackageResources() {
200         return mShortcutUser.mService.injectGetResourcesForApplicationAsUser(
201                 getPackageName(), getPackageUserId());
202     }
203 
getShortcutCount()204     public int getShortcutCount() {
205         synchronized (mPackageItemLock) {
206             return mShortcuts.size();
207         }
208     }
209 
210     @Override
canRestoreAnyVersion()211     protected boolean canRestoreAnyVersion() {
212         return true;
213     }
214 
215     @Override
onRestored(int restoreBlockReason)216     protected void onRestored(int restoreBlockReason) {
217         // Shortcuts have been restored.
218         // - Unshadow all shortcuts.
219         // - Set disabled reason.
220         // - Disable if needed.
221         forEachShortcutMutate(si -> {
222             if (restoreBlockReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED
223                     && !si.hasFlags(ShortcutInfo.FLAG_SHADOW)
224                     && si.getDisabledReason() == restoreBlockReason) {
225                 return;
226             }
227             si.clearFlags(ShortcutInfo.FLAG_SHADOW);
228 
229             si.setDisabledReason(restoreBlockReason);
230             if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
231                 si.addFlags(ShortcutInfo.FLAG_DISABLED);
232             }
233         });
234         // Because some launchers may not have been restored (e.g. allowBackup=false),
235         // we need to re-calculate the pinned shortcuts.
236         refreshPinnedFlags();
237     }
238 
239     /**
240      * Note this does *not* provide a correct view to the calling launcher.
241      */
242     @Nullable
findShortcutById(@ullable final String id)243     public ShortcutInfo findShortcutById(@Nullable final String id) {
244         if (id == null) return null;
245         synchronized (mPackageItemLock) {
246             return mShortcuts.get(id);
247         }
248     }
249 
isShortcutExistsAndInvisibleToPublisher(String id)250     public boolean isShortcutExistsAndInvisibleToPublisher(String id) {
251         ShortcutInfo si = findShortcutById(id);
252         return si != null && !si.isVisibleToPublisher();
253     }
254 
isShortcutExistsAndVisibleToPublisher(String id)255     public boolean isShortcutExistsAndVisibleToPublisher(String id) {
256         ShortcutInfo si = findShortcutById(id);
257         return si != null && si.isVisibleToPublisher();
258     }
259 
ensureNotImmutable(@ullable ShortcutInfo shortcut, boolean ignoreInvisible)260     private void ensureNotImmutable(@Nullable ShortcutInfo shortcut, boolean ignoreInvisible) {
261         if (shortcut != null && shortcut.isImmutable()
262                 && (!ignoreInvisible || shortcut.isVisibleToPublisher())) {
263             throw new IllegalArgumentException(
264                     "Manifest shortcut ID=" + shortcut.getId()
265                             + " may not be manipulated via APIs");
266         }
267     }
268 
ensureNotImmutable(@onNull String id, boolean ignoreInvisible)269     public void ensureNotImmutable(@NonNull String id, boolean ignoreInvisible) {
270         ensureNotImmutable(findShortcutById(id), ignoreInvisible);
271     }
272 
ensureImmutableShortcutsNotIncludedWithIds(@onNull List<String> shortcutIds, boolean ignoreInvisible)273     public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List<String> shortcutIds,
274             boolean ignoreInvisible) {
275         for (int i = shortcutIds.size() - 1; i >= 0; i--) {
276             ensureNotImmutable(shortcutIds.get(i), ignoreInvisible);
277         }
278     }
279 
ensureImmutableShortcutsNotIncluded(@onNull List<ShortcutInfo> shortcuts, boolean ignoreInvisible)280     public void ensureImmutableShortcutsNotIncluded(@NonNull List<ShortcutInfo> shortcuts,
281             boolean ignoreInvisible) {
282         for (int i = shortcuts.size() - 1; i >= 0; i--) {
283             ensureNotImmutable(shortcuts.get(i).getId(), ignoreInvisible);
284         }
285     }
286 
ensureNoBitmapIconIfShortcutIsLongLived(@onNull List<ShortcutInfo> shortcuts)287     public void ensureNoBitmapIconIfShortcutIsLongLived(@NonNull List<ShortcutInfo> shortcuts) {
288         for (int i = shortcuts.size() - 1; i >= 0; i--) {
289             final ShortcutInfo si = shortcuts.get(i);
290             if (!si.isLongLived()) {
291                 continue;
292             }
293             final Icon icon = si.getIcon();
294             if (icon != null && icon.getType() != Icon.TYPE_BITMAP
295                     && icon.getType() != Icon.TYPE_ADAPTIVE_BITMAP) {
296                 continue;
297             }
298             if (icon == null && !si.hasIconFile()) {
299                 continue;
300             }
301 
302             // TODO: Throw IllegalArgumentException instead.
303             Slog.e(TAG, "Invalid icon type in shortcut " + si.getId() + ". Bitmaps are not allowed"
304                     + " in long-lived shortcuts. Use Resource icons, or Uri-based icons instead.");
305             return;  // Do not spam and return early.
306         }
307     }
308 
ensureAllShortcutsVisibleToLauncher(@onNull List<ShortcutInfo> shortcuts)309     public void ensureAllShortcutsVisibleToLauncher(@NonNull List<ShortcutInfo> shortcuts) {
310         for (ShortcutInfo shortcut : shortcuts) {
311             if (shortcut.isExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER)) {
312                 throw new IllegalArgumentException("Shortcut ID=" + shortcut.getId()
313                         + " is hidden from launcher and may not be manipulated via APIs");
314             }
315         }
316     }
317 
318     /**
319      * Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible.
320      */
forceDeleteShortcutInner(@onNull String id)321     private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) {
322         final ShortcutInfo shortcut;
323         synchronized (mPackageItemLock) {
324             shortcut = mShortcuts.remove(id);
325             if (shortcut != null) {
326                 removeIcon(shortcut);
327                 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
328                         | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED_ALL);
329             }
330         }
331         return shortcut;
332     }
333 
334     /**
335      * Force replace a shortcut. If there's already a shortcut with the same ID, it'll be removed,
336      * even if it's invisible.
337      */
forceReplaceShortcutInner(@onNull ShortcutInfo newShortcut)338     private void forceReplaceShortcutInner(@NonNull ShortcutInfo newShortcut) {
339         final ShortcutService s = mShortcutUser.mService;
340 
341         forceDeleteShortcutInner(newShortcut.getId());
342 
343         // Extract Icon and update the icon res ID and the bitmap path.
344         s.saveIconAndFixUpShortcutLocked(this, newShortcut);
345         s.fixUpShortcutResourceNamesAndValues(newShortcut);
346         ensureShortcutCountBeforePush();
347         saveShortcut(newShortcut);
348     }
349 
350     /**
351      * Add a shortcut. If there's already a one with the same ID, it'll be removed, even if it's
352      * invisible.
353      *
354      * It checks the max number of dynamic shortcuts.
355      *
356      * @return True if it replaced an existing shortcut, False otherwise.
357      */
addOrReplaceDynamicShortcut(@onNull ShortcutInfo newShortcut)358     public boolean addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) {
359 
360         Preconditions.checkArgument(newShortcut.isEnabled(),
361                 "add/setDynamicShortcuts() cannot publish disabled shortcuts");
362 
363         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
364 
365         final ShortcutInfo oldShortcut = findShortcutById(newShortcut.getId());
366         if (oldShortcut != null) {
367             // It's an update case.
368             // Make sure the target is updatable. (i.e. should be mutable.)
369             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
370 
371             // If it was originally pinned or cached, the new one should be pinned or cached too.
372             newShortcut.addFlags(oldShortcut.getFlags()
373                     & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL));
374         }
375 
376         if (!newShortcut.isExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER)) {
377             forceReplaceShortcutInner(newShortcut);
378         }
379         return oldShortcut != null;
380     }
381 
382     /**
383      * Push a shortcut. If the max number of dynamic shortcuts is already reached, remove the
384      * shortcut with the lowest rank before adding the new shortcut.
385      *
386      * Any shortcut that gets altered (removed or changed) as a result of this push operation will
387      * be included and returned in changedShortcuts.
388      *
389      * @return True if a shortcut had to be removed to complete this operation, False otherwise.
390      */
pushDynamicShortcut(@onNull ShortcutInfo newShortcut, @NonNull List<ShortcutInfo> changedShortcuts)391     public boolean pushDynamicShortcut(@NonNull ShortcutInfo newShortcut,
392             @NonNull List<ShortcutInfo> changedShortcuts) {
393         Preconditions.checkArgument(newShortcut.isEnabled(),
394                 "pushDynamicShortcuts() cannot publish disabled shortcuts");
395 
396         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
397 
398         changedShortcuts.clear();
399         final ShortcutInfo oldShortcut = findShortcutById(newShortcut.getId());
400         boolean deleted = false;
401 
402         if (oldShortcut == null || !oldShortcut.isDynamic()) {
403             final ShortcutService service = mShortcutUser.mService;
404             final int maxShortcuts = service.getMaxActivityShortcuts();
405 
406             final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
407                     sortShortcutsToActivities();
408             final ArrayList<ShortcutInfo> activityShortcuts = all.get(newShortcut.getActivity());
409 
410             if (activityShortcuts != null && activityShortcuts.size() > maxShortcuts) {
411                 // Root cause was discovered in b/233155034, so this should not be happening.
412                 service.wtf("Error pushing shortcut. There are already "
413                         + activityShortcuts.size() + " shortcuts.");
414             }
415             if (activityShortcuts != null && activityShortcuts.size() == maxShortcuts) {
416                 // Max has reached. Delete the shortcut with lowest rank.
417                 // Sort by isManifestShortcut() and getRank().
418                 Collections.sort(activityShortcuts, mShortcutTypeAndRankComparator);
419 
420                 final ShortcutInfo shortcut = activityShortcuts.get(maxShortcuts - 1);
421                 if (shortcut.isManifestShortcut()) {
422                     // All shortcuts are manifest shortcuts and cannot be removed.
423                     Slog.e(TAG, "Failed to remove manifest shortcut while pushing dynamic shortcut "
424                             + newShortcut.getId());
425                     return true;  // poppedShortcuts is empty which indicates a failure.
426                 }
427 
428                 changedShortcuts.add(shortcut);
429                 deleted = deleteDynamicWithId(shortcut.getId(), /* ignoreInvisible =*/ true) != null;
430             }
431         }
432         if (oldShortcut != null) {
433             // It's an update case.
434             // Make sure the target is updatable. (i.e. should be mutable.)
435             oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false);
436 
437             // If it was originally pinned or cached, the new one should be pinned or cached too.
438             newShortcut.addFlags(oldShortcut.getFlags()
439                     & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL));
440         }
441 
442         if (!newShortcut.isExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER)) {
443             forceReplaceShortcutInner(newShortcut);
444         }
445         return deleted;
446     }
447 
ensureShortcutCountBeforePush()448     private void ensureShortcutCountBeforePush() {
449         final ShortcutService service = mShortcutUser.mService;
450         // Ensure the total number of shortcuts doesn't exceed the hard limit per app.
451         final int maxShortcutPerApp = service.getMaxAppShortcuts();
452         synchronized (mPackageItemLock) {
453             final List<ShortcutInfo> appShortcuts = mShortcuts.values().stream().filter(si ->
454                     !si.isPinned()).collect(Collectors.toList());
455             if (appShortcuts.size() >= maxShortcutPerApp) {
456                 // Max has reached. Removes shortcuts until they fall within the hard cap.
457                 // Sort by isManifestShortcut(), isDynamic() and getLastChangedTimestamp().
458                 Collections.sort(appShortcuts, mShortcutTypeRankAndTimeComparator);
459 
460                 while (appShortcuts.size() >= maxShortcutPerApp) {
461                     final ShortcutInfo shortcut = appShortcuts.remove(appShortcuts.size() - 1);
462                     if (shortcut.isDeclaredInManifest()) {
463                         // All shortcuts are manifest shortcuts and cannot be removed.
464                         throw new IllegalArgumentException(getPackageName() + " has published "
465                                 + appShortcuts.size() + " manifest shortcuts across different"
466                                 + " activities.");
467                     }
468                     forceDeleteShortcutInner(shortcut.getId());
469                 }
470             }
471         }
472     }
473 
474     /**
475      * Remove all shortcuts that aren't pinned, cached nor dynamic.
476      *
477      * @return List of removed shortcuts.
478      */
removeOrphans()479     private List<ShortcutInfo> removeOrphans() {
480         final List<ShortcutInfo> removeList = new ArrayList<>(1);
481         forEachShortcut(si -> {
482             if (si.isAlive()) return;
483             removeList.add(si);
484         });
485         if (!removeList.isEmpty()) {
486             for (int i = removeList.size() - 1; i >= 0; i--) {
487                 forceDeleteShortcutInner(removeList.get(i).getId());
488             }
489         }
490         return removeList;
491     }
492 
493     /**
494      * Remove all dynamic shortcuts.
495      *
496      * @return List of shortcuts that actually got removed.
497      */
deleteAllDynamicShortcuts()498     public List<ShortcutInfo> deleteAllDynamicShortcuts() {
499         final long now = mShortcutUser.mService.injectCurrentTimeMillis();
500         boolean changed = false;
501         synchronized (mPackageItemLock) {
502             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
503                 ShortcutInfo si = mShortcuts.valueAt(i);
504                 if (si.isDynamic() && si.isVisibleToPublisher()) {
505                     changed = true;
506 
507                     si.setTimestamp(now);
508                     si.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
509                     si.setRank(0); // It may still be pinned, so clear the rank.
510                 }
511             }
512         }
513         if (changed) {
514             return removeOrphans();
515         }
516         return null;
517     }
518 
519     /**
520      * Remove a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
521      * is pinned or cached, it'll remain as a pinned or cached shortcut, and is still enabled.
522      *
523      * @return The deleted shortcut, or null if it was not actually removed because it is either
524      * pinned or cached.
525      */
deleteDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible)526     public ShortcutInfo deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) {
527         return deleteOrDisableWithId(
528                 shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible,
529                 ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
530     }
531 
532     /**
533      * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
534      * is pinned, it'll remain as a pinned shortcut, but will be disabled.
535      *
536      * @return Shortcut if the disabled shortcut got removed because it wasn't pinned. Or null if
537      * it's still pinned.
538      */
disableDynamicWithId(@onNull String shortcutId, boolean ignoreInvisible, int disabledReason)539     private ShortcutInfo disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible,
540             int disabledReason) {
541         return deleteOrDisableWithId(shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false,
542                 ignoreInvisible, disabledReason);
543     }
544 
545     /**
546      * Remove a long lived shortcut by ID. If the shortcut is pinned, it'll remain as a pinned
547      * shortcut, and is still enabled.
548      *
549      * @return The deleted shortcut, or null if it was not actually removed because it's pinned.
550      */
deleteLongLivedWithId(@onNull String shortcutId, boolean ignoreInvisible)551     public ShortcutInfo deleteLongLivedWithId(@NonNull String shortcutId, boolean ignoreInvisible) {
552         final ShortcutInfo shortcut = findShortcutById(shortcutId);
553         if (shortcut != null) {
554             mutateShortcut(shortcutId, null, si -> si.clearFlags(ShortcutInfo.FLAG_CACHED_ALL));
555         }
556         return deleteOrDisableWithId(
557                 shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible,
558                 ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
559     }
560 
561     /**
562      * Disable a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
563      * is pinned, it'll remain as a pinned shortcut but will be disabled.
564      *
565      * @return Shortcut if the disabled shortcut got removed because it wasn't pinned. Or null if
566      * it's still pinned.
567      */
disableWithId(@onNull String shortcutId, String disabledMessage, int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)568     public ShortcutInfo disableWithId(@NonNull String shortcutId, String disabledMessage,
569             int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible,
570             int disabledReason) {
571         final ShortcutInfo deleted = deleteOrDisableWithId(shortcutId, /* disable =*/ true,
572                 overrideImmutable, ignoreInvisible, disabledReason);
573 
574         // If disabled id still exists, it is pinned and we need to update the disabled message.
575         mutateShortcut(shortcutId, null, disabled -> {
576             if (disabled != null) {
577                 if (disabledMessage != null) {
578                     disabled.setDisabledMessage(disabledMessage);
579                 } else if (disabledMessageResId != 0) {
580                     disabled.setDisabledMessageResId(disabledMessageResId);
581                     mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled);
582                 }
583             }
584         });
585 
586         return deleted;
587     }
588 
589     @Nullable
deleteOrDisableWithId(@onNull String shortcutId, boolean disable, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason)590     private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable,
591             boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) {
592         Preconditions.checkState(
593                 (disable == (disabledReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED)),
594                 "disable and disabledReason disagree: " + disable + " vs " + disabledReason);
595         final ShortcutInfo oldShortcut = findShortcutById(shortcutId);
596 
597         if (oldShortcut == null || !oldShortcut.isEnabled()
598                 && (ignoreInvisible && !oldShortcut.isVisibleToPublisher())) {
599             return null; // Doesn't exist or already disabled.
600         }
601         if (!overrideImmutable) {
602             ensureNotImmutable(oldShortcut, /*ignoreInvisible=*/ true);
603         }
604         if (oldShortcut.isPinned() || oldShortcut.isCached()) {
605             mutateShortcut(oldShortcut.getId(), oldShortcut, si -> {
606                 si.setRank(0);
607                 si.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST);
608                 if (disable) {
609                     si.addFlags(ShortcutInfo.FLAG_DISABLED);
610                     // Do not overwrite the disabled reason if one is already set.
611                     if (si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
612                         si.setDisabledReason(disabledReason);
613                     }
614                 }
615                 si.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis());
616 
617                 // See ShortcutRequestPinProcessor.directPinShortcut().
618                 if (mShortcutUser.mService.isDummyMainActivity(si.getActivity())) {
619                     si.setActivity(null);
620                 }
621             });
622             return null;
623         } else {
624             forceDeleteShortcutInner(shortcutId);
625             return oldShortcut;
626         }
627     }
628 
enableWithId(@onNull String shortcutId)629     public void enableWithId(@NonNull String shortcutId) {
630         mutateShortcut(shortcutId, null, si -> {
631             ensureNotImmutable(si, /*ignoreInvisible=*/ true);
632             si.clearFlags(ShortcutInfo.FLAG_DISABLED);
633             si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
634         });
635     }
636 
updateInvisibleShortcutForPinRequestWith(@onNull ShortcutInfo shortcut)637     public void updateInvisibleShortcutForPinRequestWith(@NonNull ShortcutInfo shortcut) {
638         final ShortcutInfo source = findShortcutById(shortcut.getId());
639         Objects.requireNonNull(source);
640 
641         mShortcutUser.mService.validateShortcutForPinRequest(shortcut);
642 
643         shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
644 
645         forceReplaceShortcutInner(shortcut);
646 
647         adjustRanks();
648     }
649 
650     /**
651      * Called after a launcher updates the pinned set.  For each shortcut in this package,
652      * set FLAG_PINNED if any launcher has pinned it.  Otherwise, clear it.
653      *
654      * <p>Then remove all shortcuts that are not dynamic and no longer pinned either.
655      */
refreshPinnedFlags()656     public void refreshPinnedFlags() {
657         final Set<String> pinnedShortcuts = new ArraySet<>();
658 
659         // First, gather the pinned set from each launcher.
660         mShortcutUser.forAllLaunchers(launcherShortcuts -> {
661             final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
662                     getPackageName(), getPackageUserId());
663             if (pinned == null || pinned.size() == 0) {
664                 return;
665             }
666             pinnedShortcuts.addAll(pinned);
667         });
668         // Secondly, update the pinned state if necessary.
669         final List<ShortcutInfo> pinned = findAll(pinnedShortcuts);
670         if (pinned != null) {
671             pinned.forEach(si -> {
672                 if (!si.isPinned()) {
673                     si.addFlags(ShortcutInfo.FLAG_PINNED);
674                 }
675             });
676         }
677         forEachShortcutMutate(si -> {
678             if (!pinnedShortcuts.contains(si.getId()) && si.isPinned()) {
679                 si.clearFlags(ShortcutInfo.FLAG_PINNED);
680             }
681         });
682         // Then, schedule a background job to persist the pinned states.
683         mShortcutUser.forAllLaunchers(ShortcutPackageItem::scheduleSave);
684 
685         // Lastly, remove the ones that are no longer pinned, cached nor dynamic.
686         removeOrphans();
687     }
688 
689     /**
690      * Number of calls that the caller has made, since the last reset.
691      *
692      * <p>This takes care of the resetting the counter for foreground apps as well as after
693      * locale changes.
694      */
getApiCallCount(boolean unlimited)695     public int getApiCallCount(boolean unlimited) {
696         final ShortcutService s = mShortcutUser.mService;
697 
698         // Reset the counter if:
699         // - the package is in foreground now.
700         // - the package is *not* in foreground now, but was in foreground at some point
701         // since the previous time it had been.
702         if (s.isUidForegroundLocked(mPackageUid)
703                 || (mLastKnownForegroundElapsedTime
704                     < s.getUidLastForegroundElapsedTimeLocked(mPackageUid))
705                 || unlimited) {
706             mLastKnownForegroundElapsedTime = s.injectElapsedRealtime();
707             resetRateLimiting();
708         }
709 
710         // Note resetThrottlingIfNeeded() and resetRateLimiting() will set 0 to mApiCallCount,
711         // but we just can't return 0 at this point, because we may have to update
712         // mLastResetTime.
713 
714         final long last = s.getLastResetTimeLocked();
715 
716         final long now = s.injectCurrentTimeMillis();
717         if (ShortcutService.isClockValid(now) && mLastResetTime > now) {
718             Slog.w(TAG, "Clock rewound");
719             // Clock rewound.
720             mLastResetTime = now;
721             mApiCallCount = 0;
722             return mApiCallCount;
723         }
724 
725         // If not reset yet, then reset.
726         if (mLastResetTime < last) {
727             if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
728                 Slog.d(TAG, String.format("%s: last reset=%d, now=%d, last=%d: resetting",
729                         getPackageName(), mLastResetTime, now, last));
730             }
731             mApiCallCount = 0;
732             mLastResetTime = last;
733         }
734         return mApiCallCount;
735     }
736 
737     /**
738      * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
739      * and return true.  Otherwise just return false.
740      *
741      * <p>This takes care of the resetting the counter for foreground apps as well as after
742      * locale changes, which is done internally by {@link #getApiCallCount}.
743      */
tryApiCall(boolean unlimited)744     public boolean tryApiCall(boolean unlimited) {
745         final ShortcutService s = mShortcutUser.mService;
746 
747         if (getApiCallCount(unlimited) >= s.mMaxUpdatesPerInterval) {
748             return false;
749         }
750         mApiCallCount++;
751         scheduleSave();
752         return true;
753     }
754 
resetRateLimiting()755     public void resetRateLimiting() {
756         if (ShortcutService.DEBUG) {
757             Slog.d(TAG, "resetRateLimiting: " + getPackageName());
758         }
759         if (mApiCallCount > 0) {
760             mApiCallCount = 0;
761             scheduleSave();
762         }
763     }
764 
resetRateLimitingForCommandLineNoSaving()765     public void resetRateLimitingForCommandLineNoSaving() {
766         mApiCallCount = 0;
767         mLastResetTime = 0;
768     }
769 
770     /**
771      * Find all shortcuts that match {@code query}.
772      */
findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> filter, int cloneFlag)773     public void findAll(@NonNull List<ShortcutInfo> result,
774             @Nullable Predicate<ShortcutInfo> filter, int cloneFlag) {
775         findAll(result, filter, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false);
776     }
777 
778     /**
779      * Find all shortcuts that match {@code query}.
780      *
781      * This will also provide a "view" for each launcher -- a non-dynamic shortcut that's not pinned
782      * by the calling launcher will not be included in the result, and also "isPinned" will be
783      * adjusted for the caller too.
784      */
findAll(@onNull List<ShortcutInfo> result, @Nullable Predicate<ShortcutInfo> filter, int cloneFlag, @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher)785     public void findAll(@NonNull List<ShortcutInfo> result,
786             @Nullable Predicate<ShortcutInfo> filter, int cloneFlag,
787             @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) {
788         if (getPackageInfo().isShadow()) {
789             Log.d(TAG, "findAll() returned empty results because " + getPackageName()
790                     + " isn't installed yet");
791             // Restored and the app not installed yet, so don't return any.
792             return;
793         }
794         final ShortcutService s = mShortcutUser.mService;
795 
796         // Set of pinned shortcuts by the calling launcher.
797         final ArraySet<String> pinnedByCallerSet = (callingLauncher == null) ? null
798                 : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId)
799                         .getPinnedShortcutIds(getPackageName(), getPackageUserId());
800         forEachShortcut(si -> filter(result, filter, cloneFlag, callingLauncher, pinnedByCallerSet,
801                 getPinnedByAnyLauncher, si));
802     }
803 
filter(@onNull final List<ShortcutInfo> result, @Nullable final Predicate<ShortcutInfo> query, final int cloneFlag, @Nullable final String callingLauncher, @NonNull final ArraySet<String> pinnedByCallerSet, final boolean getPinnedByAnyLauncher, @NonNull final ShortcutInfo si)804     private void filter(@NonNull final List<ShortcutInfo> result,
805             @Nullable final Predicate<ShortcutInfo> query, final int cloneFlag,
806             @Nullable final String callingLauncher,
807             @NonNull final ArraySet<String> pinnedByCallerSet,
808             final boolean getPinnedByAnyLauncher, @NonNull final ShortcutInfo si) {
809         // Need to adjust PINNED flag depending on the caller.
810         // Basically if the caller is a launcher (callingLauncher != null) and the launcher
811         // isn't pinning it, then we need to clear PINNED for this caller.
812         final boolean isPinnedByCaller = (callingLauncher == null)
813                 || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId()));
814 
815         if (!getPinnedByAnyLauncher) {
816             if (si.isFloating() && !si.isCached()) {
817                 if (!isPinnedByCaller) {
818                     Log.d(TAG, si.getId() + " ignored because it isn't pinned by "
819                             + callingLauncher);
820                     return;
821                 }
822             }
823         }
824         final ShortcutInfo clone = si.clone(cloneFlag);
825 
826         // Fix up isPinned for the caller.  Note we need to do it before the "test" callback,
827         // since it may check isPinned.
828         // However, if getPinnedByAnyLauncher is set, we do it after the test.
829         if (!getPinnedByAnyLauncher) {
830             if (!isPinnedByCaller) {
831                 clone.clearFlags(ShortcutInfo.FLAG_PINNED);
832             }
833         }
834         if (query == null || query.test(clone)) {
835             if (!isPinnedByCaller) {
836                 clone.clearFlags(ShortcutInfo.FLAG_PINNED);
837             }
838             result.add(clone);
839         }
840     }
841 
resetThrottling()842     public void resetThrottling() {
843         mApiCallCount = 0;
844     }
845 
846     /**
847      * Returns a list of ShortcutInfos that match the given intent filter and the category of
848      * available ShareTarget definitions in this package.
849      */
getMatchingShareTargets( @onNull final IntentFilter filter, final int callingUserId)850     public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
851             @NonNull final IntentFilter filter, final int callingUserId) {
852         return getMatchingShareTargets(filter, null, callingUserId);
853     }
854 
getMatchingShareTargets( @onNull final IntentFilter filter, @Nullable final String pkgName, final int callingUserId)855     List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
856             @NonNull final IntentFilter filter, @Nullable final String pkgName,
857             final int callingUserId) {
858         synchronized (mPackageItemLock) {
859             final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
860             for (int i = 0; i < mShareTargets.size(); i++) {
861                 final ShareTargetInfo target = mShareTargets.get(i);
862                 for (ShareTargetInfo.TargetData data : target.mTargetData) {
863                     if (filter.hasDataType(data.mMimeType)) {
864                         // Matched at least with one data type
865                         matchedTargets.add(target);
866                         break;
867                     }
868                 }
869             }
870 
871             if (matchedTargets.isEmpty()) {
872                 return new ArrayList<>();
873             }
874 
875             // Get the list of all dynamic shortcuts in this package.
876             final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
877             // Pass callingLauncher to ensure pinned flag marked by system ui, e.g. ShareSheet, are
878             // included in the result
879             findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
880                     ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
881                     pkgName, callingUserId, /*getPinnedByAnyLauncher=*/ false);
882 
883             final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
884             for (int i = 0; i < shortcuts.size(); i++) {
885                 final Set<String> categories = shortcuts.get(i).getCategories();
886                 if (categories == null || categories.isEmpty()) {
887                     continue;
888                 }
889                 for (int j = 0; j < matchedTargets.size(); j++) {
890                     // Shortcut must have all of share target categories
891                     boolean hasAllCategories = true;
892                     final ShareTargetInfo target = matchedTargets.get(j);
893                     for (int q = 0; q < target.mCategories.length; q++) {
894                         if (!categories.contains(target.mCategories[q])) {
895                             hasAllCategories = false;
896                             break;
897                         }
898                     }
899                     if (hasAllCategories) {
900                         result.add(new ShortcutManager.ShareShortcutInfo(shortcuts.get(i),
901                                 new ComponentName(getPackageName(), target.mTargetClass)));
902                         break;
903                     }
904                 }
905             }
906             return result;
907         }
908     }
909 
hasShareTargets()910     public boolean hasShareTargets() {
911         synchronized (mPackageItemLock) {
912             return !mShareTargets.isEmpty();
913         }
914     }
915 
916     /**
917      * Returns the number of shortcuts that can be used as a share target in the ShareSheet. Such
918      * shortcuts must have a matching category with at least one of the defined ShareTargets from
919      * the app's Xml resource.
920      */
getSharingShortcutCount()921     int getSharingShortcutCount() {
922         synchronized (mPackageItemLock) {
923             if (mShareTargets.isEmpty()) {
924                 return 0;
925             }
926 
927             // Get the list of all dynamic shortcuts in this package
928             final ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
929             findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
930                     ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
931 
932             int sharingShortcutCount = 0;
933             for (int i = 0; i < shortcuts.size(); i++) {
934                 final Set<String> categories = shortcuts.get(i).getCategories();
935                 if (categories == null || categories.isEmpty()) {
936                     continue;
937                 }
938                 for (int j = 0; j < mShareTargets.size(); j++) {
939                     // A SharingShortcut must have all of share target categories
940                     boolean hasAllCategories = true;
941                     final ShareTargetInfo target = mShareTargets.get(j);
942                     for (int q = 0; q < target.mCategories.length; q++) {
943                         if (!categories.contains(target.mCategories[q])) {
944                             hasAllCategories = false;
945                             break;
946                         }
947                     }
948                     if (hasAllCategories) {
949                         sharingShortcutCount++;
950                         break;
951                     }
952                 }
953             }
954             return sharingShortcutCount;
955         }
956     }
957 
958     /**
959      * Return the filenames (excluding path names) of icon bitmap files from this package.
960      */
961     @GuardedBy("mPackageItemLock")
getUsedBitmapFilesLocked()962     private ArraySet<String> getUsedBitmapFilesLocked() {
963         final ArraySet<String> usedFiles = new ArraySet<>(1);
964         forEachShortcut(si -> {
965             if (si.getBitmapPath() != null) {
966                 usedFiles.add(getFileName(si.getBitmapPath()));
967             }
968         });
969         return usedFiles;
970     }
971 
cleanupDanglingBitmapFiles(@onNull File path)972     public void cleanupDanglingBitmapFiles(@NonNull File path) {
973         synchronized (mPackageItemLock) {
974             mShortcutBitmapSaver.waitForAllSavesLocked();
975             final ArraySet<String> usedFiles = getUsedBitmapFilesLocked();
976 
977             for (File child : path.listFiles()) {
978                 if (!child.isFile()) {
979                     continue;
980                 }
981                 final String name = child.getName();
982                 if (!usedFiles.contains(name)) {
983                     if (ShortcutService.DEBUG) {
984                         Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
985                     }
986                     child.delete();
987                 }
988             }
989         }
990     }
991 
getFileName(@onNull String path)992     private static String getFileName(@NonNull String path) {
993         final int sep = path.lastIndexOf(File.separatorChar);
994         if (sep == -1) {
995             return path;
996         } else {
997             return path.substring(sep + 1);
998         }
999     }
1000 
1001     /**
1002      * @return false if any of the target activities are no longer enabled.
1003      */
areAllActivitiesStillEnabled()1004     private boolean areAllActivitiesStillEnabled() {
1005         final ShortcutService s = mShortcutUser.mService;
1006 
1007         // Normally the number of target activities is 1 or so, so no need to use a complex
1008         // structure like a set.
1009         final ArrayList<ComponentName> checked = new ArrayList<>(4);
1010         final boolean[] reject = new boolean[1];
1011 
1012         forEachShortcutStopWhen(si -> {
1013             final ComponentName activity = si.getActivity();
1014 
1015             if (checked.contains(activity)) {
1016                 return false; // Already checked.
1017             }
1018             checked.add(activity);
1019 
1020             if ((activity != null)
1021                     && !s.injectIsActivityEnabledAndExported(activity, getOwnerUserId())) {
1022                 reject[0] = true;
1023                 return true; // Found at least 1 activity is disabled, so skip the rest.
1024             }
1025             return false;
1026         });
1027         return !reject[0];
1028     }
1029 
1030     /**
1031      * Called when the package may be added or updated, or its activities may be disabled, and
1032      * if so, rescan the package and do the necessary stuff.
1033      *
1034      * Add case:
1035      * - Publish manifest shortcuts.
1036      *
1037      * Update case:
1038      * - Re-publish manifest shortcuts.
1039      * - If there are shortcuts with resources (icons or strings), update their timestamps.
1040      * - Disable shortcuts whose target activities are disabled.
1041      *
1042      * @return TRUE if any shortcuts have been changed.
1043      */
rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan)1044     public boolean rescanPackageIfNeeded(boolean isNewApp, boolean forceRescan) {
1045         final ShortcutService s = mShortcutUser.mService;
1046         final long start = s.getStatStartTime();
1047 
1048         final PackageInfo pi;
1049         try {
1050             pi = mShortcutUser.mService.getPackageInfo(
1051                     getPackageName(), getPackageUserId());
1052             if (pi == null) {
1053                 return false; // Shouldn't happen.
1054             }
1055 
1056             if (!isNewApp && !forceRescan) {
1057                 // Return if the package hasn't changed, ie:
1058                 // - version code hasn't change
1059                 // - lastUpdateTime hasn't change
1060                 // - all target activities are still enabled.
1061 
1062                 // Note, system apps timestamps do *not* change after OTAs.  (But they do
1063                 // after an adb sync or a local flash.)
1064                 // This means if a system app's version code doesn't change on an OTA,
1065                 // we don't notice it's updated.  But that's fine since their version code *should*
1066                 // really change on OTAs.
1067                 if ((getPackageInfo().getVersionCode() == pi.getLongVersionCode())
1068                         && (getPackageInfo().getLastUpdateTime() == pi.lastUpdateTime)
1069                         && areAllActivitiesStillEnabled()) {
1070                     return false;
1071                 }
1072             }
1073         } finally {
1074             s.logDurationStat(Stats.PACKAGE_UPDATE_CHECK, start);
1075         }
1076 
1077         // Now prepare to publish manifest shortcuts.
1078         List<ShortcutInfo> newManifestShortcutList = null;
1079         int shareTargetSize = 0;
1080         synchronized (mPackageItemLock) {
1081             try {
1082                 shareTargetSize = mShareTargets.size();
1083                 newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService,
1084                         getPackageName(), getPackageUserId(), mShareTargets);
1085             } catch (IOException | XmlPullParserException e) {
1086                 Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e);
1087             }
1088         }
1089         final int manifestShortcutSize = newManifestShortcutList == null ? 0
1090                 : newManifestShortcutList.size();
1091         if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
1092             Slog.d(TAG,
1093                     String.format(
1094                             "Package %s has %d manifest shortcut(s), and %d share target(s)",
1095                             getPackageName(), manifestShortcutSize, shareTargetSize));
1096         }
1097 
1098         if (isNewApp && (manifestShortcutSize == 0)) {
1099             // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do.
1100 
1101             // If it's an update, then it may already have manifest shortcuts, which need to be
1102             // disabled.
1103             return false;
1104         }
1105         if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
1106             Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
1107                     (isNewApp ? "added" : "updated"),
1108                     getPackageInfo().getVersionCode(), pi.getLongVersionCode()));
1109         }
1110         getPackageInfo().updateFromPackageInfo(pi);
1111 
1112         final long newVersionCode = getPackageInfo().getVersionCode();
1113 
1114         // See if there are any shortcuts that were prevented restoring because the app was of a
1115         // lower version, and re-enable them.
1116         {
1117             forEachShortcutMutate(si -> {
1118                 if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) {
1119                     return;
1120                 }
1121                 if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) {
1122                     if (ShortcutService.DEBUG) {
1123                         Slog.d(TAG,
1124                                 String.format(
1125                                         "Shortcut %s require version %s, still not restored.",
1126                                         si.getId(),
1127                                         getPackageInfo().getBackupSourceVersionCode()));
1128                     }
1129                     return;
1130                 }
1131                 Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId()));
1132                 si.clearFlags(ShortcutInfo.FLAG_DISABLED);
1133                 si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
1134             });
1135         }
1136 
1137         // For existing shortcuts, update timestamps if they have any resources.
1138         // Also check if shortcuts' activities are still main activities.  Otherwise, disable them.
1139         if (!isNewApp) {
1140             final Resources publisherRes = getPackageResources();
1141             forEachShortcutMutate(si -> {
1142                 // Disable dynamic shortcuts whose target activity is gone.
1143                 if (si.isDynamic()) {
1144                     if (si.getActivity() == null) {
1145                         // Note if it's dynamic, it must have a target activity, but b/36228253.
1146                         s.wtf("null activity detected.");
1147                         // TODO Maybe remove it?
1148                     } else if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
1149                         Slog.w(TAG, String.format(
1150                                 "%s is no longer main activity. Disabling shorcut %s.",
1151                                 getPackageName(), si.getId()));
1152                         if (disableDynamicWithId(si.getId(), /*ignoreInvisible*/ false,
1153                                 ShortcutInfo.DISABLED_REASON_APP_CHANGED) != null) {
1154                             return;
1155                         }
1156                         // Still pinned, so fall-through and possibly update the resources.
1157                     }
1158                 }
1159 
1160                 if (!si.hasAnyResources() || publisherRes == null) {
1161                     return;
1162                 }
1163 
1164                 if (!si.isOriginallyFromManifest()) {
1165                     si.lookupAndFillInResourceIds(publisherRes);
1166                 }
1167 
1168                 // If this shortcut is not from a manifest, then update all resource IDs
1169                 // from resource names.  (We don't allow resource strings for
1170                 // non-manifest at the moment, but icons can still be resources.)
1171                 si.setTimestamp(s.injectCurrentTimeMillis());
1172             });
1173         }
1174 
1175         // (Re-)publish manifest shortcut.
1176         publishManifestShortcuts(newManifestShortcutList);
1177 
1178         if (newManifestShortcutList != null) {
1179             pushOutExcessShortcuts();
1180         }
1181 
1182         s.verifyStates();
1183 
1184         // This will send a notification to the launcher, and also save .
1185         // TODO: List changed and removed manifest shortcuts and pass to packageShortcutsChanged()
1186         s.packageShortcutsChanged(this, null, null);
1187 
1188         return true;
1189     }
1190 
publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList)1191     private boolean publishManifestShortcuts(List<ShortcutInfo> newManifestShortcutList) {
1192         if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
1193             Slog.d(TAG, String.format(
1194                     "Package %s: publishing manifest shortcuts", getPackageName()));
1195         }
1196         boolean changed = false;
1197 
1198         // Keep the previous IDs.
1199         final ArraySet<String> toDisableList = new ArraySet<>(1);
1200         forEachShortcut(si -> {
1201             if (si.isManifestShortcut()) {
1202                 toDisableList.add(si.getId());
1203             }
1204         });
1205 
1206         // Publish new ones.
1207         if (newManifestShortcutList != null) {
1208             final int newListSize = newManifestShortcutList.size();
1209 
1210             for (int i = 0; i < newListSize; i++) {
1211                 changed = true;
1212 
1213                 final ShortcutInfo newShortcut = newManifestShortcutList.get(i);
1214                 final boolean newDisabled = !newShortcut.isEnabled();
1215 
1216                 final String id = newShortcut.getId();
1217                 final ShortcutInfo oldShortcut = findShortcutById(id);
1218 
1219                 boolean wasPinned = false;
1220 
1221                 if (oldShortcut != null) {
1222                     if (!oldShortcut.isOriginallyFromManifest()) {
1223                         Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId()
1224                                 + " exists but is not from AndroidManifest.xml, not updating.");
1225                         continue;
1226                     }
1227                     // Take over the pinned flag.
1228                     if (oldShortcut.isPinned()) {
1229                         wasPinned = true;
1230                         newShortcut.addFlags(ShortcutInfo.FLAG_PINNED);
1231                     }
1232                 }
1233                 if (newDisabled && !wasPinned) {
1234                     // If the shortcut is disabled, and it was *not* pinned, then this
1235                     // just doesn't have to be published.
1236                     // Just keep it in toDisableList, so the previous one would be removed.
1237                     continue;
1238                 }
1239 
1240                 // Note even if enabled=false, we still need to update all fields, so do it
1241                 // regardless.
1242                 forceReplaceShortcutInner(newShortcut); // This will clean up the old one too.
1243 
1244                 if (!newDisabled && !toDisableList.isEmpty()) {
1245                     // Still alive, don't remove.
1246                     toDisableList.remove(id);
1247                 }
1248             }
1249         }
1250 
1251         // Disable the previous manifest shortcuts that are no longer in the manifest.
1252         if (!toDisableList.isEmpty()) {
1253             if (ShortcutService.DEBUG) {
1254                 Slog.d(TAG, String.format(
1255                         "Package %s: disabling %d stale shortcuts", getPackageName(),
1256                         toDisableList.size()));
1257             }
1258             for (int i = toDisableList.size() - 1; i >= 0; i--) {
1259                 changed = true;
1260 
1261                 final String id = toDisableList.valueAt(i);
1262 
1263                 disableWithId(id, /* disable message =*/ null,
1264                         /* disable message resid */ 0,
1265                         /* overrideImmutable=*/ true, /*ignoreInvisible=*/ false,
1266                         ShortcutInfo.DISABLED_REASON_APP_CHANGED);
1267             }
1268             removeOrphans();
1269         }
1270 
1271         adjustRanks();
1272         return changed;
1273     }
1274 
1275     /**
1276      * For each target activity, make sure # of dynamic + manifest shortcuts <= max.
1277      * If too many, we'll remove the dynamic with the lowest ranks.
1278      */
pushOutExcessShortcuts()1279     private boolean pushOutExcessShortcuts() {
1280         final ShortcutService service = mShortcutUser.mService;
1281         final int maxShortcuts = service.getMaxActivityShortcuts();
1282 
1283         boolean changed = false;
1284 
1285         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
1286                 sortShortcutsToActivities();
1287         for (int outer = all.size() - 1; outer >= 0; outer--) {
1288             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
1289             if (list.size() <= maxShortcuts) {
1290                 continue;
1291             }
1292             // Sort by isManifestShortcut() and getRank().
1293             Collections.sort(list, mShortcutTypeAndRankComparator);
1294 
1295             // Keep [0 .. max), and remove (as dynamic) [max .. size)
1296             for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) {
1297                 final ShortcutInfo shortcut = list.get(inner);
1298 
1299                 if (shortcut.isManifestShortcut()) {
1300                     // This shouldn't happen -- excess shortcuts should all be non-manifest.
1301                     // But just in case.
1302                     service.wtf("Found manifest shortcuts in excess list.");
1303                     continue;
1304                 }
1305                 deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true);
1306             }
1307         }
1308 
1309         return changed;
1310     }
1311 
1312     /**
1313      * To sort by isManifestShortcut() and getRank(). i.e.  manifest shortcuts come before
1314      * non-manifest shortcuts, then sort by rank.
1315      *
1316      * This is used to decide which dynamic shortcuts to remove when an upgraded version has more
1317      * manifest shortcuts than before and as a result we need to remove some of the dynamic
1318      * shortcuts.  We sort manifest + dynamic shortcuts by this order, and remove the ones with
1319      * the last ones.
1320      *
1321      * (Note the number of manifest shortcuts is always <= the max number, because if there are
1322      * more, ShortcutParser would ignore the rest.)
1323      */
1324     final Comparator<ShortcutInfo> mShortcutTypeAndRankComparator = (ShortcutInfo a,
1325             ShortcutInfo b) -> {
1326         if (a.isManifestShortcut() && !b.isManifestShortcut()) {
1327             return -1;
1328         }
1329         if (!a.isManifestShortcut() && b.isManifestShortcut()) {
1330             return 1;
1331         }
1332         return Integer.compare(a.getRank(), b.getRank());
1333     };
1334 
1335     /**
1336      * To sort by isManifestShortcut(), isDynamic(), getRank() and
1337      * getLastChangedTimestamp(). i.e. manifest shortcuts come before non-manifest shortcuts,
1338      * dynamic shortcuts come before floating shortcuts, then sort by last changed timestamp.
1339      *
1340      * This is used to decide which shortcuts to remove when the total number of shortcuts retained
1341      * for the app exceeds the limit defined in {@link ShortcutService#getMaxAppShortcuts()}.
1342      *
1343      * (Note the number of manifest shortcuts is always <= the max number, because if there are
1344      * more, ShortcutParser would ignore the rest.)
1345      */
1346     final Comparator<ShortcutInfo> mShortcutTypeRankAndTimeComparator = (ShortcutInfo a,
1347             ShortcutInfo b) -> {
1348         if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
1349             return -1;
1350         }
1351         if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
1352             return 1;
1353         }
1354         if (a.isDynamic() && b.isDynamic()) {
1355             return Integer.compare(a.getRank(), b.getRank());
1356         }
1357         if (a.isDynamic()) {
1358             return -1;
1359         }
1360         if (b.isDynamic()) {
1361             return 1;
1362         }
1363         if (a.isCached() && b.isCached()) {
1364             // if both shortcuts are cached, prioritize shortcuts cached by people tile,
1365             if (a.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)
1366                     && !b.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)) {
1367                 return -1;
1368             } else if (!a.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)
1369                     && b.hasFlags(ShortcutInfo.FLAG_CACHED_PEOPLE_TILE)) {
1370                 return 1;
1371             }
1372             // followed by bubbles.
1373             if (a.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)
1374                     && !b.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)) {
1375                 return -1;
1376             } else if (!a.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)
1377                     && b.hasFlags(ShortcutInfo.FLAG_CACHED_BUBBLES)) {
1378                 return 1;
1379             }
1380         }
1381         if (a.isCached()) {
1382             return -1;
1383         }
1384         if (b.isCached()) {
1385             return 1;
1386         }
1387         return Long.compare(b.getLastChangedTimestamp(), a.getLastChangedTimestamp());
1388     };
1389 
1390     /**
1391      * Build a list of shortcuts for each target activity and return as a map. The result won't
1392      * contain "floating" shortcuts because they don't belong on any activities.
1393      */
sortShortcutsToActivities()1394     private ArrayMap<ComponentName, ArrayList<ShortcutInfo>> sortShortcutsToActivities() {
1395         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> activitiesToShortcuts
1396                 = new ArrayMap<>();
1397         forEachShortcut(si -> {
1398             if (si.isFloating()) {
1399                 return; // Ignore floating shortcuts, which are not tied to any activities.
1400             }
1401 
1402             final ComponentName activity = si.getActivity();
1403             if (activity == null) {
1404                 mShortcutUser.mService.wtf("null activity detected.");
1405                 return;
1406             }
1407 
1408             ArrayList<ShortcutInfo> list = activitiesToShortcuts.computeIfAbsent(activity,
1409                     k -> new ArrayList<>());
1410             list.add(si);
1411         });
1412         return activitiesToShortcuts;
1413     }
1414 
1415     /** Used by {@link #enforceShortcutCountsBeforeOperation} */
incrementCountForActivity(ArrayMap<ComponentName, Integer> counts, ComponentName cn, int increment)1416     private void incrementCountForActivity(ArrayMap<ComponentName, Integer> counts,
1417             ComponentName cn, int increment) {
1418         Integer oldValue = counts.get(cn);
1419         if (oldValue == null) {
1420             oldValue = 0;
1421         }
1422 
1423         counts.put(cn, oldValue + increment);
1424     }
1425 
1426     /**
1427      * Called by
1428      * {@link android.content.pm.ShortcutManager#setDynamicShortcuts},
1429      * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and
1430      * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing
1431      * the operation to make sure the operation wouldn't result in the target activities having
1432      * more than the allowed number of dynamic/manifest shortcuts.
1433      *
1434      * @param newList shortcut list passed to set, add or updateShortcuts().
1435      * @param operation add, set or update.
1436      * @throws IllegalArgumentException if the operation would result in going over the max
1437      *                                  shortcut count for any activity.
1438      */
enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList, @ShortcutOperation int operation)1439     public void enforceShortcutCountsBeforeOperation(List<ShortcutInfo> newList,
1440             @ShortcutOperation int operation) {
1441         final ShortcutService service = mShortcutUser.mService;
1442 
1443         // Current # of dynamic / manifest shortcuts for each activity.
1444         // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced
1445         // anyway.)
1446         final ArrayMap<ComponentName, Integer> counts = new ArrayMap<>(4);
1447         forEachShortcut(shortcut -> {
1448             if (shortcut.isManifestShortcut()) {
1449                 incrementCountForActivity(counts, shortcut.getActivity(), 1);
1450             } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) {
1451                 incrementCountForActivity(counts, shortcut.getActivity(), 1);
1452             }
1453         });
1454 
1455         for (int i = newList.size() - 1; i >= 0; i--) {
1456             final ShortcutInfo newShortcut = newList.get(i);
1457             final ComponentName newActivity = newShortcut.getActivity();
1458             if (newActivity == null) {
1459                 if (operation != ShortcutService.OPERATION_UPDATE) {
1460                     service.wtf("Activity must not be null at this point");
1461                     continue; // Just ignore this invalid case.
1462                 }
1463                 continue; // Activity can be null for update.
1464             }
1465 
1466             final ShortcutInfo original = findShortcutById(newShortcut.getId());
1467             if (original == null) {
1468                 if (operation == ShortcutService.OPERATION_UPDATE) {
1469                     continue; // When updating, ignore if there's no target.
1470                 }
1471                 // Add() or set(), and there's no existing shortcut with the same ID.  We're
1472                 // simply publishing (as opposed to updating) this shortcut, so just +1.
1473                 incrementCountForActivity(counts, newActivity, 1);
1474                 continue;
1475             }
1476             if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) {
1477                 // Updating floating shortcuts doesn't affect the count, so ignore.
1478                 continue;
1479             }
1480 
1481             // If it's add() or update(), then need to decrement for the previous activity.
1482             // Skip it for set() since it's already been taken care of by not counting the original
1483             // dynamic shortcuts in the first loop.
1484             if (operation != ShortcutService.OPERATION_SET) {
1485                 final ComponentName oldActivity = original.getActivity();
1486                 if (!original.isFloating()) {
1487                     incrementCountForActivity(counts, oldActivity, -1);
1488                 }
1489             }
1490             incrementCountForActivity(counts, newActivity, 1);
1491         }
1492 
1493         // Then make sure none of the activities have more than the max number of shortcuts.
1494         for (int i = counts.size() - 1; i >= 0; i--) {
1495             service.enforceMaxActivityShortcuts(counts.valueAt(i));
1496         }
1497     }
1498 
1499     /**
1500      * For all the text fields, refresh the string values if they're from resources.
1501      */
resolveResourceStrings()1502     public void resolveResourceStrings() {
1503         final ShortcutService s = mShortcutUser.mService;
1504 
1505         final Resources publisherRes = getPackageResources();
1506         final List<ShortcutInfo> changedShortcuts = new ArrayList<>(1);
1507 
1508         if (publisherRes != null) {
1509             forEachShortcutMutate(si -> {
1510                 if (!si.hasStringResources()) return;
1511                 si.resolveResourceStrings(publisherRes);
1512                 si.setTimestamp(s.injectCurrentTimeMillis());
1513                 changedShortcuts.add(si);
1514             });
1515         }
1516         if (!CollectionUtils.isEmpty(changedShortcuts)) {
1517             s.packageShortcutsChanged(this, changedShortcuts, null);
1518         }
1519     }
1520 
1521     /** Clears the implicit ranks for all shortcuts. */
clearAllImplicitRanks()1522     public void clearAllImplicitRanks() {
1523         forEachShortcutMutate(ShortcutInfo::clearImplicitRankAndRankChangedFlag);
1524     }
1525 
1526     /**
1527      * Used to sort shortcuts for rank auto-adjusting.
1528      */
1529     final Comparator<ShortcutInfo> mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> {
1530         // First, sort by rank.
1531         int ret = Integer.compare(a.getRank(), b.getRank());
1532         if (ret != 0) {
1533             return ret;
1534         }
1535         // When ranks are tie, then prioritize the ones that have just been assigned new ranks.
1536         // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively,
1537         // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because
1538         // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set.
1539         // Similarly, updating s3's rank to 1 will insert it between s1 and s2.
1540         if (a.isRankChanged() != b.isRankChanged()) {
1541             return a.isRankChanged() ? -1 : 1;
1542         }
1543         // If they're still tie, sort by implicit rank -- i.e. preserve the order in which
1544         // they're passed to the API.
1545         ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank());
1546         if (ret != 0) {
1547             return ret;
1548         }
1549         // If they're still tie, just sort by their IDs.
1550         // This may happen with updateShortcuts() -- see
1551         // the testUpdateShortcuts_noManifestShortcuts() test.
1552         return a.getId().compareTo(b.getId());
1553     };
1554 
1555     /**
1556      * Re-calculate the ranks for all shortcuts.
1557      */
adjustRanks()1558     public void adjustRanks() {
1559         final ShortcutService s = mShortcutUser.mService;
1560         final long now = s.injectCurrentTimeMillis();
1561 
1562         // First, clear ranks for floating shortcuts.
1563         forEachShortcutMutate(si -> {
1564             if (si.isFloating() && si.getRank() != 0) {
1565                 si.setTimestamp(now);
1566                 si.setRank(0);
1567             }
1568         });
1569 
1570         // Then adjust ranks.  Ranks are unique for each activity, so we first need to sort
1571         // shortcuts to each activity.
1572         // Then sort the shortcuts within each activity with mShortcutRankComparator, and
1573         // assign ranks from 0.
1574         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
1575                 sortShortcutsToActivities();
1576         for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity.
1577             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
1578 
1579             // Sort by ranks and other signals.
1580             Collections.sort(list, mShortcutRankComparator);
1581 
1582             int rank = 0;
1583 
1584             final int size = list.size();
1585             for (int i = 0; i < size; i++) {
1586                 final ShortcutInfo si = list.get(i);
1587                 if (si.isManifestShortcut()) {
1588                     // Don't adjust ranks for manifest shortcuts.
1589                     continue;
1590                 }
1591                 // At this point, it must be dynamic.
1592                 if (!si.isDynamic()) {
1593                     s.wtf("Non-dynamic shortcut found. " + si.toInsecureString());
1594                     continue;
1595                 }
1596                 final int thisRank = rank++;
1597                 if (si.getRank() != thisRank) {
1598                     mutateShortcut(si.getId(), si, shortcut -> {
1599                         shortcut.setTimestamp(now);
1600                         shortcut.setRank(thisRank);
1601                     });
1602                 }
1603             }
1604         }
1605     }
1606 
1607     /** @return true if there's any shortcuts that are not manifest shortcuts. */
hasNonManifestShortcuts()1608     public boolean hasNonManifestShortcuts() {
1609         final boolean[] condition = new boolean[1];
1610         forEachShortcutStopWhen(si -> {
1611             if (!si.isDeclaredInManifest()) {
1612                 condition[0] = true;
1613                 return true;
1614             }
1615             return false;
1616         });
1617         return condition[0];
1618     }
1619 
reportShortcutUsed(@onNull final UsageStatsManagerInternal usageStatsManagerInternal, @NonNull final String shortcutId)1620     void reportShortcutUsed(@NonNull final UsageStatsManagerInternal usageStatsManagerInternal,
1621             @NonNull final String shortcutId) {
1622         synchronized (mPackageItemLock) {
1623             final long currentTS = SystemClock.elapsedRealtime();
1624             final ShortcutService s = mShortcutUser.mService;
1625             if (currentTS - mLastReportedTime > s.mSaveDelayMillis) {
1626                 mLastReportedTime = currentTS;
1627             } else {
1628                 return;
1629             }
1630             final long token = s.injectClearCallingIdentity();
1631             try {
1632                 usageStatsManagerInternal.reportShortcutUsage(getPackageName(), shortcutId,
1633                         getPackageUserId());
1634             } finally {
1635                 s.injectRestoreCallingIdentity(token);
1636             }
1637         }
1638     }
1639 
dump(@onNull PrintWriter pw, @NonNull String prefix, DumpFilter filter)1640     public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
1641         pw.println();
1642 
1643         pw.print(prefix);
1644         pw.print("Package: ");
1645         pw.print(getPackageName());
1646         pw.print("  UID: ");
1647         pw.print(mPackageUid);
1648         pw.println();
1649 
1650         pw.print(prefix);
1651         pw.print("  ");
1652         pw.print("Calls: ");
1653         pw.print(getApiCallCount(/*unlimited=*/ false));
1654         pw.println();
1655 
1656         // getApiCallCount() may have updated mLastKnownForegroundElapsedTime.
1657         pw.print(prefix);
1658         pw.print("  ");
1659         pw.print("Last known FG: ");
1660         pw.print(mLastKnownForegroundElapsedTime);
1661         pw.println();
1662 
1663         // This should be after getApiCallCount(), which may update it.
1664         pw.print(prefix);
1665         pw.print("  ");
1666         pw.print("Last reset: [");
1667         pw.print(mLastResetTime);
1668         pw.print("] ");
1669         pw.print(ShortcutService.formatTime(mLastResetTime));
1670         pw.println();
1671 
1672         getPackageInfo().dump(pw, prefix + "  ");
1673         pw.println();
1674 
1675         pw.print(prefix);
1676         pw.println("  Shortcuts:");
1677         final long[] totalBitmapSize = new long[1];
1678         forEachShortcut(si -> {
1679             pw.println(si.toDumpString(prefix + "    "));
1680             if (si.getBitmapPath() != null) {
1681                 final long len = new File(si.getBitmapPath()).length();
1682                 pw.print(prefix);
1683                 pw.print("      ");
1684                 pw.print("bitmap size=");
1685                 pw.println(len);
1686 
1687                 totalBitmapSize[0] += len;
1688             }
1689         });
1690         pw.print(prefix);
1691         pw.print("  ");
1692         pw.print("Total bitmap size: ");
1693         pw.print(totalBitmapSize[0]);
1694         pw.print(" (");
1695         pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize[0]));
1696         pw.println(")");
1697 
1698         pw.println();
1699         synchronized (mPackageItemLock) {
1700             mShortcutBitmapSaver.dumpLocked(pw, "  ");
1701         }
1702     }
1703 
dumpShortcuts(@onNull PrintWriter pw, int matchFlags)1704     public void dumpShortcuts(@NonNull PrintWriter pw, int matchFlags) {
1705         final boolean matchDynamic = (matchFlags & ShortcutManager.FLAG_MATCH_DYNAMIC) != 0;
1706         final boolean matchPinned = (matchFlags & ShortcutManager.FLAG_MATCH_PINNED) != 0;
1707         final boolean matchManifest = (matchFlags & ShortcutManager.FLAG_MATCH_MANIFEST) != 0;
1708         final boolean matchCached = (matchFlags & ShortcutManager.FLAG_MATCH_CACHED) != 0;
1709 
1710         final int shortcutFlags = (matchDynamic ? ShortcutInfo.FLAG_DYNAMIC : 0)
1711                 | (matchPinned ? ShortcutInfo.FLAG_PINNED : 0)
1712                 | (matchManifest ? ShortcutInfo.FLAG_MANIFEST : 0)
1713                 | (matchCached ? ShortcutInfo.FLAG_CACHED_ALL : 0);
1714 
1715         forEachShortcut(si -> {
1716             if ((si.getFlags() & shortcutFlags) != 0) {
1717                 pw.println(si.toDumpString(""));
1718             }
1719         });
1720     }
1721 
1722     @Override
dumpCheckin(boolean clear)1723     public JSONObject dumpCheckin(boolean clear) throws JSONException {
1724         final JSONObject result = super.dumpCheckin(clear);
1725 
1726         final int[] numDynamic = new int[1];
1727         final int[] numPinned = new int[1];
1728         final int[] numManifest = new int[1];
1729         final int[] numBitmaps = new int[1];
1730         final long[] totalBitmapSize = new long[1];
1731 
1732         forEachShortcut(si -> {
1733             if (si.isDynamic()) numDynamic[0]++;
1734             if (si.isDeclaredInManifest()) numManifest[0]++;
1735             if (si.isPinned()) numPinned[0]++;
1736 
1737             if (si.getBitmapPath() != null) {
1738                 numBitmaps[0]++;
1739                 totalBitmapSize[0] += new File(si.getBitmapPath()).length();
1740             }
1741         });
1742 
1743         result.put(KEY_DYNAMIC, numDynamic[0]);
1744         result.put(KEY_MANIFEST, numManifest[0]);
1745         result.put(KEY_PINNED, numPinned[0]);
1746         result.put(KEY_BITMAPS, numBitmaps[0]);
1747         result.put(KEY_BITMAP_BYTES, totalBitmapSize[0]);
1748 
1749         // TODO Log update frequency too.
1750 
1751         return result;
1752     }
1753 
hasNoShortcut()1754     private boolean hasNoShortcut() {
1755         return getShortcutCount() == 0;
1756     }
1757 
1758     @Override
saveToXml(@onNull TypedXmlSerializer out, boolean forBackup)1759     public void saveToXml(@NonNull TypedXmlSerializer out, boolean forBackup)
1760             throws IOException, XmlPullParserException {
1761         synchronized (mPackageItemLock) {
1762             final int size = mShortcuts.size();
1763             final int shareTargetSize = mShareTargets.size();
1764 
1765             if (hasNoShortcut() && shareTargetSize == 0 && mApiCallCount == 0) {
1766                 return; // nothing to write.
1767             }
1768 
1769             out.startTag(null, TAG_ROOT);
1770 
1771             ShortcutService.writeAttr(out, ATTR_NAME, getPackageName());
1772             ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount);
1773             ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime);
1774             getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
1775 
1776             if (ShortcutService.DEBUG_REBOOT) {
1777                 Slog.d(TAG, "Persisting shortcuts from "
1778                     + getOwnerUserId() + "@" + getPackageName());
1779             }
1780             for (int j = 0; j < size; j++) {
1781                 final ShortcutInfo si = mShortcuts.valueAt(j);
1782                 saveShortcut(
1783                         out, si, forBackup, getPackageInfo().isBackupAllowed());
1784                 if (ShortcutService.DEBUG_REBOOT) {
1785                     Slog.d(TAG, si.toSimpleString());
1786                 }
1787             }
1788 
1789             if (!forBackup) {
1790                 for (int j = 0; j < shareTargetSize; j++) {
1791                     mShareTargets.get(j).saveToXml(out);
1792                 }
1793             }
1794 
1795             out.endTag(null, TAG_ROOT);
1796         }
1797     }
1798 
saveShortcut(TypedXmlSerializer out, ShortcutInfo si, boolean forBackup, boolean appSupportsBackup)1799     private void saveShortcut(TypedXmlSerializer out, ShortcutInfo si, boolean forBackup,
1800             boolean appSupportsBackup)
1801             throws IOException, XmlPullParserException {
1802 
1803         final ShortcutService s = mShortcutUser.mService;
1804 
1805         if (forBackup) {
1806             if (!(si.isPinned() && si.isEnabled())) {
1807                 // We only backup pinned shortcuts that are enabled.
1808                 // Note, this means, shortcuts that are restored but are blocked restore, e.g. due
1809                 // to a lower version code, will not be ported to a new device.
1810                 return;
1811             }
1812         }
1813         final boolean shouldBackupDetails =
1814                 !forBackup // It's not backup
1815                 || appSupportsBackup; // Or, it's a backup and app supports backup.
1816 
1817         // Note: at this point no shortcuts should have bitmaps pending save, but if they do,
1818         // just remove the bitmap.
1819         if (si.isIconPendingSave()) {
1820             removeIcon(si);
1821         }
1822         out.startTag(null, TAG_SHORTCUT);
1823         ShortcutService.writeAttr(out, ATTR_ID, si.getId());
1824         // writeAttr(out, "package", si.getPackageName()); // not needed
1825         ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity());
1826         // writeAttr(out, "icon", si.getIcon());  // We don't save it.
1827         ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle());
1828         ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId());
1829         ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName());
1830         ShortcutService.writeAttr(out, ATTR_SPLASH_SCREEN_THEME_NAME, si.getStartingThemeResName());
1831         ShortcutService.writeAttr(out, ATTR_TEXT, si.getText());
1832         ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId());
1833         ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName());
1834         if (shouldBackupDetails) {
1835             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE, si.getDisabledMessage());
1836             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_ID,
1837                     si.getDisabledMessageResourceId());
1838             ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME,
1839                     si.getDisabledMessageResName());
1840         }
1841         ShortcutService.writeAttr(out, ATTR_DISABLED_REASON, si.getDisabledReason());
1842         ShortcutService.writeAttr(out, ATTR_TIMESTAMP,
1843                 si.getLastChangedTimestamp());
1844         final LocusId locusId = si.getLocusId();
1845         if (locusId != null) {
1846             ShortcutService.writeAttr(out, ATTR_LOCUS_ID, si.getLocusId().getId());
1847         }
1848         if (forBackup) {
1849             // Don't write icon information.  Also drop the dynamic flag.
1850 
1851             int flags = si.getFlags() &
1852                     ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
1853                             | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE
1854                             | ShortcutInfo.FLAG_DYNAMIC
1855                             | ShortcutInfo.FLAG_HAS_ICON_URI | ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
1856             ShortcutService.writeAttr(out, ATTR_FLAGS, flags);
1857 
1858             // Set the publisher version code at every backup.
1859             final long packageVersionCode = getPackageInfo().getVersionCode();
1860             if (packageVersionCode == 0) {
1861                 s.wtf("Package version code should be available at this point.");
1862                 // However, 0 is a valid version code, so we just go ahead with it...
1863             }
1864         } else {
1865             // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored
1866             // as dynamic.
1867             ShortcutService.writeAttr(out, ATTR_RANK, si.getRank());
1868 
1869             ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags());
1870             ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId());
1871             ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName());
1872             ShortcutService.writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
1873             ShortcutService.writeAttr(out, ATTR_ICON_URI, si.getIconUri());
1874         }
1875 
1876         if (shouldBackupDetails) {
1877             {
1878                 final Set<String> cat = si.getCategories();
1879                 if (cat != null && cat.size() > 0) {
1880                     out.startTag(null, TAG_CATEGORIES);
1881                     XmlUtils.writeStringArrayXml(cat.toArray(new String[cat.size()]),
1882                             NAME_CATEGORIES, XmlUtils.makeTyped(out));
1883                     out.endTag(null, TAG_CATEGORIES);
1884                 }
1885             }
1886             if (!forBackup) {  // Don't backup the persons field.
1887                 final Person[] persons = si.getPersons();
1888                 if (!ArrayUtils.isEmpty(persons)) {
1889                     for (int i = 0; i < persons.length; i++) {
1890                         final Person p = persons[i];
1891 
1892                         out.startTag(null, TAG_PERSON);
1893                         ShortcutService.writeAttr(out, ATTR_PERSON_NAME, p.getName());
1894                         ShortcutService.writeAttr(out, ATTR_PERSON_URI, p.getUri());
1895                         ShortcutService.writeAttr(out, ATTR_PERSON_KEY, p.getKey());
1896                         ShortcutService.writeAttr(out, ATTR_PERSON_IS_BOT, p.isBot());
1897                         ShortcutService.writeAttr(out, ATTR_PERSON_IS_IMPORTANT, p.isImportant());
1898                         out.endTag(null, TAG_PERSON);
1899                     }
1900                 }
1901             }
1902             final Intent[] intentsNoExtras = si.getIntentsNoExtras();
1903             final PersistableBundle[] intentsExtras = si.getIntentPersistableExtrases();
1904             if (intentsNoExtras != null && intentsExtras != null) {
1905                 final int numIntents = intentsNoExtras.length;
1906                 for (int i = 0; i < numIntents; i++) {
1907                     out.startTag(null, TAG_INTENT);
1908                     ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]);
1909                     ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]);
1910                     out.endTag(null, TAG_INTENT);
1911                 }
1912             }
1913 
1914             ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras());
1915 
1916             final Map<String, Map<String, List<String>>> capabilityBindings =
1917                     si.getCapabilityBindingsInternal();
1918             if (capabilityBindings != null && !capabilityBindings.isEmpty()) {
1919                 XmlUtils.writeMapXml(capabilityBindings, NAME_CAPABILITY, out);
1920             }
1921         }
1922 
1923         out.endTag(null, TAG_SHORTCUT);
1924     }
1925 
loadFromFile(ShortcutService s, ShortcutUser shortcutUser, File path, boolean fromBackup)1926     public static ShortcutPackage loadFromFile(ShortcutService s, ShortcutUser shortcutUser,
1927             File path, boolean fromBackup) {
1928         try (ResilientAtomicFile file = getResilientFile(path)) {
1929             FileInputStream in = null;
1930             try {
1931                 in = file.openRead();
1932                 if (in == null) {
1933                     Slog.d(TAG, "Not found " + path);
1934                     return null;
1935                 }
1936 
1937                 ShortcutPackage ret = null;
1938                 TypedXmlPullParser parser = Xml.resolvePullParser(in);
1939 
1940                 int type;
1941                 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
1942                     if (type != XmlPullParser.START_TAG) {
1943                         continue;
1944                     }
1945                     final int depth = parser.getDepth();
1946 
1947                     final String tag = parser.getName();
1948                     if (ShortcutService.DEBUG_LOAD || ShortcutService.DEBUG_REBOOT) {
1949                         Slog.d(TAG, String.format("depth=%d type=%d name=%s", depth, type, tag));
1950                     }
1951                     if ((depth == 1) && TAG_ROOT.equals(tag)) {
1952                         ret = loadFromXml(s, shortcutUser, parser, fromBackup);
1953                         continue;
1954                     }
1955                     ShortcutService.throwForInvalidTag(depth, tag);
1956                 }
1957                 return ret;
1958             } catch (Exception e) {
1959                 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
1960                 file.failRead(in, e);
1961                 return loadFromFile(s, shortcutUser, path, fromBackup);
1962             }
1963         }
1964     }
1965 
loadFromXml(ShortcutService s, ShortcutUser shortcutUser, TypedXmlPullParser parser, boolean fromBackup)1966     public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
1967             TypedXmlPullParser parser, boolean fromBackup)
1968             throws IOException, XmlPullParserException {
1969 
1970         final String packageName = ShortcutService.parseStringAttribute(parser,
1971                 ATTR_NAME);
1972 
1973         final ShortcutPackage ret = new ShortcutPackage(shortcutUser,
1974                 shortcutUser.getUserId(), packageName);
1975         synchronized (ret.mPackageItemLock) {
1976             ret.mApiCallCount = ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT);
1977             ret.mLastResetTime = ShortcutService.parseLongAttribute(parser, ATTR_LAST_RESET);
1978 
1979             final int outerDepth = parser.getDepth();
1980             int type;
1981             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
1982                     && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
1983                 if (type != XmlPullParser.START_TAG) {
1984                     continue;
1985                 }
1986                 final int depth = parser.getDepth();
1987                 final String tag = parser.getName();
1988                 if (depth == outerDepth + 1) {
1989                     switch (tag) {
1990                         case ShortcutPackageInfo.TAG_ROOT:
1991                             ret.getPackageInfo().loadFromXml(parser, fromBackup);
1992 
1993                             continue;
1994                         case TAG_SHORTCUT:
1995                             try {
1996                                 final ShortcutInfo si = parseShortcut(parser, packageName,
1997                                         shortcutUser.getUserId(), fromBackup);
1998                                 // Don't use addShortcut(), we don't need to save the icon.
1999                                 ret.mShortcuts.put(si.getId(), si);
2000                             } catch (IOException e) {
2001                                 // Don't ignore IO exceptions.
2002                                 throw e;
2003                             } catch (Exception e) {
2004                                 // b/246540168 malformed shortcuts should be ignored
2005                                 Slog.e(TAG, "Failed parsing shortcut.", e);
2006                             }
2007                             continue;
2008                         case TAG_SHARE_TARGET:
2009                             ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
2010                             continue;
2011                     }
2012                 }
2013                 ShortcutService.warnForInvalidTag(depth, tag);
2014             }
2015         }
2016         return ret;
2017     }
2018 
parseShortcut(TypedXmlPullParser parser, String packageName, @UserIdInt int userId, boolean fromBackup)2019     private static ShortcutInfo parseShortcut(TypedXmlPullParser parser, String packageName,
2020             @UserIdInt int userId, boolean fromBackup)
2021             throws IOException, XmlPullParserException {
2022         String id;
2023         ComponentName activityComponent;
2024         // Icon icon;
2025         String title;
2026         int titleResId;
2027         String titleResName;
2028         String text;
2029         int textResId;
2030         String textResName;
2031         String disabledMessage;
2032         int disabledMessageResId;
2033         String disabledMessageResName;
2034         int disabledReason;
2035         Intent intentLegacy;
2036         PersistableBundle intentPersistableExtrasLegacy = null;
2037         ArrayList<Intent> intents = new ArrayList<>();
2038         int rank;
2039         PersistableBundle extras = null;
2040         long lastChangedTimestamp;
2041         int flags;
2042         int iconResId;
2043         String iconResName;
2044         String bitmapPath;
2045         String iconUri;
2046         final String locusIdString;
2047         String splashScreenThemeResName;
2048         int backupVersionCode;
2049         ArraySet<String> categories = null;
2050         ArrayList<Person> persons = new ArrayList<>();
2051         Map<String, Map<String, List<String>>> capabilityBindings = null;
2052 
2053         id = ShortcutService.parseStringAttribute(parser, ATTR_ID);
2054         activityComponent = ShortcutService.parseComponentNameAttribute(parser,
2055                 ATTR_ACTIVITY);
2056         title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE);
2057         titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID);
2058         titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME);
2059         splashScreenThemeResName = ShortcutService.parseStringAttribute(parser,
2060                 ATTR_SPLASH_SCREEN_THEME_NAME);
2061         text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT);
2062         textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID);
2063         textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME);
2064         disabledMessage = ShortcutService.parseStringAttribute(parser, ATTR_DISABLED_MESSAGE);
2065         disabledMessageResId = ShortcutService.parseIntAttribute(parser,
2066                 ATTR_DISABLED_MESSAGE_RES_ID);
2067         disabledMessageResName = ShortcutService.parseStringAttribute(parser,
2068                 ATTR_DISABLED_MESSAGE_RES_NAME);
2069         disabledReason = ShortcutService.parseIntAttribute(parser, ATTR_DISABLED_REASON);
2070         intentLegacy = ShortcutService.parseIntentAttributeNoDefault(parser, ATTR_INTENT_LEGACY);
2071         rank = (int) ShortcutService.parseLongAttribute(parser, ATTR_RANK);
2072         lastChangedTimestamp = ShortcutService.parseLongAttribute(parser, ATTR_TIMESTAMP);
2073         flags = (int) ShortcutService.parseLongAttribute(parser, ATTR_FLAGS);
2074         iconResId = (int) ShortcutService.parseLongAttribute(parser, ATTR_ICON_RES_ID);
2075         iconResName = ShortcutService.parseStringAttribute(parser, ATTR_ICON_RES_NAME);
2076         bitmapPath = ShortcutService.parseStringAttribute(parser, ATTR_BITMAP_PATH);
2077         iconUri = ShortcutService.parseStringAttribute(parser, ATTR_ICON_URI);
2078         locusIdString = ShortcutService.parseStringAttribute(parser, ATTR_LOCUS_ID);
2079 
2080         final int outerDepth = parser.getDepth();
2081         int type;
2082         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2083                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2084             if (type != XmlPullParser.START_TAG) {
2085                 continue;
2086             }
2087             final int depth = parser.getDepth();
2088             final String tag = parser.getName();
2089             if (ShortcutService.DEBUG_LOAD || ShortcutService.DEBUG_REBOOT) {
2090                 Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
2091                         depth, type, tag));
2092             }
2093             switch (tag) {
2094                 case TAG_INTENT_EXTRAS_LEGACY:
2095                     intentPersistableExtrasLegacy = PersistableBundle.restoreFromXml(parser);
2096                     continue;
2097                 case TAG_INTENT:
2098                     intents.add(parseIntent(parser));
2099                     continue;
2100                 case TAG_EXTRAS:
2101                     extras = PersistableBundle.restoreFromXml(parser);
2102                     continue;
2103                 case TAG_CATEGORIES:
2104                     // This just contains string-array.
2105                     continue;
2106                 case TAG_PERSON:
2107                     persons.add(parsePerson(parser));
2108                     continue;
2109                 case TAG_STRING_ARRAY_XMLUTILS:
2110                     if (NAME_CATEGORIES.equals(ShortcutService.parseStringAttribute(parser,
2111                             ATTR_NAME_XMLUTILS))) {
2112                         final String[] ar = XmlUtils.readThisStringArrayXml(
2113                                 XmlUtils.makeTyped(parser), TAG_STRING_ARRAY_XMLUTILS, null);
2114                         categories = new ArraySet<>(ar.length);
2115                         for (int i = 0; i < ar.length; i++) {
2116                             categories.add(ar[i]);
2117                         }
2118                     }
2119                     continue;
2120                 case TAG_MAP_XMLUTILS:
2121                     if (NAME_CAPABILITY.equals(ShortcutService.parseStringAttribute(parser,
2122                             ATTR_NAME_XMLUTILS))) {
2123                         capabilityBindings = (Map<String, Map<String, List<String>>>)
2124                                 XmlUtils.readValueXml(parser, new String[1]);
2125                     }
2126                     continue;
2127             }
2128             throw ShortcutService.throwForInvalidTag(depth, tag);
2129         }
2130 
2131         if (intentLegacy != null) {
2132             // For the legacy file format which supported only one intent per shortcut.
2133             ShortcutInfo.setIntentExtras(intentLegacy, intentPersistableExtrasLegacy);
2134             intents.clear();
2135             intents.add(intentLegacy);
2136         }
2137 
2138 
2139         if ((disabledReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)
2140                 && ((flags & ShortcutInfo.FLAG_DISABLED) != 0)) {
2141             // We didn't used to have the disabled reason, so if a shortcut is disabled
2142             // and has no reason, we assume it was disabled by publisher.
2143             disabledReason = ShortcutInfo.DISABLED_REASON_BY_APP;
2144         }
2145 
2146         // All restored shortcuts are initially "shadow".
2147         if (fromBackup) {
2148             flags |= ShortcutInfo.FLAG_SHADOW;
2149         }
2150 
2151         final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString);
2152 
2153         return new ShortcutInfo(
2154                 userId, id, packageName, activityComponent, /* icon= */ null,
2155                 title, titleResId, titleResName, text, textResId, textResName,
2156                 disabledMessage, disabledMessageResId, disabledMessageResName,
2157                 categories,
2158                 intents.toArray(new Intent[intents.size()]),
2159                 rank, extras, lastChangedTimestamp, flags,
2160                 iconResId, iconResName, bitmapPath, iconUri,
2161                 disabledReason, persons.toArray(new Person[persons.size()]), locusId,
2162                 splashScreenThemeResName, capabilityBindings);
2163     }
2164 
parseIntent(TypedXmlPullParser parser)2165     private static Intent parseIntent(TypedXmlPullParser parser)
2166             throws IOException, XmlPullParserException {
2167 
2168         Intent intent = ShortcutService.parseIntentAttribute(parser,
2169                 ATTR_INTENT_NO_EXTRA);
2170 
2171         final int outerDepth = parser.getDepth();
2172         int type;
2173         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2174                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2175             if (type != XmlPullParser.START_TAG) {
2176                 continue;
2177             }
2178             final int depth = parser.getDepth();
2179             final String tag = parser.getName();
2180             if (ShortcutService.DEBUG_LOAD || ShortcutService.DEBUG_REBOOT) {
2181                 Slog.d(TAG, String.format("  depth=%d type=%d name=%s",
2182                         depth, type, tag));
2183             }
2184             switch (tag) {
2185                 case TAG_EXTRAS:
2186                     ShortcutInfo.setIntentExtras(intent,
2187                             PersistableBundle.restoreFromXml(parser));
2188                     continue;
2189             }
2190             throw ShortcutService.throwForInvalidTag(depth, tag);
2191         }
2192         return intent;
2193     }
2194 
parsePerson(TypedXmlPullParser parser)2195     private static Person parsePerson(TypedXmlPullParser parser)
2196             throws IOException, XmlPullParserException {
2197         CharSequence name = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_NAME);
2198         String uri = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_URI);
2199         String key = ShortcutService.parseStringAttribute(parser, ATTR_PERSON_KEY);
2200         boolean isBot = ShortcutService.parseBooleanAttribute(parser, ATTR_PERSON_IS_BOT);
2201         boolean isImportant = ShortcutService.parseBooleanAttribute(parser,
2202                 ATTR_PERSON_IS_IMPORTANT);
2203 
2204         Person.Builder builder = new Person.Builder();
2205         builder.setName(name).setUri(uri).setKey(key).setBot(isBot).setImportant(isImportant);
2206         return builder.build();
2207     }
2208 
2209     @VisibleForTesting
getAllShortcutsForTest()2210     List<ShortcutInfo> getAllShortcutsForTest() {
2211         final List<ShortcutInfo> ret = new ArrayList<>(1);
2212         forEachShortcut(ret::add);
2213         return ret;
2214     }
2215 
2216     @VisibleForTesting
getAllShareTargetsForTest()2217     List<ShareTargetInfo> getAllShareTargetsForTest() {
2218         synchronized (mPackageItemLock) {
2219             return new ArrayList<>(mShareTargets);
2220         }
2221     }
2222 
2223     @Override
verifyStates()2224     public void verifyStates() {
2225         super.verifyStates();
2226 
2227         final boolean[] failed = new boolean[1];
2228 
2229         final ShortcutService s = mShortcutUser.mService;
2230 
2231         final ArrayMap<ComponentName, ArrayList<ShortcutInfo>> all =
2232                 sortShortcutsToActivities();
2233 
2234         // Make sure each activity won't have more than max shortcuts.
2235         for (int outer = all.size() - 1; outer >= 0; outer--) {
2236             final ArrayList<ShortcutInfo> list = all.valueAt(outer);
2237             if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) {
2238                 failed[0] = true;
2239                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer)
2240                         + " has " + all.valueAt(outer).size() + " shortcuts.");
2241             }
2242 
2243             // Sort by rank.
2244             Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank()));
2245 
2246             // Split into two arrays for each kind.
2247             final ArrayList<ShortcutInfo> dynamicList = new ArrayList<>(list);
2248             dynamicList.removeIf((si) -> !si.isDynamic());
2249 
2250             final ArrayList<ShortcutInfo> manifestList = new ArrayList<>(list);
2251             manifestList.removeIf((si) -> !si.isManifestShortcut());
2252 
2253             verifyRanksSequential(dynamicList);
2254             verifyRanksSequential(manifestList);
2255         }
2256 
2257         // Verify each shortcut's status.
2258         forEachShortcut(si -> {
2259             if (!(si.isDeclaredInManifest() || si.isDynamic() || si.isPinned() || si.isCached())) {
2260                 failed[0] = true;
2261                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2262                         + " is not manifest, dynamic or pinned.");
2263             }
2264             if (si.isDeclaredInManifest() && si.isDynamic()) {
2265                 failed[0] = true;
2266                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2267                         + " is both dynamic and manifest at the same time.");
2268             }
2269             if (si.getActivity() == null && !si.isFloating()) {
2270                 failed[0] = true;
2271                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2272                         + " has null activity, but not floating.");
2273             }
2274             if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) {
2275                 failed[0] = true;
2276                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2277                         + " is not floating, but is disabled.");
2278             }
2279             if (si.isFloating() && si.getRank() != 0) {
2280                 failed[0] = true;
2281                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2282                         + " is floating, but has rank=" + si.getRank());
2283             }
2284             if (si.getIcon() != null) {
2285                 failed[0] = true;
2286                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2287                         + " still has an icon");
2288             }
2289             if (si.hasAdaptiveBitmap() && !(si.hasIconFile() || si.hasIconUri())) {
2290                 failed[0] = true;
2291                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2292                         + " has adaptive bitmap but was not saved to a file nor has icon uri.");
2293             }
2294             if (si.hasIconFile() && si.hasIconResource()) {
2295                 failed[0] = true;
2296                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2297                         + " has both resource and bitmap icons");
2298             }
2299             if (si.hasIconFile() && si.hasIconUri()) {
2300                 failed[0] = true;
2301                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2302                         + " has both url and bitmap icons");
2303             }
2304             if (si.hasIconUri() && si.hasIconResource()) {
2305                 failed[0] = true;
2306                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2307                         + " has both url and resource icons");
2308             }
2309             if (si.isEnabled()
2310                     != (si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_NOT_DISABLED)) {
2311                 failed[0] = true;
2312                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2313                         + " isEnabled() and getDisabledReason() disagree: "
2314                         + si.isEnabled() + " vs " + si.getDisabledReason());
2315             }
2316             if ((si.getDisabledReason() == ShortcutInfo.DISABLED_REASON_VERSION_LOWER)
2317                     && (getPackageInfo().getBackupSourceVersionCode()
2318                     == ShortcutInfo.VERSION_CODE_UNKNOWN)) {
2319                 failed[0] = true;
2320                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2321                         + " RESTORED_VERSION_LOWER with no backup source version code.");
2322             }
2323             if (s.isDummyMainActivity(si.getActivity())) {
2324                 failed[0] = true;
2325                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2326                         + " has a dummy target activity");
2327             }
2328         });
2329 
2330         if (failed[0]) {
2331             throw new IllegalStateException("See logcat for errors");
2332         }
2333     }
2334 
mutateShortcut(@onNull final String id, @Nullable final ShortcutInfo shortcut, @NonNull final Consumer<ShortcutInfo> transform)2335     void mutateShortcut(@NonNull final String id, @Nullable final ShortcutInfo shortcut,
2336             @NonNull final Consumer<ShortcutInfo> transform) {
2337         Objects.requireNonNull(id);
2338         Objects.requireNonNull(transform);
2339         synchronized (mPackageItemLock) {
2340             if (shortcut != null) {
2341                 transform.accept(shortcut);
2342             }
2343             final ShortcutInfo si = findShortcutById(id);
2344             if (si == null) {
2345                 return;
2346             }
2347             transform.accept(si);
2348             saveShortcut(si);
2349         }
2350     }
2351 
saveShortcut(@onNull final ShortcutInfo... shortcuts)2352     private void saveShortcut(@NonNull final ShortcutInfo... shortcuts) {
2353         Objects.requireNonNull(shortcuts);
2354         saveShortcut(Arrays.asList(shortcuts));
2355     }
2356 
saveShortcut(@onNull final Collection<ShortcutInfo> shortcuts)2357     private void saveShortcut(@NonNull final Collection<ShortcutInfo> shortcuts) {
2358         Objects.requireNonNull(shortcuts);
2359         synchronized (mPackageItemLock) {
2360             for (ShortcutInfo si : shortcuts) {
2361                 mShortcuts.put(si.getId(), si);
2362             }
2363         }
2364     }
2365 
2366     @Nullable
findAll(@onNull final Collection<String> ids)2367     List<ShortcutInfo> findAll(@NonNull final Collection<String> ids) {
2368         synchronized (mPackageItemLock) {
2369             return ids.stream().map(mShortcuts::get)
2370                     .filter(Objects::nonNull).collect(Collectors.toList());
2371         }
2372     }
2373 
forEachShortcut(@onNull final Consumer<ShortcutInfo> cb)2374     private void forEachShortcut(@NonNull final Consumer<ShortcutInfo> cb) {
2375         forEachShortcutStopWhen(si -> {
2376             cb.accept(si);
2377             return false;
2378         });
2379     }
2380 
forEachShortcutMutate(@onNull final Consumer<ShortcutInfo> cb)2381     private void forEachShortcutMutate(@NonNull final Consumer<ShortcutInfo> cb) {
2382         for (int i = mShortcuts.size() - 1; i >= 0; i--) {
2383             ShortcutInfo si = mShortcuts.valueAt(i);
2384             cb.accept(si);
2385         }
2386     }
2387 
forEachShortcutStopWhen( @onNull final Function<ShortcutInfo, Boolean> cb)2388     private void forEachShortcutStopWhen(
2389             @NonNull final Function<ShortcutInfo, Boolean> cb) {
2390         synchronized (mPackageItemLock) {
2391             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
2392                 final ShortcutInfo si = mShortcuts.valueAt(i);
2393                 if (cb.apply(si)) {
2394                     return;
2395                 }
2396             }
2397         }
2398     }
2399 
verifyRanksSequential(List<ShortcutInfo> list)2400     private boolean verifyRanksSequential(List<ShortcutInfo> list) {
2401         boolean failed = false;
2402 
2403         for (int i = 0; i < list.size(); i++) {
2404             final ShortcutInfo si = list.get(i);
2405             if (si.getRank() != i) {
2406                 failed = true;
2407                 Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId()
2408                         + " rank=" + si.getRank() + " but expected to be " + i);
2409             }
2410         }
2411         return failed;
2412     }
2413 
runAsSystem(@onNull final Runnable fn)2414     private void runAsSystem(@NonNull final Runnable fn) {
2415         final long callingIdentity = Binder.clearCallingIdentity();
2416         try {
2417             fn.run();
2418         } finally {
2419             Binder.restoreCallingIdentity(callingIdentity);
2420         }
2421     }
2422 
2423     @Override
getShortcutPackageItemFile()2424     protected File getShortcutPackageItemFile() {
2425         final File path = new File(mShortcutUser.mService.injectUserDataPath(
2426                 mShortcutUser.getUserId()), ShortcutUser.DIRECTORY_PACKAGES);
2427         final String fileName = getPackageName() + ".xml";
2428         return new File(path, fileName);
2429     }
2430 }
2431