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