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