diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index b4bd086af2726..42c88b3eee8ae 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -22,8 +22,6 @@ import android.app.Person; import android.app.appsearch.AppSearchManager; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSession; -import android.app.appsearch.GenericDocument; -import android.app.appsearch.GetByDocumentIdRequest; import android.app.appsearch.PackageIdentifier; import android.app.appsearch.PutDocumentsRequest; import android.app.appsearch.RemoveByDocumentIdRequest; @@ -56,11 +54,12 @@ import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; +import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; -import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import com.android.server.pm.ShortcutService.DumpFilter; @@ -88,7 +87,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -160,6 +159,8 @@ class ShortcutPackage extends ShortcutPackageItem { private final Object mLock = new Object(); + private final Executor mExecutor; + /** * An temp in-memory copy of shortcuts for this package that was loaded from xml, keyed on IDs. */ @@ -189,11 +190,8 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; - private boolean mIsInitilized; - - private boolean mRescanRequired; - private boolean mIsNewApp; - private List mManifestShortcuts; + @GuardedBy("mLock") + private boolean mIsAppSearchSchemaUpToDate; private ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi) { @@ -201,6 +199,7 @@ class ShortcutPackage extends ShortcutPackageItem { spi != null ? spi : ShortcutPackageInfo.newEmpty()); mPackageUid = shortcutUser.mService.injectGetPackageUid(packageName, packageUserId); + mExecutor = BackgroundThread.getExecutor(); } public ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName) { @@ -245,11 +244,11 @@ class ShortcutPackage extends ShortcutPackageItem { final String query = String.format("%s:-%s AND %s:%s", AppSearchShortcutInfo.KEY_FLAGS, ShortcutInfo.FLAG_SHADOW, AppSearchShortcutInfo.KEY_DISABLED_REASON, restoreBlockReason); - forEachShortcutMutateIf(query, si -> { + forEachShortcutMutate(si -> { if (restoreBlockReason == ShortcutInfo.DISABLED_REASON_NOT_DISABLED && !si.hasFlags(ShortcutInfo.FLAG_SHADOW) && si.getDisabledReason() == restoreBlockReason) { - return false; + return; } si.clearFlags(ShortcutInfo.FLAG_SHADOW); @@ -257,7 +256,6 @@ class ShortcutPackage extends ShortcutPackageItem { if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) { si.addFlags(ShortcutInfo.FLAG_DISABLED); } - return true; }); // Because some launchers may not have been restored (e.g. allowBackup=false), // we need to re-calculate the pinned shortcuts. @@ -270,8 +268,7 @@ class ShortcutPackage extends ShortcutPackageItem { @Nullable public ShortcutInfo findShortcutById(@Nullable final String id) { if (id == null) return null; - final List ret = getShortcutById(Collections.singleton(id)); - return (ret == null || ret.isEmpty()) ? null : ret.get(0); + return mShortcuts.get(id); } public boolean isShortcutExistsAndInvisibleToPublisher(String id) { @@ -337,9 +334,8 @@ class ShortcutPackage extends ShortcutPackageItem { * Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible. */ private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) { - final ShortcutInfo shortcut = findShortcutById(id); + final ShortcutInfo shortcut = mShortcuts.remove(id); if (shortcut != null) { - removeShortcut(id); mShortcutUser.mService.removeIconLocked(shortcut); shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED_ALL); @@ -435,7 +431,8 @@ class ShortcutPackage extends ShortcutPackageItem { } changedShortcuts.add(shortcut); - deleted = deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true) != null; + deleted = deleteDynamicWithId(shortcut.getId(), /* ignoreInvisible =*/ true, + /*ignorePersistedShortcuts=*/ true) != null; } } else { // It's an update case. @@ -449,16 +446,14 @@ class ShortcutPackage extends ShortcutPackageItem { forceReplaceShortcutInner(newShortcut); if (isAppSearchEnabled()) { - mShortcutUser.mService.injectPostToHandler(() -> awaitInAppSearch("reportUsage", - session -> { - final AndroidFuture future = new AndroidFuture<>(); - session.reportUsage( - new ReportUsageRequest.Builder( - getPackageName(), newShortcut.getId()).build(), - mShortcutUser.mExecutor, - result -> future.complete(result.isSuccess())); - return future; - })); + runAsSystem(() -> fromAppSearch().thenAccept(session -> + session.reportUsage(new ReportUsageRequest.Builder( + getPackageName(), newShortcut.getId()).build(), mExecutor, result -> { + if (!result.isSuccess()) { + Slog.e(TAG, "Failed to report usage via AppSearch. " + + result.getErrorMessage()); + } + }))); } return deleted; } @@ -470,12 +465,7 @@ class ShortcutPackage extends ShortcutPackageItem { */ private List removeOrphans() { final List removeList = new ArrayList<>(1); - final String query = String.format("%s OR %s OR %s OR %s", - AppSearchShortcutInfo.QUERY_IS_PINNED, - AppSearchShortcutInfo.QUERY_IS_DYNAMIC, - AppSearchShortcutInfo.QUERY_IS_MANIFEST, - AppSearchShortcutInfo.QUERY_IS_CACHED); - forEachShortcut(query, si -> { + forEachShortcut(si -> { if (si.isAlive()) return; removeList.add(si); }); @@ -484,7 +474,6 @@ class ShortcutPackage extends ShortcutPackageItem { forceDeleteShortcutInner(removeList.get(i).getId()); } } - return removeList; } @@ -493,29 +482,21 @@ class ShortcutPackage extends ShortcutPackageItem { * * @return List of shortcuts that actually got removed. */ - public List deleteAllDynamicShortcuts(boolean ignoreInvisible) { + public List deleteAllDynamicShortcuts() { final long now = mShortcutUser.mService.injectCurrentTimeMillis(); - final String query; - if (!ignoreInvisible) { - query = AppSearchShortcutInfo.QUERY_IS_DYNAMIC; - } else { - query = String.format("%s %s", - AppSearchShortcutInfo.QUERY_IS_DYNAMIC, - AppSearchShortcutInfo.QUERY_IS_VISIBLE_TO_PUBLISHER); - } - final boolean[] changed = new boolean[1]; - forEachShortcutMutateIf(query, si -> { - if (si.isDynamic() && (!ignoreInvisible || si.isVisibleToPublisher())) { - changed[0] = true; + boolean changed = false; + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + ShortcutInfo si = mShortcuts.valueAt(i); + if (si.isDynamic() && si.isVisibleToPublisher()) { + changed = true; si.setTimestamp(now); si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); si.setRank(0); // It may still be pinned, so clear the rank. - return true; } - return false; - }); - if (changed[0]) { + } + removeAllShortcutsAsync(); + if (changed) { return removeOrphans(); } return null; @@ -528,10 +509,11 @@ class ShortcutPackage extends ShortcutPackageItem { * @return The deleted shortcut, or null if it was not actually removed because it is either * pinned or cached. */ - public ShortcutInfo deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) { + public ShortcutInfo deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible, + boolean ignorePersistedShortcuts) { return deleteOrDisableWithId( shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible, - ShortcutInfo.DISABLED_REASON_NOT_DISABLED); + ShortcutInfo.DISABLED_REASON_NOT_DISABLED, ignorePersistedShortcuts); } /** @@ -542,9 +524,9 @@ class ShortcutPackage extends ShortcutPackageItem { * it's still pinned. */ private ShortcutInfo disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible, - int disabledReason) { + int disabledReason, boolean ignorePersistedShortcuts) { return deleteOrDisableWithId(shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, - ignoreInvisible, disabledReason); + ignoreInvisible, disabledReason, ignorePersistedShortcuts); } /** @@ -560,7 +542,7 @@ class ShortcutPackage extends ShortcutPackageItem { } return deleteOrDisableWithId( shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible, - ShortcutInfo.DISABLED_REASON_NOT_DISABLED); + ShortcutInfo.DISABLED_REASON_NOT_DISABLED, /*ignorePersistedShortcuts=*/ false); } /** @@ -574,7 +556,8 @@ class ShortcutPackage extends ShortcutPackageItem { int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) { final ShortcutInfo deleted = deleteOrDisableWithId(shortcutId, /* disable =*/ true, - overrideImmutable, ignoreInvisible, disabledReason); + overrideImmutable, ignoreInvisible, disabledReason, + /*ignorePersistedShortcuts=*/ false); // If disabled id still exists, it is pinned and we need to update the disabled message. mutateShortcut(shortcutId, null, disabled -> { @@ -593,7 +576,8 @@ class ShortcutPackage extends ShortcutPackageItem { @Nullable private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable, - boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) { + boolean overrideImmutable, boolean ignoreInvisible, int disabledReason, + boolean ignorePersistedShortcuts) { Preconditions.checkState( (disable == (disabledReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED)), "disable and disabledReason disagree: " + disable + " vs " + disabledReason); @@ -606,8 +590,10 @@ class ShortcutPackage extends ShortcutPackageItem { if (!overrideImmutable) { ensureNotImmutable(oldShortcut, /*ignoreInvisible=*/ true); } + if (!ignorePersistedShortcuts) { + removeShortcutAsync(shortcutId); + } if (oldShortcut.isPinned() || oldShortcut.isCached()) { - mutateShortcut(oldShortcut.getId(), oldShortcut, si -> { si.setRank(0); si.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); @@ -672,21 +658,19 @@ class ShortcutPackage extends ShortcutPackageItem { pinnedShortcuts.addAll(pinned); }); // Then, update the pinned state if necessary. - final List pinned = getShortcutById(pinnedShortcuts); + final List pinned = findAll(pinnedShortcuts); if (pinned != null) { pinned.forEach(si -> { if (!si.isPinned()) { si.addFlags(ShortcutInfo.FLAG_PINNED); } }); - saveShortcut(pinned); } - forEachShortcutMutateIf(AppSearchShortcutInfo.QUERY_IS_PINNED, si -> { + forEachShortcutMutate(si -> { if (!pinnedShortcuts.contains(si.getId()) && si.isPinned()) { si.clearFlags(ShortcutInfo.FLAG_PINNED); - return true; + return; } - return false; }); // Lastly, remove the ones that are no longer pinned, cached nor dynamic. @@ -777,9 +761,9 @@ class ShortcutPackage extends ShortcutPackageItem { /** * Find all shortcuts that match {@code query}. */ - public void findAll(@NonNull List result, @Nullable String query, + public void findAll(@NonNull List result, @Nullable Predicate filter, int cloneFlag) { - findAll(result, query, filter, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false); + findAll(result, filter, cloneFlag, null, 0, /*getPinnedByAnyLauncher=*/ false); } /** @@ -790,7 +774,7 @@ class ShortcutPackage extends ShortcutPackageItem { * adjusted for the caller too. */ public void findAll(@NonNull List result, - @Nullable String query, @Nullable Predicate filter, int cloneFlag, + @Nullable Predicate filter, int cloneFlag, @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) { if (getPackageInfo().isShadow()) { // Restored and the app not installed yet, so don't return any. @@ -802,9 +786,8 @@ class ShortcutPackage extends ShortcutPackageItem { final ArraySet pinnedByCallerSet = (callingLauncher == null) ? null : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) .getPinnedShortcutIds(getPackageName(), getPackageUserId()); - forEachShortcut(query == null ? "" : query, si -> - filter(result, filter, cloneFlag, callingLauncher, pinnedByCallerSet, - getPinnedByAnyLauncher, si)); + forEachShortcut(si -> filter(result, filter, cloneFlag, callingLauncher, pinnedByCallerSet, + getPinnedByAnyLauncher, si)); } /** @@ -837,35 +820,12 @@ class ShortcutPackage extends ShortcutPackageItem { final ArraySet pinnedByCallerSet = (callingLauncher == null) ? null : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) .getPinnedShortcutIds(getPackageName(), getPackageUserId()); - final List shortcuts = getShortcutById(ids); - if (shortcuts != null) { - for (ShortcutInfo si : shortcuts) { - filter(result, query, cloneFlag, callingLauncher, pinnedByCallerSet, - getPinnedByAnyLauncher, si); - } + for (ShortcutInfo si : mShortcuts.values()) { + filter(result, query, cloneFlag, callingLauncher, pinnedByCallerSet, + getPinnedByAnyLauncher, si); } } - /** - * Find all pinned shortcuts that match {@code query}. - */ - public void findAllPinned(@NonNull List result, - @Nullable Predicate query, int cloneFlag, - @Nullable String callingLauncher, int launcherUserId, boolean getPinnedByAnyLauncher) { - if (getPackageInfo().isShadow()) { - // Restored and the app not installed yet, so don't return any. - return; - } - final ShortcutService s = mShortcutUser.mService; - - // Set of pinned shortcuts by the calling launcher. - final ArraySet pinnedByCallerSet = (callingLauncher == null) ? null - : s.getLauncherShortcutsLocked(callingLauncher, getPackageUserId(), launcherUserId) - .getPinnedShortcutIds(getPackageName(), getPackageUserId()); - mShortcuts.values().forEach(si -> filter(result, query, cloneFlag, callingLauncher, - pinnedByCallerSet, getPinnedByAnyLauncher, si)); - } - private void filter(@NonNull final List result, @Nullable final Predicate query, final int cloneFlag, @Nullable final String callingLauncher, @@ -930,8 +890,8 @@ class ShortcutPackage extends ShortcutPackageItem { // Get the list of all dynamic shortcuts in this package. final ArrayList shortcuts = new ArrayList<>(); - findAll(shortcuts, AppSearchShortcutInfo.QUERY_IS_NON_MANIFEST_VISIBLE, - ShortcutInfo::isNonManifestVisible, ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION); + findAll(shortcuts, ShortcutInfo::isNonManifestVisible, + ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION); final List result = new ArrayList<>(); for (int i = 0; i < shortcuts.size(); i++) { @@ -975,8 +935,8 @@ class ShortcutPackage extends ShortcutPackageItem { // Get the list of all dynamic shortcuts in this package final ArrayList shortcuts = new ArrayList<>(); - findAll(shortcuts, AppSearchShortcutInfo.QUERY_IS_NON_MANIFEST_VISIBLE, - ShortcutInfo::isNonManifestVisible, ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); + findAll(shortcuts, ShortcutInfo::isNonManifestVisible, + ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); int sharingShortcutCount = 0; for (int i = 0; i < shortcuts.size(); i++) { @@ -1129,53 +1089,37 @@ class ShortcutPackage extends ShortcutPackageItem { getPackageInfo().getVersionCode(), pi.getLongVersionCode())); } getPackageInfo().updateFromPackageInfo(pi); - if (isAppSearchEnabled()) { - // Save the states in memory and resume package rescan when needed - mRescanRequired = true; - mIsNewApp = isNewApp; - mManifestShortcuts = newManifestShortcutList; - } else { - rescanPackage(isNewApp, newManifestShortcutList); - } - return true; // true means changed. - } - - private void rescanPackage( - final boolean isNewApp, @NonNull final List newManifestShortcutList) { - final ShortcutService s = mShortcutUser.mService; final long newVersionCode = getPackageInfo().getVersionCode(); // See if there are any shortcuts that were prevented restoring because the app was of a // lower version, and re-enable them. { - forEachShortcutMutateIf( - AppSearchShortcutInfo.QUERY_DISABLED_REASON_VERSION_LOWER, si -> { - if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { - return false; + forEachShortcutMutate(si -> { + if (si.getDisabledReason() != ShortcutInfo.DISABLED_REASON_VERSION_LOWER) { + return; + } + if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) { + if (ShortcutService.DEBUG) { + Slog.d(TAG, + String.format( + "Shortcut %s require version %s, still not restored.", + si.getId(), + getPackageInfo().getBackupSourceVersionCode())); } - if (getPackageInfo().getBackupSourceVersionCode() > newVersionCode) { - if (ShortcutService.DEBUG) { - Slog.d(TAG, - String.format( - "Shortcut %s require version %s, still not restored.", - si.getId(), - getPackageInfo().getBackupSourceVersionCode())); - } - return false; - } - Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId())); - si.clearFlags(ShortcutInfo.FLAG_DISABLED); - si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED); - return true; - }); + return; + } + Slog.i(TAG, String.format("Restoring shortcut: %s", si.getId())); + si.clearFlags(ShortcutInfo.FLAG_DISABLED); + si.setDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED); + }); } // For existing shortcuts, update timestamps if they have any resources. // Also check if shortcuts' activities are still main activities. Otherwise, disable them. if (!isNewApp) { final Resources publisherRes = getPackageResources(); - forEachShortcutMutateIf(si -> { + forEachShortcutMutate(si -> { // Disable dynamic shortcuts whose target activity is gone. if (si.isDynamic()) { if (si.getActivity() == null) { @@ -1187,15 +1131,16 @@ class ShortcutPackage extends ShortcutPackageItem { "%s is no longer main activity. Disabling shorcut %s.", getPackageName(), si.getId())); if (disableDynamicWithId(si.getId(), /*ignoreInvisible*/ false, - ShortcutInfo.DISABLED_REASON_APP_CHANGED) != null) { - return false; // Actually removed. + ShortcutInfo.DISABLED_REASON_APP_CHANGED, + /*ignorePersistedShortcuts*/ false) != null) { + return; } // Still pinned, so fall-through and possibly update the resources. } } if (!si.hasAnyResources() || publisherRes == null) { - return false; + return; } if (!si.isOriginallyFromManifest()) { @@ -1206,7 +1151,6 @@ class ShortcutPackage extends ShortcutPackageItem { // from resource names. (We don't allow resource strings for // non-manifest at the moment, but icons can still be resources.) si.setTimestamp(s.injectCurrentTimeMillis()); - return true; }); } @@ -1222,7 +1166,8 @@ class ShortcutPackage extends ShortcutPackageItem { // This will send a notification to the launcher, and also save . // TODO: List changed and removed manifest shortcuts and pass to packageShortcutsChanged() s.packageShortcutsChanged(getPackageName(), getPackageUserId(), null, null); - mManifestShortcuts = null; + + return true; } private boolean publishManifestShortcuts(List newManifestShortcutList) { @@ -1297,7 +1242,8 @@ class ShortcutPackage extends ShortcutPackageItem { final String id = toDisableList.valueAt(i); - disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0, + disableWithId(id, /* disable message =*/ null, + /* disable message resid */ 0, /* overrideImmutable=*/ true, /*ignoreInvisible=*/ false, ShortcutInfo.DISABLED_REASON_APP_CHANGED); } @@ -1338,7 +1284,8 @@ class ShortcutPackage extends ShortcutPackageItem { service.wtf("Found manifest shortcuts in excess list."); continue; } - deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true); + deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true, + /*ignorePersistedShortcuts=*/ true); } } @@ -1494,12 +1441,11 @@ class ShortcutPackage extends ShortcutPackageItem { final List changedShortcuts = new ArrayList<>(1); if (publisherRes != null) { - forEachShortcutMutateIf(AppSearchShortcutInfo.QUERY_HAS_STRING_RESOURCE, si -> { - if (!si.hasStringResources()) return false; + forEachShortcutMutate(si -> { + if (!si.hasStringResources()) return; si.resolveResourceStrings(publisherRes); si.setTimestamp(s.injectCurrentTimeMillis()); changedShortcuts.add(si); - return true; }); } if (!CollectionUtils.isEmpty(changedShortcuts)) { @@ -1549,13 +1495,11 @@ class ShortcutPackage extends ShortcutPackageItem { final long now = s.injectCurrentTimeMillis(); // First, clear ranks for floating shortcuts. - forEachShortcutMutateIf(AppSearchShortcutInfo.QUERY_IS_FLOATING_AND_HAS_RANK, si -> { + forEachShortcutMutate(si -> { if (si.isFloating() && si.getRank() != 0) { si.setTimestamp(now); si.setRank(0); - return true; } - return false; }); // Then adjust ranks. Ranks are unique for each activity, so we first need to sort @@ -1581,7 +1525,7 @@ class ShortcutPackage extends ShortcutPackageItem { } // At this point, it must be dynamic. if (!si.isDynamic()) { - s.wtf("Non-dynamic shortcut found."); + s.wtf("Non-dynamic shortcut found. " + si.toInsecureString()); continue; } final int thisRank = rank++; @@ -1745,13 +1689,10 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount); ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime); if (!forBackup) { - /** - * Schema version should not be included in the backup because: - * 1. Schemas in AppSearch are created from scratch on new device - * 2. Shortcuts are restored from xml file (as opposed to from AppSearch) on new device - */ - ShortcutService.writeAttr(out, ATTR_SCHEMA_VERSON, (mIsInitilized) - ? AppSearchShortcutInfo.SCHEMA_VERSION : 0); + synchronized (mLock) { + ShortcutService.writeAttr(out, ATTR_SCHEMA_VERSON, (mIsAppSearchSchemaUpToDate) + ? AppSearchShortcutInfo.SCHEMA_VERSION : 0); + } } getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup); @@ -1763,6 +1704,8 @@ class ShortcutPackage extends ShortcutPackageItem { for (int j = 0; j < shareTargetSize; j++) { mShareTargets.get(j).saveToXml(out); } + saveShortcutsAsync(mShortcuts.values().stream().filter(ShortcutInfo::usesQuota) + .collect(Collectors.toList())); } out.endTag(null, TAG_ROOT); @@ -1943,8 +1886,10 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutPackage ret = new ShortcutPackage(shortcutUser, shortcutUser.getUserId(), packageName); - ret.mIsInitilized = ShortcutService.parseIntAttribute(parser, ATTR_SCHEMA_VERSON, 0) - == AppSearchShortcutInfo.SCHEMA_VERSION; + synchronized (ret.mLock) { + ret.mIsAppSearchSchemaUpToDate = ShortcutService.parseIntAttribute( + parser, ATTR_SCHEMA_VERSON, 0) == AppSearchShortcutInfo.SCHEMA_VERSION; + } ret.mApiCallCount = ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT); ret.mLastResetTime = @@ -2297,8 +2242,15 @@ class ShortcutPackage extends ShortcutPackageItem { } else { mPackageIdentifiers.remove(packageName); } - awaitInAppSearch(true, "Update visibility", - session -> AndroidFuture.completedFuture(true)); + synchronized (mLock) { + mIsAppSearchSchemaUpToDate = false; + } + final long callingIdentity = Binder.clearCallingIdentity(); + try { + fromAppSearch(); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } } void mutateShortcut(@NonNull final String id, @Nullable final ShortcutInfo shortcut, @@ -2325,148 +2277,15 @@ class ShortcutPackage extends ShortcutPackageItem { private void saveShortcut(@NonNull final Collection shortcuts) { Objects.requireNonNull(shortcuts); - if (!isAppSearchEnabled()) { - // If AppSearch isn't enabled, save it in memory and we are done. - for (ShortcutInfo si : shortcuts) { - mShortcuts.put(si.getId(), si); - } - return; + for (ShortcutInfo si : shortcuts) { + mShortcuts.put(si.getId(), si); } - // Otherwise, save pinned shortcuts in memory. - shortcuts.forEach(si -> { - if (si.isPinned()) { - mShortcuts.put(si.getId(), si); - } else { - mShortcuts.remove(si.getId()); - } - }); - // Then proceed to app search. - saveToAppSearch(shortcuts); - } - - private void saveToAppSearch(@NonNull final Collection shortcuts) { - Objects.requireNonNull(shortcuts); - if (!isAppSearchEnabled() || shortcuts.isEmpty()) { - // No need to invoke AppSearch when there's nothing to save. - return; - } - if (ShortcutService.DEBUG_REBOOT) { - Slog.d(TAG, "Saving shortcuts for user=" + mShortcutUser.getUserId() - + " pkg=" + getPackageName() + " ids=[" - + shortcuts.stream().map(ShortcutInfo::getId) - .collect(Collectors.joining(",")) + "]"); - } - awaitInAppSearch("Saving shortcuts", session -> { - final AndroidFuture future = new AndroidFuture<>(); - session.put(new PutDocumentsRequest.Builder() - .addGenericDocuments( - AppSearchShortcutInfo.toGenericDocuments(shortcuts)) - .build(), - mShortcutUser.mExecutor, - result -> { - if (!result.isSuccess()) { - for (AppSearchResult k : result.getFailures().values()) { - Slog.e(TAG, k.getErrorMessage()); - } - future.completeExceptionally(new RuntimeException( - "Failed to save shortcuts")); - return; - } - future.complete(true); - }); - return future; - }); - } - - /** - * Removes shortcuts from AppSearch. - */ - void removeShortcuts() { - if (!isAppSearchEnabled()) { - return; - } - awaitInAppSearch("Removing all shortcuts from " + getPackageName(), session -> { - final AndroidFuture future = new AndroidFuture<>(); - session.remove("", getSearchSpec(), mShortcutUser.mExecutor, result -> { - if (!result.isSuccess()) { - future.completeExceptionally( - new RuntimeException(result.getErrorMessage())); - return; - } - future.complete(true); - }); - return future; - }); - } - - private void removeShortcut(@NonNull final String id) { - Objects.requireNonNull(id); - mShortcuts.remove(id); - if (!isAppSearchEnabled()) { - return; - } - awaitInAppSearch("Removing shortcut with id=" + id, session -> { - final AndroidFuture future = new AndroidFuture<>(); - session.remove( - new RemoveByDocumentIdRequest.Builder(getPackageName()).addIds(id).build(), - mShortcutUser.mExecutor, result -> { - if (!result.isSuccess()) { - final Map> failures = - result.getFailures(); - for (String key : failures.keySet()) { - Slog.e(TAG, "Failed deleting " + key + ", error message:" - + failures.get(key).getErrorMessage()); - } - future.completeExceptionally(new RuntimeException( - "Failed to delete shortcut: " + id)); - return; - } - future.complete(true); - }); - return future; - }); } @Nullable - private List getShortcutById(@NonNull final Collection ids) { - final List shortcutIds = new ArrayList<>(1); - for (String id : ids) { - if (id != null) { - shortcutIds.add(id); - } - } - if (!isAppSearchEnabled()) { - final List ret = new ArrayList<>(1); - for (int i = mShortcuts.size() - 1; i >= 0; i--) { - ShortcutInfo si = mShortcuts.valueAt(i); - if (shortcutIds.contains(si.getId())) { - ret.add(si); - } - } - return ret; - } - if (ShortcutService.DEBUG_REBOOT) { - Slog.d(TAG, "Getting shortcuts for user=" + mShortcutUser.getUserId() - + " pkg=" + getPackageName() + " ids: [" + String.join(",", ids) + "]"); - } - return awaitInAppSearch("Getting shortcut by id", session -> { - final AndroidFuture> future = new AndroidFuture<>(); - session.getByDocumentId( - new GetByDocumentIdRequest.Builder(getPackageName()) - .addIds(shortcutIds).build(), - mShortcutUser.mExecutor, - results -> { - final List ret = new ArrayList<>(1); - Map documents = results.getSuccesses(); - for (GenericDocument doc : documents.values()) { - final ShortcutInfo info = new AppSearchShortcutInfo(doc) - .toShortcutInfo(mShortcutUser.getUserId()); - ret.add(info); - } - future.complete(ret); - }); - return future; - }); + List findAll(@NonNull final Collection ids) { + return ids.stream().map(mShortcuts::get) + .filter(Objects::nonNull).collect(Collectors.toList()); } private void forEachShortcut(@NonNull final Consumer cb) { @@ -2482,40 +2301,9 @@ class ShortcutPackage extends ShortcutPackageItem { } private void forEachShortcutMutate(@NonNull final Consumer cb) { - forEachShortcutMutateIf(si -> { + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + ShortcutInfo si = mShortcuts.valueAt(i); cb.accept(si); - return true; - }); - } - - private void forEachShortcutMutateIf(@NonNull final Function cb) { - forEachShortcutMutateIf("", cb); - } - - private void forEachShortcutMutateIf(@NonNull final String query, - @NonNull final Function cb) { - if (!isAppSearchEnabled()) { - for (int i = mShortcuts.size() - 1; i >= 0; i--) { - ShortcutInfo si = mShortcuts.valueAt(i); - cb.apply(si); - } - return; - } - if (ShortcutService.DEBUG_REBOOT) { - Slog.d(TAG, "Changing shortcuts for user=" + mShortcutUser.getUserId() - + " pkg=" + getPackageName()); - } - final SearchResults res = awaitInAppSearch("Mutating shortcuts", session -> - AndroidFuture.completedFuture(session.search(query, getSearchSpec()))); - if (res == null) return; - List shortcuts = getNextPage(res); - while (!shortcuts.isEmpty()) { - final List changed = new ArrayList<>(1); - for (ShortcutInfo si : shortcuts) { - if (cb.apply(si)) changed.add(si); - } - saveShortcut(changed); - shortcuts = getNextPage(res); } } @@ -2526,114 +2314,10 @@ class ShortcutPackage extends ShortcutPackageItem { private void forEachShortcutStopWhen( @NonNull final String query, @NonNull final Function cb) { - if (!isAppSearchEnabled()) { - for (int i = mShortcuts.size() - 1; i >= 0; i--) { - final ShortcutInfo si = mShortcuts.valueAt(i); - if (cb.apply(si)) { - return; - } - } - return; - } - if (ShortcutService.DEBUG_REBOOT) { - Slog.d(TAG, "Iterating shortcuts for user=" + mShortcutUser.getUserId() - + " pkg=" + getPackageName()); - } - final SearchResults res = awaitInAppSearch("Iterating shortcuts", session -> - AndroidFuture.completedFuture(session.search(query, getSearchSpec()))); - if (res == null) return; - List shortcuts = getNextPage(res); - while (!shortcuts.isEmpty()) { - for (ShortcutInfo si : shortcuts) { - if (cb.apply(si)) return; - } - shortcuts = getNextPage(res); - } - } - - private List getNextPage(@NonNull final SearchResults res) { - if (ShortcutService.DEBUG_REBOOT) { - Slog.d(TAG, "Get next page for search result for user=" + mShortcutUser.getUserId() - + " pkg=" + getPackageName()); - } - final AndroidFuture> future = new AndroidFuture<>(); - final List ret = new ArrayList<>(); - final long callingIdentity = Binder.clearCallingIdentity(); - try { - res.getNextPage(mShortcutUser.mExecutor, nextPage -> { - if (!nextPage.isSuccess()) { - future.complete(ret); - return; - } - final List results = nextPage.getResultValue(); - if (results.isEmpty()) { - future.complete(ret); - return; - } - final List page = new ArrayList<>(results.size()); - for (SearchResult result : results) { - final ShortcutInfo si = new AppSearchShortcutInfo(result.getGenericDocument()) - .toShortcutInfo(mShortcutUser.getUserId()); - page.add(si); - } - ret.addAll(page); - future.complete(ret); - }); - return ConcurrentUtils.waitForFutureNoInterrupt(future, - "Getting next batch of shortcuts"); - } finally { - Binder.restoreCallingIdentity(callingIdentity); - } - } - - @Nullable - private T awaitInAppSearch( - @NonNull final String description, - @NonNull final Function> cb) { - return awaitInAppSearch(false, description, cb); - } - - @Nullable - private T awaitInAppSearch( - final boolean forceReset, - @NonNull final String description, - @NonNull final Function> cb) { - if (!isAppSearchEnabled()) { - throw new IllegalStateException( - "awaitInAppSearch called when app search integration is disabled"); - } - synchronized (mLock) { - final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); - final long callingIdentity = Binder.clearCallingIdentity(); - final AppSearchManager.SearchContext searchContext = - new AppSearchManager.SearchContext.Builder(getPackageName()).build(); - try (AppSearchSession session = ConcurrentUtils.waitForFutureNoInterrupt( - mShortcutUser.getAppSearch(searchContext), "Resetting app search")) { - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() // TODO: change this to penaltyDeath to fix the call-site - .build()); - final boolean wasInitialized = mIsInitilized; - if (!wasInitialized || forceReset) { - ConcurrentUtils.waitForFutureNoInterrupt( - setupSchema(session), "Setting up schema"); - } - mIsInitilized = true; - if (!wasInitialized) { - restoreParsedShortcuts(false); - } - if (mRescanRequired) { - mRescanRequired = false; - rescanPackage(mIsNewApp, mManifestShortcuts); - } - return ConcurrentUtils.waitForFutureNoInterrupt(cb.apply(session), description); - } catch (Exception e) { - Slog.e(TAG, "Failed to initiate app search for shortcut package " - + getPackageName() + " user " + mShortcutUser.getUserId(), e); - return null; - } finally { - Binder.restoreCallingIdentity(callingIdentity); - StrictMode.setThreadPolicy(oldPolicy); + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + if (cb.apply(si)) { + return; } } } @@ -2657,7 +2341,7 @@ class ShortcutPackage extends ShortcutPackageItem { } final AndroidFuture future = new AndroidFuture<>(); session.setSchema( - schemaBuilder.build(), mShortcutUser.mExecutor, mShortcutUser.mExecutor, result -> { + schemaBuilder.build(), mExecutor, mShortcutUser.mExecutor, result -> { if (!result.isSuccess()) { future.completeExceptionally( new IllegalArgumentException(result.getErrorMessage())); @@ -2677,29 +2361,6 @@ class ShortcutPackage extends ShortcutPackageItem { .build(); } - /** - * Replace shortcuts parsed from xml file. - */ - void restoreParsedShortcuts() { - restoreParsedShortcuts(true); - } - - private void restoreParsedShortcuts(final boolean replace) { - if (ShortcutService.DEBUG_REBOOT) { - if (replace) { - Slog.d(TAG, "Replacing all shortcuts with the ones parsed from xml for user=" - + mShortcutUser.getUserId() + " pkg=" + getPackageName()); - } else { - Slog.d(TAG, "Restoring pinned shortcuts from xml for user=" - + mShortcutUser.getUserId() + " pkg=" + getPackageName()); - } - } - if (replace) { - removeShortcuts(); - } - saveToAppSearch(mShortcuts.values()); - } - private boolean verifyRanksSequential(List list) { boolean failed = false; @@ -2713,4 +2374,133 @@ class ShortcutPackage extends ShortcutPackageItem { } return failed; } + + // Async Operations + + /** + * Removes all shortcuts from AppSearch. + */ + void removeAllShortcutsAsync() { + if (!isAppSearchEnabled()) { + return; + } + runAsSystem(() -> fromAppSearch().thenAccept(session -> + session.remove("", getSearchSpec(), mShortcutUser.mExecutor, result -> { + if (!result.isSuccess()) { + Slog.e(TAG, "Failed to remove shortcuts from AppSearch. " + + result.getErrorMessage()); + } + }))); + } + + private void removeShortcutAsync(@NonNull final String... id) { + Objects.requireNonNull(id); + removeShortcutAsync(Arrays.asList(id)); + } + + private void removeShortcutAsync(@NonNull final Collection ids) { + if (!isAppSearchEnabled()) { + return; + } + runAsSystem(() -> fromAppSearch().thenAccept(session -> + session.remove( + new RemoveByDocumentIdRequest.Builder(getPackageName()).addIds(ids).build(), + mShortcutUser.mExecutor, result -> { + if (!result.isSuccess()) { + final Map> failures = + result.getFailures(); + for (String key : failures.keySet()) { + Slog.e(TAG, "Failed deleting " + key + ", error message:" + + failures.get(key).getErrorMessage()); + } + } + }))); + } + + private void saveShortcutsAsync( + @NonNull final Collection shortcuts) { + Objects.requireNonNull(shortcuts); + if (!isAppSearchEnabled() || shortcuts.isEmpty()) { + // No need to invoke AppSearch when there's nothing to save. + return; + } + if (ShortcutService.DEBUG_REBOOT) { + Slog.d(TAG, "Saving shortcuts async for user=" + mShortcutUser.getUserId() + + " pkg=" + getPackageName() + " ids=[" + + shortcuts.stream().map(ShortcutInfo::getId) + .collect(Collectors.joining(",")) + "]"); + } + runAsSystem(() -> fromAppSearch().thenAccept(session -> { + if (shortcuts.isEmpty()) { + return; + } + session.put(new PutDocumentsRequest.Builder() + .addGenericDocuments( + AppSearchShortcutInfo.toGenericDocuments(shortcuts)) + .build(), + mShortcutUser.mExecutor, + result -> { + if (!result.isSuccess()) { + for (AppSearchResult k : result.getFailures().values()) { + Slog.e(TAG, k.getErrorMessage()); + } + } + }); + })); + } + + @VisibleForTesting + void getTopShortcutsFromPersistence(AndroidFuture> cb) { + runAsSystem(() -> fromAppSearch().thenAccept(session -> { + SearchResults res = session.search("", getSearchSpec()); + res.getNextPage(mShortcutUser.mExecutor, results -> { + if (!results.isSuccess()) { + cb.completeExceptionally(new IllegalStateException(results.getErrorMessage())); + return; + } + cb.complete(results.getResultValue().stream() + .map(SearchResult::getGenericDocument) + .map(AppSearchShortcutInfo::new) + .map(si -> si.toShortcutInfo(mShortcutUser.getUserId())) + .collect(Collectors.toList())); + }); + })); + } + + @NonNull + private AndroidFuture fromAppSearch() { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + final AppSearchManager.SearchContext searchContext = + new AppSearchManager.SearchContext.Builder(getPackageName()).build(); + AndroidFuture future = null; + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() // TODO: change this to penaltyDeath to fix the call-site + .build()); + future = mShortcutUser.getAppSearch(searchContext); + synchronized (mLock) { + if (!mIsAppSearchSchemaUpToDate) { + future = future.thenCompose(this::setupSchema); + } + mIsAppSearchSchemaUpToDate = true; + } + } catch (Exception e) { + Slog.e(TAG, "Failed to invoke app search pkg=" + + getPackageName() + " user=" + mShortcutUser.getUserId(), e); + Objects.requireNonNull(future).completeExceptionally(e); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + return Objects.requireNonNull(future); + } + + private void runAsSystem(@NonNull final Runnable fn) { + final long callingIdentity = Binder.clearCallingIdentity(); + try { + fn.run(); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } } diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java index e21c9c2d0086f..c1f57f970127b 100644 --- a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java +++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java @@ -467,7 +467,7 @@ class ShortcutRequestPinProcessor { launcher.attemptToRestoreIfNeededAndSave(); if (launcher.hasPinned(original)) { if (DEBUG) { - Slog.d(TAG, "Shortcut " + original + " already pinned."); // This too. + Slog.d(TAG, "Shortcut " + original + " already pinned."); // This too. } return true; } @@ -517,7 +517,8 @@ class ShortcutRequestPinProcessor { if (DEBUG) { Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); } - ps.deleteDynamicWithId(shortcutId, /*ignoreInvisible=*/ false); + ps.deleteDynamicWithId(shortcutId, /*ignoreInvisible=*/ false, + /*wasPushedOut=*/ false); } ps.adjustRanks(); // Shouldn't be needed, but just in case. diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 687a165e40323..85b743594b75b 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1965,13 +1965,12 @@ public class ShortcutService extends IShortcutService.Stub { ArrayList cachedOrPinned = new ArrayList<>(); ps.findAll(cachedOrPinned, - AppSearchShortcutInfo.QUERY_IS_VISIBLE_CACHED_OR_PINNED, (ShortcutInfo si) -> si.isVisibleToPublisher() && si.isDynamic() && (si.isCached() || si.isPinned()), ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); // First, remove all un-pinned and non-cached; dynamic shortcuts - removedShortcuts = ps.deleteAllDynamicShortcuts(/*ignoreInvisible=*/ true); + removedShortcuts = ps.deleteAllDynamicShortcuts(); // Then, add/update all. We need to make sure to take over "pinned" flag. for (int i = 0; i < size; i++) { @@ -2381,7 +2380,8 @@ public class ShortcutService extends IShortcutService.Stub { if (!ps.isShortcutExistsAndVisibleToPublisher(id)) { continue; } - ShortcutInfo removed = ps.deleteDynamicWithId(id, /*ignoreInvisible=*/ true); + ShortcutInfo removed = ps.deleteDynamicWithId(id, /*ignoreInvisible=*/ true, + /*wasPushedOut*/ false); if (removed == null) { if (changedShortcuts == null) { changedShortcuts = new ArrayList<>(1); @@ -2412,11 +2412,10 @@ public class ShortcutService extends IShortcutService.Stub { userId); // Dynamic shortcuts that are either cached or pinned will not get deleted. ps.findAll(changedShortcuts, - AppSearchShortcutInfo.QUERY_IS_VISIBLE_CACHED_OR_PINNED, (ShortcutInfo si) -> si.isVisibleToPublisher() && si.isDynamic() && (si.isCached() || si.isPinned()), ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); - removedShortcuts = ps.deleteAllDynamicShortcuts(/*ignoreInvisible=*/ true); + removedShortcuts = ps.deleteAllDynamicShortcuts(); changedShortcuts = prepareChangedShortcuts( changedShortcuts, null, removedShortcuts, ps); } @@ -2475,10 +2474,8 @@ public class ShortcutService extends IShortcutService.Stub { | (matchPinned ? ShortcutInfo.FLAG_PINNED : 0) | (matchManifest ? ShortcutInfo.FLAG_MANIFEST : 0) | (matchCached ? ShortcutInfo.FLAG_CACHED_ALL : 0); - final String query = AppSearchShortcutInfo.QUERY_IS_VISIBLE_TO_PUBLISHER + " " - + createQuery(matchDynamic, matchPinned, matchManifest, matchCached); return getShortcutsWithQueryLocked( - packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, query, + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, (ShortcutInfo si) -> si.isVisibleToPublisher() && (si.getFlags() & shortcutFlags) != 0); @@ -2542,13 +2539,12 @@ public class ShortcutService extends IShortcutService.Stub { @GuardedBy("mLock") private ParceledListSlice getShortcutsWithQueryLocked(@NonNull String packageName, - @UserIdInt int userId, int cloneFlags, @NonNull final String query, - @NonNull Predicate filter) { + @UserIdInt int userId, int cloneFlags, @NonNull Predicate filter) { final ArrayList ret = new ArrayList<>(); final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); - ps.findAll(ret, query, filter, cloneFlags); + ps.findAll(ret, filter, cloneFlags); return new ParceledListSlice<>(setReturnedByServer(ret)); } @@ -2955,27 +2951,14 @@ public class ShortcutService extends IShortcutService.Stub { ((queryFlags & ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER) != 0); queryFlags |= (getPinnedByAnyLauncher ? ShortcutQuery.FLAG_MATCH_PINNED : 0); - final boolean matchPinnedOnly = - ((queryFlags & ShortcutQuery.FLAG_MATCH_PINNED) != 0) - && ((queryFlags & ShortcutQuery.FLAG_MATCH_CACHED) == 0) - && ((queryFlags & ShortcutQuery.FLAG_MATCH_DYNAMIC) == 0) - && ((queryFlags & ShortcutQuery.FLAG_MATCH_MANIFEST) == 0); - final Predicate filter = getFilterFromQuery(ids, locusIds, changedSince, componentName, queryFlags, getPinnedByAnyLauncher); - if (matchPinnedOnly) { - p.findAllPinned(ret, filter, cloneFlag, callingPackage, launcherUserId, - getPinnedByAnyLauncher); - } else if (ids != null && !ids.isEmpty()) { + if (ids != null && !ids.isEmpty()) { p.findAllByIds(ret, ids, filter, cloneFlag, callingPackage, launcherUserId, getPinnedByAnyLauncher); } else { - final boolean matchDynamic = (queryFlags & ShortcutQuery.FLAG_MATCH_DYNAMIC) != 0; - final boolean matchPinned = (queryFlags & ShortcutQuery.FLAG_MATCH_PINNED) != 0; - final boolean matchManifest = (queryFlags & ShortcutQuery.FLAG_MATCH_MANIFEST) != 0; - final boolean matchCached = (queryFlags & ShortcutQuery.FLAG_MATCH_CACHED) != 0; - p.findAll(ret, createQuery(matchDynamic, matchPinned, matchManifest, matchCached), - filter, cloneFlag, callingPackage, launcherUserId, getPinnedByAnyLauncher); + p.findAll(ret, filter, cloneFlag, callingPackage, launcherUserId, + getPinnedByAnyLauncher); } } @@ -3090,8 +3073,7 @@ public class ShortcutService extends IShortcutService.Stub { if (sp != null) { // List the shortcuts that are pinned only, these will get removed. removedShortcuts = new ArrayList<>(); - sp.findAll(removedShortcuts, AppSearchShortcutInfo.QUERY_IS_VISIBLE_PINNED_ONLY, - (ShortcutInfo si) -> si.isVisibleToPublisher() + sp.findAll(removedShortcuts, (ShortcutInfo si) -> si.isVisibleToPublisher() && si.isPinned() && !si.isCached() && !si.isDynamic() && !si.isDeclaredInManifest(), ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO, @@ -3183,8 +3165,7 @@ public class ShortcutService extends IShortcutService.Stub { if (doCache) { if (si.isLongLived()) { - sp.mutateShortcut(si.getId(), si, - shortcut -> shortcut.addFlags(cacheFlags)); + si.addFlags(cacheFlags); if (changedShortcuts == null) { changedShortcuts = new ArrayList<>(1); } @@ -3195,21 +3176,20 @@ public class ShortcutService extends IShortcutService.Stub { } } else { ShortcutInfo removed = null; - sp.mutateShortcut(si.getId(), si, shortcut -> - shortcut.clearFlags(cacheFlags)); + si.clearFlags(cacheFlags); if (!si.isDynamic() && !si.isCached()) { removed = sp.deleteLongLivedWithId(id, /*ignoreInvisible=*/ true); } - if (removed != null) { - if (removedShortcuts == null) { - removedShortcuts = new ArrayList<>(1); - } - removedShortcuts.add(removed); - } else { + if (removed == null) { if (changedShortcuts == null) { changedShortcuts = new ArrayList<>(1); } changedShortcuts.add(si); + } else { + if (removedShortcuts == null) { + removedShortcuts = new ArrayList<>(1); + } + removedShortcuts.add(removed); } } } @@ -5084,8 +5064,7 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId); if (pkg == null) return; - - pkg.mutateShortcut(shortcutId, null, cb); + cb.accept(pkg.findShortcutById(shortcutId)); } } diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java index e66cb03950cc9..408f045f47b8c 100644 --- a/services/core/java/com/android/server/pm/ShortcutUser.java +++ b/services/core/java/com/android/server/pm/ShortcutUser.java @@ -186,7 +186,7 @@ class ShortcutUser { final ShortcutPackage removed = mPackages.remove(packageName); if (removed != null) { - removed.removeShortcuts(); + removed.removeAllShortcutsAsync(); } mService.cleanupBitmapsForPackage(mUserId, packageName); @@ -577,7 +577,7 @@ class ShortcutUser { Log.w(TAG, "Shortcuts for package " + sp.getPackageName() + " are being restored." + " Existing non-manifeset shortcuts will be overwritten."); } - sp.restoreParsedShortcuts(); + sp.removeAllShortcutsAsync(); addPackage(sp); restoredPackages[0]++; restoredShortcuts[0] += sp.getShortcutCount(); @@ -714,6 +714,7 @@ class ShortcutUser { .setSubtype(totalSharingShortcutCount)); } + @NonNull AndroidFuture getAppSearch( @NonNull final AppSearchManager.SearchContext searchContext) { final AndroidFuture future = new AndroidFuture<>(); diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 4f77afb4969ff..f45c869949b59 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -46,18 +46,6 @@ import android.app.IUidObserver; import android.app.PendingIntent; import android.app.Person; import android.app.admin.DevicePolicyManager; -import android.app.appsearch.AppSearchBatchResult; -import android.app.appsearch.AppSearchManager; -import android.app.appsearch.AppSearchResult; -import android.app.appsearch.GenericDocument; -import android.app.appsearch.PackageIdentifier; -import android.app.appsearch.SearchResultPage; -import android.app.appsearch.SetSchemaResponse; -import android.app.appsearch.aidl.AppSearchBatchResultParcel; -import android.app.appsearch.aidl.AppSearchResultParcel; -import android.app.appsearch.aidl.IAppSearchBatchResultCallback; -import android.app.appsearch.aidl.IAppSearchManager; -import android.app.appsearch.aidl.IAppSearchResultCallback; import android.app.role.OnRoleHoldersChangedListener; import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; @@ -92,9 +80,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.FileUtils; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; -import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; @@ -106,6 +92,7 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Pair; +import com.android.internal.infra.AndroidFuture; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.LauncherAppsService.LauncherAppsImpl; @@ -168,7 +155,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { case Context.DEVICE_POLICY_SERVICE: return mMockDevicePolicyManager; case Context.APP_SEARCH_SERVICE: - return new AppSearchManager(this, mMockAppSearchManager); case Context.ROLE_SERVICE: // RoleManager is final and cannot be mocked, so we only override the inject // accessor methods in ShortcutService. @@ -647,260 +633,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } } - protected class MockAppSearchManager implements IAppSearchManager { - - protected Map> mSchemasVisibleToPackages = - new ArrayMap<>(1); - private Map> mDocumentMap = new ArrayMap<>(1); - - private String getKey(UserHandle userHandle, String databaseName) { - return userHandle.getIdentifier() + "@" + databaseName; - } - - @Override - public void setSchema(String packageName, String databaseName, List schemaBundles, - List schemasNotDisplayedBySystem, - Map> schemasVisibleToPackagesBundles, boolean forceOverride, - int version, UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchResultCallback callback) throws RemoteException { - for (Map.Entry> entry : - schemasVisibleToPackagesBundles.entrySet()) { - final String key = entry.getKey(); - final List packageIdentifiers; - if (!mSchemasVisibleToPackages.containsKey(key)) { - packageIdentifiers = new ArrayList<>(entry.getValue().size()); - mSchemasVisibleToPackages.put(key, packageIdentifiers); - } else { - packageIdentifiers = mSchemasVisibleToPackages.get(key); - } - for (int i = 0; i < entry.getValue().size(); i++) { - packageIdentifiers.add(new PackageIdentifier(entry.getValue().get(i))); - } - } - final SetSchemaResponse response = new SetSchemaResponse.Builder().build(); - callback.onResult( - new AppSearchResultParcel( - AppSearchResult.newSuccessfulResult(response.getBundle()))); - } - - @Override - public void getSchema(String packageName, String databaseName, UserHandle userHandle, - IAppSearchResultCallback callback) throws RemoteException { - ignore(callback); - } - - @Override - public void getNamespaces(String packageName, String databaseName, UserHandle userHandle, - IAppSearchResultCallback callback) throws RemoteException { - ignore(callback); - } - - @Override - public void putDocuments(String packageName, String databaseName, - List documentBundles, UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchBatchResultCallback callback) - throws RemoteException { - final List docs = new ArrayList<>(documentBundles.size()); - for (Bundle bundle : documentBundles) { - docs.add(new GenericDocument(bundle)); - } - final AppSearchBatchResult.Builder builder = - new AppSearchBatchResult.Builder<>(); - final String key = getKey(userHandle, databaseName); - Map docMap = mDocumentMap.get(key); - for (GenericDocument doc : docs) { - builder.setSuccess(doc.getId(), null); - if (docMap == null) { - docMap = new ArrayMap<>(1); - mDocumentMap.put(key, docMap); - } - docMap.put(doc.getId(), doc); - } - callback.onResult(new AppSearchBatchResultParcel<>(builder.build())); - } - - @Override - public void getDocuments(String packageName, String databaseName, String namespace, - List ids, Map> typePropertyPaths, - UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchBatchResultCallback callback) throws RemoteException { - final AppSearchBatchResult.Builder builder = - new AppSearchBatchResult.Builder<>(); - final String key = getKey(userHandle, databaseName); - if (!mDocumentMap.containsKey(key)) { - for (String id : ids) { - builder.setFailure(id, AppSearchResult.RESULT_NOT_FOUND, - key + " not found when getting: " + id); - } - } else { - final Map docs = mDocumentMap.get(key); - for (String id : ids) { - if (docs.containsKey(id)) { - builder.setSuccess(id, docs.get(id).getBundle()); - } else { - builder.setFailure(id, AppSearchResult.RESULT_NOT_FOUND, - "shortcut not found: " + id); - } - } - } - callback.onResult(new AppSearchBatchResultParcel<>(builder.build())); - } - - @Override - public void query(String packageName, String databaseName, String queryExpression, - Bundle searchSpecBundle, UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchResultCallback callback) - throws RemoteException { - final String key = getKey(userHandle, databaseName); - if (!mDocumentMap.containsKey(key)) { - final Bundle page = new Bundle(); - page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 1); - page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, new ArrayList<>()); - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(page))); - return; - } - final List documents = new ArrayList<>(mDocumentMap.get(key).values()); - final Bundle page = new Bundle(); - page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 0); - final ArrayList resultBundles = new ArrayList<>(); - for (GenericDocument document : documents) { - final Bundle resultBundle = new Bundle(); - resultBundle.putBundle("document", document.getBundle()); - resultBundle.putString("packageName", packageName); - resultBundle.putString("databaseName", databaseName); - resultBundle.putParcelableArrayList("matches", new ArrayList<>()); - resultBundles.add(resultBundle); - } - page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles); - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(page))); - } - - @Override - public void globalQuery(String packageName, String queryExpression, Bundle searchSpecBundle, - UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchResultCallback callback) throws RemoteException { - ignore(callback); - } - - @Override - public void getNextPage(String packageName, long nextPageToken, UserHandle userHandle, - IAppSearchResultCallback callback) throws RemoteException { - final Bundle page = new Bundle(); - page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 1); - page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, new ArrayList<>()); - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(page))); - } - - @Override - public void invalidateNextPageToken(String packageName, long nextPageToken, - UserHandle userHandle) throws RemoteException { - } - - @Override - public void writeQueryResultsToFile(String packageName, String databaseName, - ParcelFileDescriptor fileDescriptor, String queryExpression, - Bundle searchSpecBundle, UserHandle userHandle, IAppSearchResultCallback callback) - throws RemoteException { - ignore(callback); - } - - @Override - public void putDocumentsFromFile(String packageName, String databaseName, - ParcelFileDescriptor fileDescriptor, UserHandle userHandle, - IAppSearchResultCallback callback) - throws RemoteException { - ignore(callback); - } - - @Override - public void reportUsage(String packageName, String databaseName, String namespace, - String documentId, long usageTimestampMillis, boolean systemUsage, - UserHandle userHandle, - IAppSearchResultCallback callback) - throws RemoteException { - ignore(callback); - } - - @Override - public void removeByDocumentId(String packageName, String databaseName, String namespace, - List ids, UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchBatchResultCallback callback) - throws RemoteException { - final AppSearchBatchResult.Builder builder = - new AppSearchBatchResult.Builder<>(); - final String key = getKey(userHandle, databaseName); - if (!mDocumentMap.containsKey(key)) { - for (String id : ids) { - builder.setFailure(id, AppSearchResult.RESULT_NOT_FOUND, - "package " + key + " not found when removing " + id); - } - } else { - final Map docs = mDocumentMap.get(key); - for (String id : ids) { - if (docs.containsKey(id)) { - docs.remove(id); - builder.setSuccess(id, null); - } else { - builder.setFailure(id, AppSearchResult.RESULT_NOT_FOUND, - "shortcut not found when removing " + id); - } - } - } - callback.onResult(new AppSearchBatchResultParcel<>(builder.build())); - } - - @Override - public void removeByQuery(String packageName, String databaseName, String queryExpression, - Bundle searchSpecBundle, UserHandle userHandle, long binderCallStartTimeMillis, - IAppSearchResultCallback callback) - throws RemoteException { - final String key = getKey(userHandle, databaseName); - if (!mDocumentMap.containsKey(key)) { - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(null))); - return; - } - mDocumentMap.get(key).clear(); - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(null))); - } - - @Override - public void getStorageInfo(String packageName, String databaseName, UserHandle userHandle, - IAppSearchResultCallback callback) throws RemoteException { - ignore(callback); - } - - @Override - public void persistToDisk(String packageName, UserHandle userHandle, - long binderCallStartTimeMillis) throws RemoteException { - } - - @Override - public void initialize(String packageName, UserHandle userHandle, - long binderCallStartTimeMillis, IAppSearchResultCallback callback) - throws RemoteException { - ignore(callback); - } - - @Override - public IBinder asBinder() { - return null; - } - - private void removeShortcuts() { - mDocumentMap.clear(); - } - - private void ignore(IAppSearchResultCallback callback) throws RemoteException { - callback.onResult( - new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(null))); - } - } - public static class ShortcutActivity extends Activity { } @@ -952,7 +684,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected PackageManagerInternal mMockPackageManagerInternal; protected UserManager mMockUserManager; protected DevicePolicyManager mMockDevicePolicyManager; - protected MockAppSearchManager mMockAppSearchManager; protected UserManagerInternal mMockUserManagerInternal; protected UsageStatsManagerInternal mMockUsageStatsManagerInternal; protected ActivityManagerInternal mMockActivityManagerInternal; @@ -1102,7 +833,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { mMockPackageManagerInternal = mock(PackageManagerInternal.class); mMockUserManager = mock(UserManager.class); mMockDevicePolicyManager = mock(DevicePolicyManager.class); - mMockAppSearchManager = new MockAppSearchManager(); mMockUserManagerInternal = mock(UserManagerInternal.class); mMockUsageStatsManagerInternal = mock(UsageStatsManagerInternal.class); mMockActivityManagerInternal = mock(ActivityManagerInternal.class); @@ -1314,9 +1044,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { shutdownServices(); - mMockAppSearchManager.removeShortcuts(); - mMockAppSearchManager = null; - super.tearDown(); } @@ -2235,6 +1962,18 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return p == null ? null : p.getAllShareTargetsForTest(); } + protected void resetPersistedShortcuts() { + final ShortcutPackage p = mService.getPackageShortcutForTest( + getCallingPackage(), getCallingUserId()); + p.removeAllShortcutsAsync(); + } + + protected void getPersistedShortcut(AndroidFuture> cb) { + final ShortcutPackage p = mService.getPackageShortcutForTest( + getCallingPackage(), getCallingUserId()); + p.getTopShortcutsFromPersistence(cb); + } + /** * @return the number of shortcuts stored internally for the caller that can be used as a share * target in the ShareSheet. Such shortcuts have a matching category with at least one of the @@ -2425,8 +2164,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { deleteAllSavedFiles(); - mMockAppSearchManager.removeShortcuts(); - initService(); mService.applyRestore(payload, USER_0); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java index 9598a00df33d2..bc2d2563b12d8 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java @@ -18,9 +18,20 @@ package com.android.server.pm; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list; import android.app.PendingIntent; +import android.content.pm.ShortcutInfo; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; +import com.android.internal.infra.AndroidFuture; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + /** * Tests for {@link android.app.appsearch.AppSearchManager} and relevant APIs in ShortcutManager. * @@ -28,6 +39,21 @@ import android.os.UserHandle; */ public class ShortcutManagerTest12 extends BaseShortcutManagerTest { + @Override + protected void setUp() throws Exception { + super.setUp(); + mService.updateConfigurationLocked( + ShortcutService.ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); + } + + @Override + protected void tearDown() throws Exception { + setCaller(CALLING_PACKAGE_1, USER_0); + mService.getPackageShortcutForTest(CALLING_PACKAGE_1, USER_0).removeAllShortcutsAsync(); + super.tearDown(); + } + public void testGetShortcutIntents_ReturnsMutablePendingIntents() throws RemoteException { setDefaultLauncher(USER_0, LAUNCHER_1); @@ -41,4 +67,201 @@ public class ShortcutManagerTest12 extends BaseShortcutManagerTest { assertNotNull(intent); }); } + + public void testSetDynamicShortcuts_PersistsShortcutsToDisk() throws RemoteException { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + // Verifies setDynamicShortcuts persists shortcuts into AppSearch + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3") + )); + List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(3, shortcuts.size()); + Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s3")); + + // Verifies removeAllDynamicShortcuts removes shortcuts from persistence layer + mManager.removeAllDynamicShortcuts(); + shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertTrue(shortcuts.isEmpty()); + } + + public void testAddDynamicShortcuts_PersistsShortcutsToDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3") + )); + // Verifies addDynamicShortcuts persists shortcuts into AppSearch + mManager.addDynamicShortcuts(list(makeShortcut("s4"), makeShortcut("s5"))); + final List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(5, shortcuts.size()); + final Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s3")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + } + + public void testPushDynamicShortcuts_PersistsShortcutsToDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3"), + makeShortcut("s4"), + makeShortcut("s5") + )); + List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(5, shortcuts.size()); + Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s3")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + // Verifies pushDynamicShortcuts further persists shortcuts into AppSearch without + // removing previous shortcuts when max number of shortcuts is reached. + mManager.pushDynamicShortcut(makeShortcut("s6")); + shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(6, shortcuts.size()); + shortcutIds = shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s3")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + assertTrue(shortcutIds.contains("s6")); + } + + public void testRemoveDynamicShortcuts_RemovesShortcutsFromDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3"), + makeShortcut("s4"), + makeShortcut("s5") + )); + + // Verifies removeDynamicShortcuts removes shortcuts from persistence layer + mManager.removeDynamicShortcuts(list("s1")); + final List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(4, shortcuts.size()); + final Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s3")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + } + + public void testRemoveLongLivedShortcuts_RemovesShortcutsFromDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3"), + makeShortcut("s4"), + makeShortcut("s5") + )); + mManager.removeDynamicShortcuts(list("s2")); + final List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(4, shortcuts.size()); + final Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s3")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + } + + public void testDisableShortcuts_RemovesShortcutsFromDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3"), + makeShortcut("s4"), + makeShortcut("s5") + )); + // Verifies disableShortcuts removes shortcuts from persistence layer + mManager.disableShortcuts(list("s3")); + final List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(4, shortcuts.size()); + final Set shortcutIds = + shortcuts.stream().map(ShortcutInfo::getId).collect(Collectors.toSet()); + assertTrue(shortcutIds.contains("s1")); + assertTrue(shortcutIds.contains("s2")); + assertTrue(shortcutIds.contains("s4")); + assertTrue(shortcutIds.contains("s5")); + } + + public void testUpdateShortcuts_UpdateShortcutsOnDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeShortcut("s2"), + makeShortcut("s3"), + makeShortcut("s4"), + makeShortcut("s5") + )); + // Verifies disableShortcuts removes shortcuts from persistence layer + mManager.updateShortcuts(list(makeShortcutWithShortLabel("s3", "custom"))); + final List shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(5, shortcuts.size()); + final Map map = shortcuts.stream() + .collect(Collectors.toMap(ShortcutInfo::getId, Function.identity())); + assertTrue(map.containsKey("s3")); + assertEquals("custom", map.get("s3").getShortLabel()); + } + + private List getAllPersistedShortcuts() { + try { + SystemClock.sleep(500); + final AndroidFuture> future = new AndroidFuture<>(); + getPersistedShortcut(future); + return future.get(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } }