Merge "Adds unit tests for ShortcutChangeCallback APIs" into rvc-dev

This commit is contained in:
Mehdi Alizadeh
2020-04-27 19:34:25 +00:00
committed by Android (Google) Code Review
6 changed files with 1067 additions and 166 deletions

View File

@@ -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)
*
* <p>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)
*
* <p>Only the applications that are allowed to access the shortcut information,
* as defined in {@link #hasShortcutHostPermission()}, will receive it.

View File

@@ -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<String> mShortcutIds;
final List<LocusId> mLocusIds;
final ComponentName mActivity;
final int mQueryFlags;
final UserHandle mCallbackUser;
QueryInfo(long changedSince, String packageName, List<String> shortcutIds,
List<LocusId> 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<ShortcutQueryWrapper, UserHandle> cookie =
(Pair<ShortcutQueryWrapper, UserHandle>)
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<ShortcutInfo> matchedList =
filterShortcutsByQuery(packageName, shortcuts, query);
List<ShortcutInfo> 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<ShortcutInfo> filterShortcutsByQuery(String packageName,
List<ShortcutInfo> shortcuts, QueryInfo query) {
if (query.mPackage != null && query.mPackage != packageName) {
List<ShortcutInfo> shortcuts, ShortcutQueryWrapper query,
boolean shortcutsRemoved) {
final long changedSince = query.getChangedSince();
final String queryPackage = query.getPackage();
final List<String> shortcutIds = query.getShortcutIds();
final List<LocusId> locusIds = query.getLocusIds();
final ComponentName activity = query.getActivity();
final int flags = query.getQueryFlags();
if (queryPackage != null && !queryPackage.equals(packageName)) {
return null;
}
List<ShortcutInfo> 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);
}
}

View File

@@ -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<ShortcutInfo> 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<ShortcutInfo> removeOrphans() {
ArrayList<String> removeList = null; // Lazily initialize.
List<ShortcutInfo> removedShortcuts = null;
List<ShortcutInfo> 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.

View File

@@ -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();

View File

@@ -1666,11 +1666,10 @@ public class ShortcutService extends IShortcutService.Stub {
* - Write to file
*/
void packageShortcutsChanged(@NonNull String packageName, @UserIdInt int userId,
@Nullable List<ShortcutInfo> addedOrUpdatedShortcuts,
@Nullable List<ShortcutInfo> removedShortcuts) {
@Nullable final List<ShortcutInfo> changedShortcuts,
@Nullable final List<ShortcutInfo> 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<ShortcutInfo> addedOrUpdatedShortcuts,
@Nullable List<ShortcutInfo> removedShortcuts) {
@Nullable final List<ShortcutInfo> changedShortcuts,
@Nullable final List<ShortcutInfo> removedShortcuts) {
final List<ShortcutInfo> changedList = removeNonKeyFields(changedShortcuts);
final List<ShortcutInfo> 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<ShortcutInfo> removeNonKeyFields(@Nullable List<ShortcutInfo> shortcutInfos) {
if (CollectionUtils.isEmpty(shortcutInfos)) {
return shortcutInfos;
}
final int size = shortcutInfos.size();
List<ShortcutInfo> 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<ShortcutInfo> changedShortcuts = null;
List<ShortcutInfo> 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<ShortcutInfo> 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<ShortcutInfo> changedShortcuts = new ArrayList<>();
List<ShortcutInfo> 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<ShortcutInfo> changedShortcuts = null;
List<ShortcutInfo> 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<ShortcutInfo> changedShortcuts = null;
List<ShortcutInfo> changedShortcuts = new ArrayList<>();
List<ShortcutInfo> 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<ShortcutInfo> changedShortcuts = null;
List<ShortcutInfo> 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<String> 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<ShortcutInfo> prepareChangedShortcuts(ArraySet<String> changedIds,
ArraySet<String> newIds, List<ShortcutInfo> 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<String> 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<ShortcutInfo> result = new ArrayList<>();
ps.findAll(result, (ShortcutInfo si) -> resultIds.contains(si.getId()),
ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
return result;
}
private List<ShortcutInfo> prepareChangedShortcuts(List<ShortcutInfo> changedList,
List<ShortcutInfo> newList, List<ShortcutInfo> deletedList, final ShortcutPackage ps) {
ArraySet<String> changedIds = new ArraySet<>();
addShortcutIdsToSet(changedIds, changedList);
ArraySet<String> newIds = new ArraySet<>();
addShortcutIdsToSet(newIds, newList);
return prepareChangedShortcuts(changedIds, newIds, deletedList, ps);
}
private void addShortcutIdsToSet(ArraySet<String> ids, List<ShortcutInfo> shortcuts) {
if (CollectionUtils.isEmpty(shortcuts)) {
return;
}
final int size = shortcuts.size();
for (int i = 0; i < size; i++) {
ids.add(shortcuts.get(i).getId());
}
}
}

View File

@@ -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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> 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<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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<List> 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<List> changedShortcuts = ArgumentCaptor.forClass(List.class);
verify(callback, times(1)).onShortcutsAddedOrUpdated(
eq(CALLING_PACKAGE_1), changedShortcuts.capture(), eq(HANDLE_USER_0));
ArgumentCaptor<List> 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);
}
}