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