From 908945668b4c1a2ee384b1f0a8e0c55673cf6dac Mon Sep 17 00:00:00 2001 From: Pinyao Ting Date: Tue, 27 Apr 2021 17:00:03 -0700 Subject: [PATCH] AppSearch as persistent layer for shortcut Currently shortcuts are persisted into xml file upon creation/update. This CL additionally writes those shortcuts into AppSearch. Shortcuts persisted in AppSearch will only be removed when its publisher calls setDynamicShortcuts/disableShortcuts or one of the removal api. Shortcuts won't get removed as a result of exceeding the limit defined in getMaxShortcutCountPerActivity(). - setDynamicShortcuts: 1. Turn all dynamic shortcuts in memory into floating shortcuts. 2. Remove orphaned shortcuts (floating shotcuts that are neither cached nor pinned). 3. Add given shortcuts into memory. 4. Remove all shortcuts from AppSearch in background thread. 5. Schedule a job that writes shortcuts from memory into AppSearch in a background thread. - addDynamicShortcuts/updateShortcuts: 1. Add or update shortcuts in memory. 2. Schedule a job that writes shortcuts from memory into AppSearch in a background thread. - remove APIs: 1. Removes shortcuts from the system ram synchronously. 2. Removes shortcuts from AppSearch in a background thread. - reportShortcutUsed: 1. Generates an Usage Event that gets propagated to UsageEventManager. 2. Reports document usage in AppSearch in a background thread. - applyRestore: 1. Parses backup payload and load shortcuts into memory. 2. Persists shortcuts in memory into xml. 3. Schedule a job that writes shortcuts from memory into AppSearch in a background thread. - disableShortcuts 1. Turn correponding dynamic shortcuts in memory into floating shortcuts. 2. Remove orphaned shortcuts (floating shortcuts that are neither cached nor pinned). 3. If any of the shortcuts are removed as a result of step 2, removes corresponding shortcuts from AppSearch in a background thread. - enableShortcuts 1. Given the list of shortcutIds, if there exists a matching floating shortcut, that floating shortcut will be converted into dynamic shortcut again. Otherwise the shortcutId is ignored. 2. Since this API only mutates in-memory state of a shortcut that is not persisted into AppSearch, there will be no behavioral change here. - requestPinShortcuts: This api mutates in-memory states of a shortcut, these states are not persisted in AppSearch, thus there will be no behavioral change. Bug: 151359749 Test: manually enable feature flag and run ShortcutManagerTest12 Test: atest ShortcutManagerTest1 ShortcutManagerTest2 ShortcutManagerTest3 ShortcutManagerTest4 ShortcutManagerTest5 ShortcutManagerTest6 ShortcutManagerTest7 ShortcutManagerTest8 ShortcutManagerTest9 ShortcutManagerTest10 ShortcutManagerTest11 ShortcutManagerTest12 Test: atest CtsShortcutManagerTestCases Change-Id: Ibc06fa705cb65c220c4991a1b0247c1a4f318fed --- .../android/server/pm/ShortcutPackage.java | 714 +++++++----------- .../pm/ShortcutRequestPinProcessor.java | 5 +- .../android/server/pm/ShortcutService.java | 61 +- .../com/android/server/pm/ShortcutUser.java | 5 +- .../server/pm/BaseShortcutManagerTest.java | 289 +------ .../server/pm/ShortcutManagerTest12.java | 223 ++++++ 6 files changed, 514 insertions(+), 783 deletions(-) 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); + } + } }