diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 87dc0a17f41cc..bbcac56cf8afd 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -507,7 +507,8 @@ public class LauncherApps { /** * Indicates that one or more shortcuts, that match the {@link ShortcutQuery} used to * register this callback, have been added or updated. - * @see LauncherApps#registerShortcutChangeCallback(ShortcutChangeCallback, ShortcutQuery) + * @see LauncherApps#registerShortcutChangeCallback(ShortcutChangeCallback, ShortcutQuery, + * Executor) * *

Only the applications that are allowed to access the shortcut information, * as defined in {@link #hasShortcutHostPermission()}, will receive it. @@ -525,7 +526,8 @@ public class LauncherApps { /** * Indicates that one or more shortcuts, that match the {@link ShortcutQuery} used to * register this callback, have been removed. - * @see LauncherApps#registerShortcutChangeCallback(ShortcutChangeCallback, ShortcutQuery) + * @see LauncherApps#registerShortcutChangeCallback(ShortcutChangeCallback, ShortcutQuery, + * Executor) * *

Only the applications that are allowed to access the shortcut information, * as defined in {@link #hasShortcutHostPermission()}, will receive it. diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 1d5c304388706..385ace8a511b4 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -746,9 +746,8 @@ public class LauncherAppsService extends SystemService { } UserHandle user = UserHandle.of(injectCallingUserId()); - if (mContext.checkCallingOrSelfPermission( - android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) - == PackageManager.PERMISSION_GRANTED) { + if (injectHasInteractAcrossUsersFullPermission(injectBinderCallingPid(), + injectBinderCallingUid())) { user = null; } @@ -1053,29 +1052,6 @@ public class LauncherAppsService extends SystemService { } public static class ShortcutChangeHandler implements LauncherApps.ShortcutChangeCallback { - - static class QueryInfo { - final long mChangedSince; - final String mPackage; - final List mShortcutIds; - final List mLocusIds; - final ComponentName mActivity; - final int mQueryFlags; - final UserHandle mCallbackUser; - - QueryInfo(long changedSince, String packageName, List shortcutIds, - List locusIds, ComponentName activity, int flags, - UserHandle callbackUser) { - mChangedSince = changedSince; - mPackage = packageName; - mShortcutIds = shortcutIds; - mLocusIds = locusIds; - mActivity = activity; - mQueryFlags = flags; - mCallbackUser = callbackUser; - } - } - private final UserManagerInternal mUserManagerInternal; ShortcutChangeHandler(UserManagerInternal userManager) { @@ -1088,9 +1064,7 @@ public class LauncherAppsService extends SystemService { public synchronized void addShortcutChangeCallback(IShortcutChangeCallback callback, ShortcutQueryWrapper query, UserHandle user) { mCallbacks.unregister(callback); - mCallbacks.register(callback, new QueryInfo(query.getChangedSince(), - query.getPackage(), query.getShortcutIds(), query.getLocusIds(), - query.getActivity(), query.getQueryFlags(), user)); + mCallbacks.register(callback, new Pair<>(query, user)); } public synchronized void removeShortcutChangeCallback( @@ -1116,16 +1090,19 @@ public class LauncherAppsService extends SystemService { for (int i = 0; i < count; i++) { final IShortcutChangeCallback callback = mCallbacks.getBroadcastItem(i); - final QueryInfo query = (QueryInfo) mCallbacks.getBroadcastCookie(i); + final Pair cookie = + (Pair) + mCallbacks.getBroadcastCookie(i); - if (query.mCallbackUser != null && !hasUserAccess(query.mCallbackUser, user)) { + final UserHandle callbackUser = cookie.second; + if (callbackUser != null && !hasUserAccess(callbackUser, user)) { // Callback owner does not have access to the shortcuts' user. continue; } // Filter the list by query, if any matches exists, send via callback. - List matchedList = - filterShortcutsByQuery(packageName, shortcuts, query); + List matchedList = filterShortcutsByQuery(packageName, shortcuts, + cookie.first, shortcutsRemoved); if (!CollectionUtils.isEmpty(matchedList)) { try { if (shortcutsRemoved) { @@ -1143,21 +1120,25 @@ public class LauncherAppsService extends SystemService { } public static List filterShortcutsByQuery(String packageName, - List shortcuts, QueryInfo query) { - if (query.mPackage != null && query.mPackage != packageName) { + List shortcuts, ShortcutQueryWrapper query, + boolean shortcutsRemoved) { + final long changedSince = query.getChangedSince(); + final String queryPackage = query.getPackage(); + final List shortcutIds = query.getShortcutIds(); + final List locusIds = query.getLocusIds(); + final ComponentName activity = query.getActivity(); + final int flags = query.getQueryFlags(); + + if (queryPackage != null && !queryPackage.equals(packageName)) { return null; } List matches = new ArrayList<>(); - final boolean matchDynamic = - (query.mQueryFlags & ShortcutQuery.FLAG_MATCH_DYNAMIC) != 0; - final boolean matchPinned = - (query.mQueryFlags & ShortcutQuery.FLAG_MATCH_PINNED) != 0; - final boolean matchManifest = - (query.mQueryFlags & ShortcutQuery.FLAG_MATCH_MANIFEST) != 0; - final boolean matchCached = - (query.mQueryFlags & ShortcutQuery.FLAG_MATCH_CACHED) != 0; + final boolean matchDynamic = (flags & ShortcutQuery.FLAG_MATCH_DYNAMIC) != 0; + final boolean matchPinned = (flags & ShortcutQuery.FLAG_MATCH_PINNED) != 0; + final boolean matchManifest = (flags & ShortcutQuery.FLAG_MATCH_MANIFEST) != 0; + final boolean matchCached = (flags & ShortcutQuery.FLAG_MATCH_CACHED) != 0; final int shortcutFlags = (matchDynamic ? ShortcutInfo.FLAG_DYNAMIC : 0) | (matchPinned ? ShortcutInfo.FLAG_PINNED : 0) | (matchManifest ? ShortcutInfo.FLAG_MANIFEST : 0) @@ -1166,24 +1147,19 @@ public class LauncherAppsService extends SystemService { for (int i = 0; i < shortcuts.size(); i++) { final ShortcutInfo si = shortcuts.get(i); - if (query.mActivity != null && !query.mActivity.equals(si.getActivity())) { + if (activity != null && !activity.equals(si.getActivity())) { continue; } - - if (query.mChangedSince != 0 - && query.mChangedSince > si.getLastChangedTimestamp()) { + if (changedSince != 0 && changedSince > si.getLastChangedTimestamp()) { continue; } - - if (query.mShortcutIds != null && !query.mShortcutIds.contains(si.getId())) { + if (shortcutIds != null && !shortcutIds.contains(si.getId())) { continue; } - - if (query.mLocusIds != null && !query.mLocusIds.contains(si.getLocusId())) { + if (locusIds != null && !locusIds.contains(si.getLocusId())) { continue; } - - if ((shortcutFlags & si.getFlags()) != 0) { + if (shortcutsRemoved || (shortcutFlags & si.getFlags()) != 0) { matches.add(si); } } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 65f466f1ea6d5..71a4bb4319859 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -256,7 +256,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (shortcut != null) { mShortcutUser.mService.removeIconLocked(shortcut); shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED - | ShortcutInfo.FLAG_MANIFEST); + | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED); } return shortcut; } @@ -281,8 +281,10 @@ class ShortcutPackage extends ShortcutPackageItem { * invisible. * * It checks the max number of dynamic shortcuts. + * + * @return True if it replaced an existing shortcut, False otherwise. */ - public void addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) { + public boolean addOrReplaceDynamicShortcut(@NonNull ShortcutInfo newShortcut) { Preconditions.checkArgument(newShortcut.isEnabled(), "add/setDynamicShortcuts() cannot publish disabled shortcuts"); @@ -291,38 +293,59 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); + final boolean replaced; + final boolean wasPinned; + final boolean wasCached; if (oldShortcut == null) { + replaced = false; wasPinned = false; + wasCached = false; } else { // It's an update case. // Make sure the target is updatable. (i.e. should be mutable.) oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false); + replaced = true; wasPinned = oldShortcut.isPinned(); + wasCached = oldShortcut.isCached(); } // If it was originally pinned, the new one should be pinned too. if (wasPinned) { newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); } + if (wasCached) { + newShortcut.addFlags(ShortcutInfo.FLAG_CACHED); + } forceReplaceShortcutInner(newShortcut); + return replaced; } /** * Push a shortcut. If the max number of dynamic shortcuts is already reached, remove the * shortcut with the lowest rank before adding the new shortcut. + * + * Any shortcut that gets altered (removed or changed) as a result of this push operation will + * be included and returned in changedShortcuts. + * + * @return True if a shortcut had to be removed to complete this operation, False otherwise. */ - public boolean pushDynamicShortcut(@NonNull ShortcutInfo newShortcut) { + public boolean pushDynamicShortcut(@NonNull ShortcutInfo newShortcut, + @NonNull List changedShortcuts) { Preconditions.checkArgument(newShortcut.isEnabled(), "pushDynamicShortcuts() cannot publish disabled shortcuts"); newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + changedShortcuts.clear(); final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); boolean wasPinned = false; + boolean wasCached = false; + + boolean deleted = false; if (oldShortcut == null) { final ShortcutService service = mShortcutUser.mService; @@ -343,10 +366,11 @@ class ShortcutPackage extends ShortcutPackageItem { // All shortcuts are manifest shortcuts and cannot be removed. Slog.e(TAG, "Failed to remove manifest shortcut while pushing dynamic shortcut " + newShortcut.getId()); - return false; + return true; // poppedShortcuts is empty which indicates a failure. } - deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true); + changedShortcuts.add(shortcut); + deleted = deleteDynamicWithId(shortcut.getId(), /*ignoreInvisible=*/ true) != null; } } else { // It's an update case. @@ -354,15 +378,19 @@ class ShortcutPackage extends ShortcutPackageItem { oldShortcut.ensureUpdatableWith(newShortcut, /*isUpdating=*/ false); wasPinned = oldShortcut.isPinned(); + wasCached = oldShortcut.isCached(); } - // If it was originally pinned, the new one should be pinned too. + // If it was originally pinned or cached, the new one should be pinned or cached too. if (wasPinned) { newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); } + if (wasCached) { + newShortcut.addFlags(ShortcutInfo.FLAG_CACHED); + } forceReplaceShortcutInner(newShortcut); - return true; + return deleted; } /** @@ -371,8 +399,7 @@ class ShortcutPackage extends ShortcutPackageItem { * @return List of removed shortcuts. */ private List removeOrphans() { - ArrayList removeList = null; // Lazily initialize. - List removedShortcuts = null; + List removeList = null; for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); @@ -381,18 +408,16 @@ class ShortcutPackage extends ShortcutPackageItem { if (removeList == null) { removeList = new ArrayList<>(); - removedShortcuts = new ArrayList<>(); } - removeList.add(si.getId()); - removedShortcuts.add(si); + removeList.add(si); } if (removeList != null) { for (int i = removeList.size() - 1; i >= 0; i--) { - forceDeleteShortcutInner(removeList.get(i)); + forceDeleteShortcutInner(removeList.get(i).getId()); } } - return removedShortcuts; + return removeList; } /** @@ -424,68 +449,69 @@ class ShortcutPackage extends ShortcutPackageItem { * Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut * is pinned or cached, it'll remain as a pinned or cached shortcut, and is still enabled. * - * @return true if it's removed, or false if it was not actually removed because it is either + * @return The deleted shortcut, or null if it was not actually removed because it is either * pinned or cached. */ - public boolean deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) { - final ShortcutInfo removed = deleteOrDisableWithId( + public ShortcutInfo deleteDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible) { + return deleteOrDisableWithId( shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible, ShortcutInfo.DISABLED_REASON_NOT_DISABLED); - return removed == null; } /** - * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut + * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut * is pinned, it'll remain as a pinned shortcut, but will be disabled. * - * @return true if it's actually removed because it wasn't pinned, or false if it's still - * pinned. + * @return Shortcut if the disabled shortcut got removed because it wasn't pinned. Or null if + * it's still pinned. */ - private boolean disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible, + private ShortcutInfo disableDynamicWithId(@NonNull String shortcutId, boolean ignoreInvisible, int disabledReason) { - final ShortcutInfo disabled = deleteOrDisableWithId( - shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, ignoreInvisible, - disabledReason); - return disabled == null; + return deleteOrDisableWithId(shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false, + ignoreInvisible, disabledReason); } /** * Remove a long lived shortcut by ID. If the shortcut is pinned, it'll remain as a pinned * shortcut, and is still enabled. * - * @return true if it's actually removed because it wasn't pinned, or false if it's still - * pinned. + * @return The deleted shortcut, or null if it was not actually removed because it's pinned. */ - public boolean deleteLongLivedWithId(@NonNull String shortcutId, boolean ignoreInvisible) { + public ShortcutInfo deleteLongLivedWithId(@NonNull String shortcutId, boolean ignoreInvisible) { final ShortcutInfo shortcut = mShortcuts.get(shortcutId); if (shortcut != null) { shortcut.clearFlags(ShortcutInfo.FLAG_CACHED); } - final ShortcutInfo removed = deleteOrDisableWithId( + return deleteOrDisableWithId( shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false, ignoreInvisible, ShortcutInfo.DISABLED_REASON_NOT_DISABLED); - return removed == null; } /** * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut * is pinned, it'll remain as a pinned shortcut but will be disabled. + * + * @return Shortcut if the disabled shortcut got removed because it wasn't pinned. Or null if + * it's still pinned. */ - public void disableWithId(@NonNull String shortcutId, String disabledMessage, + public ShortcutInfo disableWithId(@NonNull String shortcutId, String disabledMessage, int disabledMessageResId, boolean overrideImmutable, boolean ignoreInvisible, int disabledReason) { - final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true, + final ShortcutInfo deleted = deleteOrDisableWithId(shortcutId, /* disable =*/ true, overrideImmutable, ignoreInvisible, disabledReason); + // If disabled id still exists, it is pinned and we need to update the disabled message. + final ShortcutInfo disabled = mShortcuts.get(shortcutId); if (disabled != null) { if (disabledMessage != null) { disabled.setDisabledMessage(disabledMessage); } else if (disabledMessageResId != 0) { disabled.setDisabledMessageResId(disabledMessageResId); - mShortcutUser.mService.fixUpShortcutResourceNamesAndValues(disabled); } } + + return deleted; } @Nullable @@ -521,10 +547,10 @@ class ShortcutPackage extends ShortcutPackageItem { oldShortcut.setActivity(null); } - return oldShortcut; + return null; } else { forceDeleteShortcutInner(shortcutId); - return null; + return oldShortcut; } } @@ -1005,7 +1031,7 @@ 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)) { + ShortcutInfo.DISABLED_REASON_APP_CHANGED) != null) { continue; // Actually removed. } // Still pinned, so fall-through and possibly update the resources. diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java index 6fd997db8be8f..d4a02a9ceb2ae 100644 --- a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java +++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java @@ -35,7 +35,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -513,10 +513,6 @@ class ShortcutRequestPinProcessor { launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId, /*forPinRequest=*/ true); - if (changedShortcuts == null) { - changedShortcuts = new ArrayList<>(1); - } - changedShortcuts.add(original); if (current == null) { if (DEBUG) { @@ -526,6 +522,8 @@ class ShortcutRequestPinProcessor { } ps.adjustRanks(); // Shouldn't be needed, but just in case. + + changedShortcuts = Collections.singletonList(ps.findShortcutById(shortcutId)); } mService.verifyStates(); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 8d53d15546194..e4ae007dadee3 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1666,11 +1666,10 @@ public class ShortcutService extends IShortcutService.Stub { * - Write to file */ void packageShortcutsChanged(@NonNull String packageName, @UserIdInt int userId, - @Nullable List addedOrUpdatedShortcuts, - @Nullable List removedShortcuts) { + @Nullable final List changedShortcuts, + @Nullable final List removedShortcuts) { notifyListeners(packageName, userId); - notifyShortcutChangeCallbacks(packageName, userId, addedOrUpdatedShortcuts, - removedShortcuts); + notifyShortcutChangeCallbacks(packageName, userId, changedShortcuts, removedShortcuts); scheduleSaveUser(userId); } @@ -1699,8 +1698,11 @@ public class ShortcutService extends IShortcutService.Stub { } private void notifyShortcutChangeCallbacks(@NonNull String packageName, @UserIdInt int userId, - @Nullable List addedOrUpdatedShortcuts, - @Nullable List removedShortcuts) { + @Nullable final List changedShortcuts, + @Nullable final List removedShortcuts) { + final List changedList = removeNonKeyFields(changedShortcuts); + final List removedList = removeNonKeyFields(removedShortcuts); + final UserHandle user = UserHandle.of(userId); injectPostToHandler(() -> { try { @@ -1713,12 +1715,11 @@ public class ShortcutService extends IShortcutService.Stub { copy = new ArrayList<>(mShortcutChangeCallbacks); } for (int i = copy.size() - 1; i >= 0; i--) { - if (!CollectionUtils.isEmpty(addedOrUpdatedShortcuts)) { - copy.get(i).onShortcutsAddedOrUpdated(packageName, addedOrUpdatedShortcuts, - user); + if (!CollectionUtils.isEmpty(changedList)) { + copy.get(i).onShortcutsAddedOrUpdated(packageName, changedList, user); } - if (!CollectionUtils.isEmpty(removedShortcuts)) { - copy.get(i).onShortcutsRemoved(packageName, removedShortcuts, user); + if (!CollectionUtils.isEmpty(removedList)) { + copy.get(i).onShortcutsRemoved(packageName, removedList, user); } } } catch (Exception ignore) { @@ -1726,6 +1727,25 @@ public class ShortcutService extends IShortcutService.Stub { }); } + private List removeNonKeyFields(@Nullable List shortcutInfos) { + if (CollectionUtils.isEmpty(shortcutInfos)) { + return shortcutInfos; + } + + final int size = shortcutInfos.size(); + List keyFieldOnlyShortcuts = new ArrayList<>(size); + + for (int i = 0; i < size; i++) { + final ShortcutInfo si = shortcutInfos.get(i); + if (si.hasKeyFieldsOnly()) { + keyFieldOnlyShortcuts.add(si); + } else { + keyFieldOnlyShortcuts.add(si.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO)); + } + } + return keyFieldOnlyShortcuts; + } + /** * Clean up / validate an incoming shortcut. * - Make sure all mandatory fields are set. @@ -1820,6 +1840,7 @@ public class ShortcutService extends IShortcutService.Stub { final boolean unlimited = injectHasUnlimitedShortcutsApiCallsPermission( injectBinderCallingPid(), injectBinderCallingUid()); + List changedShortcuts = null; List removedShortcuts = null; synchronized (mLock) { @@ -1846,7 +1867,12 @@ public class ShortcutService extends IShortcutService.Stub { fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); } - // First, remove all un-pinned; dynamic shortcuts + ArrayList cachedOrPinned = new ArrayList<>(); + ps.findAll(cachedOrPinned, (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); // Then, add/update all. We need to make sure to take over "pinned" flag. @@ -1857,8 +1883,12 @@ public class ShortcutService extends IShortcutService.Stub { // Lastly, adjust the ranks. ps.adjustRanks(); + + changedShortcuts = prepareChangedShortcuts( + cachedOrPinned, newShortcuts, removedShortcuts, ps); } - packageShortcutsChanged(packageName, userId, newShortcuts, removedShortcuts); + + packageShortcutsChanged(packageName, userId, changedShortcuts, removedShortcuts); verifyStates(); @@ -1916,6 +1946,11 @@ public class ShortcutService extends IShortcutService.Stub { "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); } + if (target.isLongLived() != source.isLongLived()) { + Slog.w(TAG, + "ShortcutInfo.longLived cannot be changed with updateShortcuts()"); + } + // When updating the rank, we need to insert between existing ranks, so set // this setRankChanged, and also copy the implicit rank fo adjustRanks(). if (source.hasRank()) { @@ -2026,8 +2061,8 @@ public class ShortcutService extends IShortcutService.Stub { verifyCaller(packageName, userId); verifyShortcutInfoPackage(packageName, shortcut); - final boolean unlimited = injectHasUnlimitedShortcutsApiCallsPermission( - injectBinderCallingPid(), injectBinderCallingUid()); + List changedShortcuts = new ArrayList<>(); + List removedShortcuts = null; synchronized (mLock) { throwIfUserLockedL(userId); @@ -2052,14 +2087,22 @@ public class ShortcutService extends IShortcutService.Stub { shortcut.setRankChanged(); // Push it. - if (!ps.pushDynamicShortcut(shortcut)) { - return; + boolean deleted = ps.pushDynamicShortcut(shortcut, changedShortcuts); + + if (deleted) { + if (changedShortcuts.isEmpty()) { + return; // Failed to push. + } + removedShortcuts = Collections.singletonList(changedShortcuts.get(0)); + changedShortcuts.clear(); } + changedShortcuts.add(shortcut); // Lastly, adjust the ranks. ps.adjustRanks(); } - packageShortcutsChanged(packageName, userId, Collections.singletonList(shortcut), null); + + packageShortcutsChanged(packageName, userId, changedShortcuts, removedShortcuts); verifyStates(); } @@ -2147,6 +2190,9 @@ public class ShortcutService extends IShortcutService.Stub { verifyCaller(packageName, userId); Objects.requireNonNull(shortcutIds, "shortcutIds must be provided"); + List changedShortcuts = null; + List removedShortcuts = null; + synchronized (mLock) { throwIfUserLockedL(userId); @@ -2163,17 +2209,30 @@ public class ShortcutService extends IShortcutService.Stub { if (!ps.isShortcutExistsAndVisibleToPublisher(id)) { continue; } - ps.disableWithId(id, + + final ShortcutInfo deleted = ps.disableWithId(id, disabledMessageString, disabledMessageResId, /* overrideImmutable=*/ false, /*ignoreInvisible=*/ true, ShortcutInfo.DISABLED_REASON_BY_APP); + + if (deleted == null) { + if (changedShortcuts == null) { + changedShortcuts = new ArrayList<>(1); + } + changedShortcuts.add(ps.findShortcutById(id)); + } else { + if (removedShortcuts == null) { + removedShortcuts = new ArrayList<>(1); + } + removedShortcuts.add(deleted); + } } // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. ps.adjustRanks(); } - // TODO: Disabling dynamic shortcuts will removed them if not pinned. Cover all cases. - packageShortcutsChanged(packageName, userId, null, null); + + packageShortcutsChanged(packageName, userId, changedShortcuts, removedShortcuts); verifyStates(); } @@ -2200,13 +2259,10 @@ public class ShortcutService extends IShortcutService.Stub { } ps.enableWithId(id); - final ShortcutInfo si = ps.findShortcutById(id); - if (si != null) { - if (changedShortcuts == null) { - changedShortcuts = new ArrayList<>(1); - } - changedShortcuts.add(si); + if (changedShortcuts == null) { + changedShortcuts = new ArrayList<>(1); } + changedShortcuts.add(ps.findShortcutById(id)); } } @@ -2237,18 +2293,18 @@ public class ShortcutService extends IShortcutService.Stub { if (!ps.isShortcutExistsAndVisibleToPublisher(id)) { continue; } - final ShortcutInfo si = ps.findShortcutById(id); - final boolean removed = ps.deleteDynamicWithId(id, /*ignoreInvisible=*/ true); - if (removed) { - if (removedShortcuts == null) { - removedShortcuts = new ArrayList<>(1); - } - removedShortcuts.add(si); - } else { + + ShortcutInfo removed = ps.deleteDynamicWithId(id, /*ignoreInvisible=*/ true); + if (removed == null) { if (changedShortcuts == null) { changedShortcuts = new ArrayList<>(1); } - changedShortcuts.add(si); + changedShortcuts.add(ps.findShortcutById(id)); + } else { + if (removedShortcuts == null) { + removedShortcuts = new ArrayList<>(1); + } + removedShortcuts.add(removed); } } @@ -2264,7 +2320,7 @@ public class ShortcutService extends IShortcutService.Stub { public void removeAllDynamicShortcuts(String packageName, @UserIdInt int userId) { verifyCaller(packageName, userId); - List changedShortcuts = null; + List changedShortcuts = new ArrayList<>(); List removedShortcuts = null; synchronized (mLock) { @@ -2272,10 +2328,16 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); + // Dynamic shortcuts that are either cached or pinned will not get deleted. + ps.findAll(changedShortcuts, (ShortcutInfo si) -> si.isVisibleToPublisher() + && si.isDynamic() && (si.isCached() || si.isPinned()), + ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); + removedShortcuts = ps.deleteAllDynamicShortcuts(/*ignoreInvisible=*/ true); + changedShortcuts = prepareChangedShortcuts( + changedShortcuts, null, removedShortcuts, ps); } - // TODO: Pinned and cached shortcuts are not removed, add those to changedShortcuts list packageShortcutsChanged(packageName, userId, changedShortcuts, removedShortcuts); verifyStates(); @@ -2300,22 +2362,21 @@ public class ShortcutService extends IShortcutService.Stub { for (int i = shortcutIds.size() - 1; i >= 0; i--) { final String id = Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)); + if (!ps.isShortcutExistsAndVisibleToPublisher(id)) { + continue; + } - final ShortcutInfo si = ps.findShortcutById(id); - final boolean removed = ps.deleteLongLivedWithId(id, /*ignoreInvisible=*/ true); - - if (si != null) { - if (removed) { - if (removedShortcuts == null) { - removedShortcuts = new ArrayList<>(1); - } - removedShortcuts.add(si); - } else { - if (changedShortcuts == null) { - changedShortcuts = new ArrayList<>(1); - } - changedShortcuts.add(si); + ShortcutInfo removed = ps.deleteLongLivedWithId(id, /*ignoreInvisible=*/ true); + if (removed != null) { + if (removedShortcuts == null) { + removedShortcuts = new ArrayList<>(1); } + removedShortcuts.add(removed); + } else { + if (changedShortcuts == null) { + changedShortcuts = new ArrayList<>(1); + } + changedShortcuts.add(ps.findShortcutById(id)); } } @@ -2939,6 +3000,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(shortcutIds, "shortcutIds"); List changedShortcuts = null; + List removedShortcuts = null; synchronized (mLock) { throwIfUserLockedL(userId); @@ -2948,24 +3010,31 @@ public class ShortcutService extends IShortcutService.Stub { getLauncherShortcutsLocked(callingPackage, userId, launcherUserId); launcher.attemptToRestoreIfNeededAndSave(); - launcher.pinShortcuts(userId, packageName, shortcutIds, /*forPinRequest=*/ false); - final ShortcutPackage sp = getUserShortcutsLocked(userId) .getPackageShortcutsIfExists(packageName); if (sp != null) { - for (int i = 0; i < shortcutIds.size(); i++) { - final ShortcutInfo si = sp.findShortcutById(shortcutIds.get(i)); - if (si != null) { - if (changedShortcuts == null) { - changedShortcuts = new ArrayList<>(1); - } - changedShortcuts.add(si); - } + // List the shortcuts that are pinned only, these will get removed. + removedShortcuts = new ArrayList<>(); + sp.findAll(removedShortcuts, (ShortcutInfo si) -> si.isVisibleToPublisher() + && si.isPinned() && !si.isCached() && !si.isDynamic() + && !si.isDeclaredInManifest(), ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO, + callingPackage, launcherUserId, false); + } + // Get list of shortcuts that will get unpinned. + ArraySet oldPinnedIds = launcher.getPinnedShortcutIds(packageName, userId); + + launcher.pinShortcuts(userId, packageName, shortcutIds, /*forPinRequest=*/ false); + + if (oldPinnedIds != null && removedShortcuts != null) { + for (int i = 0; i < removedShortcuts.size(); i++) { + oldPinnedIds.remove(removedShortcuts.get(i).getId()); } } + changedShortcuts = prepareChangedShortcuts( + oldPinnedIds, new ArraySet<>(shortcutIds), removedShortcuts, sp); } - // TODO: Include previously pinned shortcuts since they are not pinned anymore. - packageShortcutsChanged(packageName, userId, changedShortcuts, null); + + packageShortcutsChanged(packageName, userId, changedShortcuts, removedShortcuts); verifyStates(); } @@ -3045,17 +3114,17 @@ public class ShortcutService extends IShortcutService.Stub { + "shortcut " + si.getId()); } } else { - boolean removed = false; + ShortcutInfo removed = null; if (si.isDynamic()) { si.clearFlags(ShortcutInfo.FLAG_CACHED); } else { removed = sp.deleteLongLivedWithId(id, /*ignoreInvisible=*/ true); } - if (removed) { + if (removed != null) { if (removedShortcuts == null) { removedShortcuts = new ArrayList<>(1); } - removedShortcuts.add(si); + removedShortcuts.add(removed); } else { if (changedShortcuts == null) { changedShortcuts = new ArrayList<>(1); @@ -3222,8 +3291,6 @@ public class ShortcutService extends IShortcutService.Stub { // Grant read uri permission to the caller on behalf of the shortcut owner. All // granted permissions are revoked when the default launcher changes, or when // device is rebooted. - // b/151572645 is tracking a bug where Uri permissions are persisted across - // reboots, even when Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION is not used. mUriGrantsManager.grantUriPermissionFromOwner(mUriPermissionOwner, packageUid, launcherPackage, Uri.parse(uri), Intent.FLAG_GRANT_READ_URI_PERMISSION, userId, launcherUserId); @@ -4872,4 +4939,61 @@ public class ShortcutService extends IShortcutService.Stub { mShortcutBitmapSaver.waitForAllSavesLocked(); } } + + /** + * This helper method does the following 3 tasks: + * + * 1- Combines the |changed| and |updated| shortcut lists, while removing duplicates. + * 2- If a shortcut is deleted and added at once in the same operation, removes it from the + * |removed| list. + * 3- Reloads the final list to get the latest flags. + */ + private List prepareChangedShortcuts(ArraySet changedIds, + ArraySet newIds, List deletedList, final ShortcutPackage ps) { + if (ps == null) { + // This can happen when package restore is not finished yet. + return null; + } + if (CollectionUtils.isEmpty(changedIds) && CollectionUtils.isEmpty(newIds)) { + return null; + } + + ArraySet resultIds = new ArraySet<>(); + if (!CollectionUtils.isEmpty(changedIds)) { + resultIds.addAll(changedIds); + } + if (!CollectionUtils.isEmpty(newIds)) { + resultIds.addAll(newIds); + } + + if (!CollectionUtils.isEmpty(deletedList)) { + deletedList.removeIf((ShortcutInfo si) -> resultIds.contains(si.getId())); + } + + List result = new ArrayList<>(); + ps.findAll(result, (ShortcutInfo si) -> resultIds.contains(si.getId()), + ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); + return result; + } + + private List prepareChangedShortcuts(List changedList, + List newList, List deletedList, final ShortcutPackage ps) { + ArraySet changedIds = new ArraySet<>(); + addShortcutIdsToSet(changedIds, changedList); + + ArraySet newIds = new ArraySet<>(); + addShortcutIdsToSet(newIds, newList); + + return prepareChangedShortcuts(changedIds, newIds, deletedList, ps); + } + + private void addShortcutIdsToSet(ArraySet ids, List shortcuts) { + if (CollectionUtils.isEmpty(shortcuts)) { + return; + } + final int size = shortcuts.size(); + for (int i = 0; i < size; i++) { + ids.add(shortcuts.get(i).getId()); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java new file mode 100644 index 0000000000000..50d290a837cd8 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest11.java @@ -0,0 +1,775 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.ComponentName; +import android.content.pm.LauncherApps.ShortcutChangeCallback; +import android.content.pm.LauncherApps.ShortcutQuery; +import android.content.pm.ShortcutInfo; +import android.os.test.TestLooper; + +import org.mockito.ArgumentCaptor; + +import java.util.List; + +/** + * Tests for {@link android.content.pm.LauncherApps.ShortcutChangeCallback} and relevant APIs. + * + atest -c com.android.server.pm.ShortcutManagerTest11 + */ +public class ShortcutManagerTest11 extends BaseShortcutManagerTest { + + private static final ShortcutQuery QUERY_MATCH_ALL = createShortcutQuery( + ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED); + + private final TestLooper mTestLooper = new TestLooper(); + + public void testShortcutChangeCallback_setDynamicShortcuts() { + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2"); + } + + public void testShortcutChangeCallback_setDynamicShortcuts_replaceSameId() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s2", "s3"))); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_setDynamicShortcuts_pinnedAndCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts( + list(makeShortcut("s1"), makeLongLivedShortcut("s2")))); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_0); + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s3", "s4"))); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2", "s3", "s4"); + } + + public void testShortcutChangeCallback_pinShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_0); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_pinShortcuts_unpinOthers() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2", "s3"))); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("s1", "s2")); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), HANDLE_USER_0); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_cacheShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeLongLivedShortcut("s1"), + makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s3"), HANDLE_USER_0); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s3"); + } + + public void testShortcutChangeCallback_uncacheShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeLongLivedShortcut("s1"), + makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s1", "s2"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2"); + } + + public void testShortcutChangeCallback_uncacheShortcuts_causeDeletion() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeLongLivedShortcut("s1"), + makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3")))); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("s2", "s3")); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + mLauncherApps.uncacheShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), HANDLE_USER_0); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s3"); + } + + public void testShortcutChangeCallback_updateShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), + makeShortcutWithActivity("s2", new ComponentName(CALLING_PACKAGE_1, "test"))))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + final ComponentName updatedCn = new ComponentName(CALLING_PACKAGE_1, "updated activity"); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.updateShortcuts(list(makeShortcutWithActivity("s2", updatedCn)))); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2"); + assertEquals(updatedCn, ((ShortcutInfo) shortcuts.getValue().get(0)).getActivity()); + } + + public void testShortcutChangeCallback_addDynamicShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.addDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2"); + } + + public void testShortcutChangeCallback_pushDynamicShortcut() { + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.pushDynamicShortcut(makeShortcut("s1")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_pushDynamicShortcut_existingId() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts((makeShortcuts("s1", "s2", "s3", "s4", "s5", + "s6", "s7", "s8", "s9", "s10")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.pushDynamicShortcut(makeShortcut("s5")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s5"); + } + + public void testShortcutChangeCallback_pushDynamicShortcut_causeDeletion() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts((makeShortcuts("s1", "s2", "s3", "s4", "s5", + "s6", "s7", "s8", "s9", "s10")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.pushDynamicShortcut(makeShortcut("s11")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s11"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s10"); + } + + public void testShortcutChangeCallback_pushDynamicShortcut_causeDeletionButCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts((makeShortcuts("s1", "s2", "s3", "s4", "s5", + "s6", "s7", "s8", "s9")))); + ShortcutInfo s10 = makeLongLivedShortcut("s10"); + s10.setRank(10); + mManager.pushDynamicShortcut(s10); // Add a long lived shortcut to the end of the list. + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s10"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.pushDynamicShortcut(makeShortcut("s11")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s10", "s11"); + } + + public void testShortcutChangeCallback_disableShortcuts() { + updatePackageVersion(CALLING_PACKAGE_1, 1); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.disableShortcuts(list("s2")); + }); + + mTestLooper.dispatchAll(); + + verify(callback, times(0)).onShortcutsAddedOrUpdated(any(), any(), any()); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2"); + } + + public void testShortcutChangeCallback_disableShortcuts_pinnedAndCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts( + list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeShortcut("s3")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.disableShortcuts(list("s1", "s2", "s3")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_enableShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts( + list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeShortcut("s3")))); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.disableShortcuts(list("s1", "s2", "s3")); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.enableShortcuts(list("s1", "s2", "s3")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + verify(callback, times(0)).onShortcutsRemoved(any(), any(), any()); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + } + + public void testShortcutChangeCallback_removeDynamicShortcuts() { + updatePackageVersion(CALLING_PACKAGE_1, 1); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("s2")); + }); + + mTestLooper.dispatchAll(); + + verify(callback, times(0)).onShortcutsAddedOrUpdated(any(), any(), any()); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2"); + } + + public void testShortcutChangeCallback_removeDynamicShortcuts_pinnedAndCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), + makeLongLivedShortcut("s2"), makeShortcut("s3"), makeShortcut("s4")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("s1", "s2", "s3")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_removeAllDynamicShortcuts() { + updatePackageVersion(CALLING_PACKAGE_1, 1); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(makeShortcuts("s1", "s2"))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeAllDynamicShortcuts(); + }); + + mTestLooper.dispatchAll(); + + verify(callback, times(0)).onShortcutsAddedOrUpdated(any(), any(), any()); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2"); + } + + public void testShortcutChangeCallback_removeAllDynamicShortcuts_pinnedAndCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts( + list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeShortcut("s3")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeAllDynamicShortcuts(); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s2", "s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1"); + } + + public void testShortcutChangeCallback_removeLongLivedShortcuts_notCached() { + updatePackageVersion(CALLING_PACKAGE_1, 1); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), + makeLongLivedShortcut("s2"), makeShortcut("s3")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeLongLivedShortcuts(list("s1", "s2")); + }); + + mTestLooper.dispatchAll(); + + verify(callback, times(0)).onShortcutsAddedOrUpdated(any(), any(), any()); + + ArgumentCaptor shortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), shortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(shortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2"); + } + + public void testShortcutChangeCallback_removeLongLivedShortcuts_pinnedAndCached() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), + makeLongLivedShortcut("s2"), makeShortcut("s3"), makeShortcut("s4")))); + }); + + ShortcutChangeCallback callback = mock(ShortcutChangeCallback.class); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.cacheShortcuts(CALLING_PACKAGE_1, list("s2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s3"), HANDLE_USER_0); + mLauncherApps.registerShortcutChangeCallback(callback, QUERY_MATCH_ALL, + mTestLooper.getNewExecutor()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeLongLivedShortcuts(list("s1", "s2", "s3")); + }); + + mTestLooper.dispatchAll(); + + ArgumentCaptor changedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsAddedOrUpdated( + eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0)); + + ArgumentCaptor removedShortcuts = ArgumentCaptor.forClass(List.class); + verify(callback, times(1)).onShortcutsRemoved( + eq(CALLING_PACKAGE_1), removedShortcuts.capture(), eq(HANDLE_USER_0)); + + assertWith(changedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s3"); + + assertWith(removedShortcuts.getValue()) + .areAllWithKeyFieldsOnly() + .haveIds("s1", "s2"); + } + + private static ShortcutQuery createShortcutQuery(int queryFlags) { + ShortcutQuery q = new ShortcutQuery(); + return q.setQueryFlags(ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED); + } +}