diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 094b89d4a4467..896fa43cd11b1 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -62,6 +62,13 @@ public final class ShortcutInfo implements Parcelable { private static final String ANDROID_PACKAGE_NAME = "android"; + private static final int IMPLICIT_RANK_MASK = 0x7fffffff; + + private static final int RANK_CHANGED_BIT = ~IMPLICIT_RANK_MASK; + + /** @hide */ + public static final int RANK_NOT_SET = Integer.MAX_VALUE; + /** @hide */ public static final int FLAG_DYNAMIC = 1 << 0; @@ -193,6 +200,15 @@ public final class ShortcutInfo implements Parcelable { private int mRank; + /** + * Internally used for auto-rank-adjustment. + * + * RANK_CHANGED_BIT is used to denote that the rank of a shortcut is changing. + * The rest of the bits are used to denote the order in which shortcuts are passed to + * APIs, which is used to preserve the argument order when ranks are tie. + */ + private int mImplicitRank; + @Nullable private PersistableBundle mExtras; @@ -544,7 +560,8 @@ public final class ShortcutInfo implements Parcelable { /** * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information - * will be overwritten. The timestamp will be updated. + * will be overwritten. The timestamp will *not* be updated to be consistent with other + * setters (and also the clock is not injectable in this file). * * - Flags will not change * - mBitmapPath will not change @@ -603,14 +620,12 @@ public final class ShortcutInfo implements Parcelable { mIntent = source.mIntent; mIntentPersistableExtras = source.mIntentPersistableExtras; } - if (source.mRank != 0) { + if (source.mRank != RANK_NOT_SET) { mRank = source.mRank; } if (source.mExtras != null) { mExtras = source.mExtras; } - - updateTimestamp(); } /** @@ -665,7 +680,7 @@ public final class ShortcutInfo implements Parcelable { private Intent mIntent; - private int mRank; + private int mRank = RANK_NOT_SET; private PersistableBundle mExtras; @@ -825,15 +840,18 @@ public final class ShortcutInfo implements Parcelable { @NonNull public Builder setIntent(@NonNull Intent intent) { mIntent = Preconditions.checkNotNull(intent, "intent"); - Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set."); + Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set"); return this; } /** - * TODO javadoc. + * "Rank" of a shortcut, which is a non-negative value that's used by the launcher app + * to sort shortcuts. */ @NonNull public Builder setRank(int rank) { + Preconditions.checkArgument((0 <= rank), + "Rank cannot be negative or bigger than MAX_RANK"); mRank = rank; return this; } @@ -1014,12 +1032,57 @@ public final class ShortcutInfo implements Parcelable { } /** - * TODO Javadoc + * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each + * {@link #getActivity} for each of the two kinds, dynamic shortcuts and manifest shortcuts. + * + *

Because manifest shortcuts and dynamic shortcuts have overlapping ranks, + * when a launcher application shows shortcuts for an activity, it should first show + * the manifest shortcuts followed by the dynamic shortcuts. Within each of those categories, + * shortcuts should be sorted by rank in ascending order. + * + *

"Floating" shortcuts (i.e. shortcuts that are neither dynamic nor manifest) will all + * have rank 0, because there's no sorting for them. */ public int getRank() { return mRank; } + /** @hide */ + public boolean hasRank() { + return mRank != RANK_NOT_SET; + } + + /** @hide */ + public void setRank(int rank) { + mRank = rank; + } + + /** @hide */ + public void clearImplicitRankAndRankChangedFlag() { + mImplicitRank = 0; + } + + /** @hide */ + public void setImplicitRank(int rank) { + // Make sure to keep RANK_CHANGED_BIT. + mImplicitRank = (mImplicitRank & RANK_CHANGED_BIT) | (rank & IMPLICIT_RANK_MASK); + } + + /** @hide */ + public int getImplicitRank() { + return mImplicitRank & IMPLICIT_RANK_MASK; + } + + /** @hide */ + public void setRankChanged() { + mImplicitRank |= RANK_CHANGED_BIT; + } + + /** @hide */ + public boolean isRankChanged() { + return (mImplicitRank & RANK_CHANGED_BIT) != 0; + } + /** * Optional values that application can set. */ diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 934545ad24618..ace14acda16df 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -19,10 +19,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.os.PersistableBundle; @@ -51,8 +49,6 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; -import sun.misc.Resource; - /** * Package information used by {@link ShortcutService}. * User information used by {@link ShortcutService}. @@ -63,6 +59,7 @@ import sun.misc.Resource; */ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; + private static final String TAG_VERIFY = ShortcutService.TAG + ".verify"; static final String TAG_ROOT = "package"; private static final String TAG_INTENT_EXTRAS = "intent-extras"; @@ -303,12 +300,17 @@ class ShortcutPackage extends ShortcutPackageItem { * Remove all dynamic shortcuts. */ public void deleteAllDynamicShortcuts() { + final long now = mShortcutUser.mService.injectCurrentTimeMillis(); + boolean changed = false; for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (si.isDynamic()) { changed = true; + + si.setTimestamp(now); si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + si.setRank(0); // It may still be pinned, so clear the rank. } } if (changed) { @@ -356,10 +358,14 @@ class ShortcutPackage extends ShortcutPackageItem { ensureNotImmutable(oldShortcut); } if (oldShortcut.isPinned()) { + + oldShortcut.setRank(0); oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); if (disable) { oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); } + oldShortcut.setTimestamp(mShortcutUser.mService.injectCurrentTimeMillis()); + return oldShortcut; } else { deleteShortcutInner(shortcutId); @@ -829,7 +835,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (!a.isManifestShortcut() && b.isManifestShortcut()) { return 1; } - return a.getRank() - b.getRank(); + return Integer.compare(a.getRank(), b.getRank()); }; /** @@ -837,8 +843,6 @@ class ShortcutPackage extends ShortcutPackageItem { * contain "floating" shortcuts because they don't belong on any activities. */ private ArrayMap> sortShortcutsToActivities() { - final int maxShortcuts = mShortcutUser.mService.getMaxActivityShortcuts(); - final ArrayMap> activitiesToShortcuts = new ArrayMap<>(); for (int i = mShortcuts.size() - 1; i >= 0; i--) { @@ -851,7 +855,7 @@ class ShortcutPackage extends ShortcutPackageItem { ArrayList list = activitiesToShortcuts.get(activity); if (list == null) { - list = new ArrayList<>(maxShortcuts * 2); + list = new ArrayList<>(); activitiesToShortcuts.put(activity, list); } list.add(si); @@ -976,6 +980,96 @@ class ShortcutPackage extends ShortcutPackageItem { } } + /** Clears the implicit ranks for all shortcuts. */ + public void clearAllImplicitRanks() { + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + si.clearImplicitRankAndRankChangedFlag(); + } + } + + /** + * Used to sort shortcuts for rank auto-adjusting. + */ + final Comparator mShortcutRankComparator = (ShortcutInfo a, ShortcutInfo b) -> { + // First, sort by rank. + int ret = Integer.compare(a.getRank(), b.getRank()); + if (ret != 0) { + return ret; + } + // When ranks are tie, then prioritize the ones that have just been assigned new ranks. + // e.g. when there are 3 shortcuts, "s1" "s2" and "s3" with rank 0, 1, 2 respectively, + // adding a shortcut "s4" with rank 1 will "insert" it between "s1" and "s2", because + // "s2" and "s4" have the same rank 1 but s4 has isRankChanged() set. + // Similarly, updating s3's rank to 1 will insert it between s1 and s2. + if (a.isRankChanged() != b.isRankChanged()) { + return a.isRankChanged() ? -1 : 1; + } + // If they're still tie, sort by implicit rank -- i.e. preserve the order in which + // they're passed to the API. + ret = Integer.compare(a.getImplicitRank(), b.getImplicitRank()); + if (ret != 0) { + return ret; + } + // If they're stil tie, just sort by their IDs. + // This may happen with updateShortcuts() -- see + // the testUpdateShortcuts_noManifestShortcuts() test. + return a.getId().compareTo(b.getId()); + }; + + /** + * Re-calculate the ranks for all shortcuts. + */ + public void adjustRanks() { + final ShortcutService s = mShortcutUser.mService; + final long now = s.injectCurrentTimeMillis(); + + // First, clear ranks for floating shortcuts. + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + if (si.isFloating()) { + if (si.getRank() != 0) { + si.setTimestamp(now); + si.setRank(0); + } + } + } + + // Then adjust ranks. Ranks are unique for each activity, so we first need to sort + // shortcuts to each activity. + // Then sort the shortcuts within each activity with mShortcutRankComparator, and + // assign ranks from 0. + final ArrayMap> all = + sortShortcutsToActivities(); + for (int outer = all.size() - 1; outer >= 0; outer--) { // For each activity. + final ArrayList list = all.valueAt(outer); + + // Sort by ranks and other signals. + Collections.sort(list, mShortcutRankComparator); + + int rank = 0; + + final int size = list.size(); + for (int i = 0; i < size; i++) { + final ShortcutInfo si = list.get(i); + if (si.isManifestShortcut()) { + // Don't adjust ranks for manifest shortcuts. + continue; + } + // At this point, it must be dynamic. + if (!si.isDynamic()) { + s.wtf("Non-dynamic shortcut found."); + continue; + } + final int thisRank = rank++; + if (si.getRank() != thisRank) { + si.setTimestamp(now); + si.setRank(thisRank); + } + } + } + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(); @@ -1087,7 +1181,6 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_DISABLED_MESSAGE_RES_NAME, si.getDisabledMessageResName()); ShortcutService.writeAttr(out, ATTR_INTENT, si.getIntentNoExtras()); - ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); ShortcutService.writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp()); if (forBackup) { @@ -1097,6 +1190,10 @@ class ShortcutPackage extends ShortcutPackageItem { ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES | ShortcutInfo.FLAG_DYNAMIC)); } else { + // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored + // as dynamic. + ShortcutService.writeAttr(out, ATTR_RANK, si.getRank()); + ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags()); ShortcutService.writeAttr(out, ATTR_ICON_RES_ID, si.getIconResourceId()); ShortcutService.writeAttr(out, ATTR_ICON_RES_NAME, si.getIconResName()); @@ -1272,35 +1369,74 @@ class ShortcutPackage extends ShortcutPackageItem { sortShortcutsToActivities(); // Make sure each activity won't have more than max shortcuts. - for (int i = all.size() - 1; i >= 0; i--) { - if (all.valueAt(i).size() > mShortcutUser.mService.getMaxActivityShortcuts()) { + for (int outer = all.size() - 1; outer >= 0; outer--) { + final ArrayList list = all.valueAt(outer); + if (list.size() > mShortcutUser.mService.getMaxActivityShortcuts()) { failed = true; - Log.e(TAG, "Package " + getPackageName() + ": activity " + all.keyAt(i) - + " has " + all.valueAt(i).size() + " shortcuts."); + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": activity " + all.keyAt(outer) + + " has " + all.valueAt(outer).size() + " shortcuts."); } + + // Sort by rank. + Collections.sort(list, (a, b) -> Integer.compare(a.getRank(), b.getRank())); + + // Split into two arrays for each kind. + final ArrayList dynamicList = new ArrayList<>(list); + dynamicList.removeIf((si) -> !si.isDynamic()); + + final ArrayList manifestList = new ArrayList<>(list); + dynamicList.removeIf((si) -> !si.isManifestShortcut()); + + verifyRanksSequential(dynamicList); + verifyRanksSequential(manifestList); } + // Verify each shortcut's status. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (!(si.isManifestShortcut() || si.isDynamic() || si.isPinned())) { failed = true; - Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not manifest, dynamic or pinned."); } + if (si.isManifestShortcut() && si.isDynamic()) { + failed = true; + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + + " is both dynamic and manifest at the same time."); + } if (si.getActivity() == null) { failed = true; - Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " has null activity."); } if ((si.isDynamic() || si.isManifestShortcut()) && !si.isEnabled()) { failed = true; - Log.e(TAG, "Package " + getPackageName() + ": shortcut " + si.getId() + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " is not floating, but is disabled."); } + if (si.isFloating() && si.getRank() != 0) { + failed = true; + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + + " is floating, but has rank=" + si.getRank()); + } } if (failed) { throw new IllegalStateException("See logcat for errors"); } } + + private boolean verifyRanksSequential(List list) { + boolean failed = false; + + for (int i = 0; i < list.size(); i++) { + final ShortcutInfo si = list.get(i); + if (si.getRank() != i) { + failed = true; + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + + " rank=" + si.getRank() + " but expected to be "+ i); + } + } + return failed; + } } diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java index 470d4afe64448..ec19927ad75a8 100644 --- a/services/core/java/com/android/server/pm/ShortcutParser.java +++ b/services/core/java/com/android/server/pm/ShortcutParser.java @@ -103,7 +103,7 @@ public class ShortcutParser { } if (depth == 2 && TAG_SHORTCUT.equals(tag)) { final ShortcutInfo si = parseShortcutAttributes( - service, attrs, packageName, activity, userId, rank++); + service, attrs, packageName, activity, userId, rank); if (ShortcutService.DEBUG) { Slog.d(TAG, "Shortcut=" + si); } @@ -128,6 +128,7 @@ public class ShortcutParser { } result.add(si); numShortcuts++; + rank++; } continue; } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 57694028d86dc..16191687770c4 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -114,7 +114,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Predicate; @@ -223,7 +222,7 @@ public class ShortcutService extends IShortcutService.Stub { String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram"; /** - * Key name for the max dynamic shortcuts per app. (int) + * Key name for the max dynamic shortcuts per activity. (int) */ String KEY_MAX_SHORTCUTS = "max_shortcuts"; @@ -1479,6 +1478,12 @@ public class ShortcutService extends IShortcutService.Stub { return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); } + private void assignImplicitRanks(List shortcuts) { + for (int i = shortcuts.size() - 1; i >= 0; i--) { + shortcuts.get(i).setImplicitRank(i); + } + } + // === APIs === @Override @@ -1501,10 +1506,9 @@ public class ShortcutService extends IShortcutService.Stub { return false; } - // Validate the shortcuts. - for (int i = 0; i < size; i++) { - fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); - } + // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). + ps.clearAllImplicitRanks(); + assignImplicitRanks(newShortcuts); // First, remove all un-pinned; dynamic shortcuts ps.deleteAllDynamicShortcuts(); @@ -1514,6 +1518,9 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutInfo newShortcut = newShortcuts.get(i); ps.addOrUpdateDynamicShortcut(newShortcut); } + + // Lastly, adjust the ranks. + ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); @@ -1542,41 +1549,58 @@ public class ShortcutService extends IShortcutService.Stub { return false; } + // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). + ps.clearAllImplicitRanks(); + assignImplicitRanks(newShortcuts); + for (int i = 0; i < size; i++) { final ShortcutInfo source = newShortcuts.get(i); fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); final ShortcutInfo target = ps.findShortcutById(source.getId()); - if (target != null) { - if (target.isEnabled() != source.isEnabled()) { - Slog.w(TAG, - "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); - } + if (target == null) { + continue; + } - final boolean replacingIcon = (source.getIcon() != null); - if (replacingIcon) { - removeIcon(userId, target); - } + if (target.isEnabled() != source.isEnabled()) { + Slog.w(TAG, + "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); + } - if (source.getActivity() != null && - !source.getActivity().equals(target.getActivity())) { - // TODO When activity is changing, check the dynamic count. - } + // 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()) { + target.setRankChanged(); + target.setImplicitRank(source.getImplicitRank()); + } - // Note copyNonNullFieldsFrom() does the "updatable with?" check too. - target.copyNonNullFieldsFrom(source); + final boolean replacingIcon = (source.getIcon() != null); + if (replacingIcon) { + removeIcon(userId, target); + } - if (replacingIcon) { - saveIconAndFixUpShortcut(userId, target); - } + if (source.getActivity() != null && + !source.getActivity().equals(target.getActivity())) { + // TODO When activity is changing, check the dynamic count. + } - // When we're updating any resource related fields, re-extract the res names and - // the values. - if (replacingIcon || source.hasStringResources()) { - fixUpShortcutResourceNamesAndValues(target); - } + // Note copyNonNullFieldsFrom() does the "updatable with?" check too. + target.copyNonNullFieldsFrom(source); + target.setTimestamp(injectCurrentTimeMillis()); + + if (replacingIcon) { + saveIconAndFixUpShortcut(userId, target); + } + + // When we're updating any resource related fields, re-extract the res names and + // the values. + if (replacingIcon || source.hasStringResources()) { + fixUpShortcutResourceNamesAndValues(target); } } + + // Lastly, adjust the ranks. + ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); @@ -1600,6 +1624,10 @@ public class ShortcutService extends IShortcutService.Stub { ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); + // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). + ps.clearAllImplicitRanks(); + assignImplicitRanks(newShortcuts); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1610,9 +1638,16 @@ public class ShortcutService extends IShortcutService.Stub { // Validate the shortcut. fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); + // When ranks are changing, we need to insert between ranks, so set the + // "rank changed" flag. + newShortcut.setRankChanged(); + // Add it. ps.addOrUpdateDynamicShortcut(newShortcut); } + + // Lastly, adjust the ranks. + ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); @@ -1637,6 +1672,9 @@ public class ShortcutService extends IShortcutService.Stub { disabledMessage, disabledMessageResId, /* overrideImmutable=*/ false); } + + // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. + ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); @@ -1677,6 +1715,9 @@ public class ShortcutService extends IShortcutService.Stub { ps.deleteDynamicWithId( Preconditions.checkStringNotEmpty((String) shortcutIds.get(i))); } + + // We may have removed dynamic shortcuts which may have left a gap, so adjust the ranks. + ps.adjustRanks(); } packageShortcutsChanged(packageName, userId); @@ -2328,6 +2369,7 @@ public class ShortcutService extends IShortcutService.Stub { } finally { logDurationStat(Stats.CHECK_PACKAGE_CHANGES, start); } + verifyStates(); } private void handlePackageAdded(String packageName, @UserIdInt int userId) { @@ -2339,6 +2381,7 @@ public class ShortcutService extends IShortcutService.Stub { user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.handlePackageAddedOrUpdated(packageName); } + verifyStates(); } private void handlePackageUpdateFinished(String packageName, @UserIdInt int userId) { @@ -2354,6 +2397,7 @@ public class ShortcutService extends IShortcutService.Stub { user.handlePackageAddedOrUpdated(packageName); } } + verifyStates(); } private void handlePackageRemoved(String packageName, @UserIdInt int packageUserId) { @@ -2362,6 +2406,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); + + verifyStates(); } private void handlePackageDataCleared(String packageName, int packageUserId) { @@ -2370,6 +2416,8 @@ public class ShortcutService extends IShortcutService.Stub { packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId); + + verifyStates(); } // === PackageManager interaction === diff --git a/services/tests/servicestests/res/xml/shortcut_5_reverse.xml b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml new file mode 100644 index 0000000000000..3d6eb222ce5b4 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 2f11967cdb020..69152d405ac05 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -742,6 +742,9 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { if (mService != null) { // Flush all the unsaved data from the previous instance. mService.saveDirtyInfo(); + + // Make sure everything is consistent. + mService.verifyStates(); } LocalServices.removeServiceForTest(ShortcutServiceInternal.class); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index fe2d1eca1cf1f..fc1ebbec86116 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -45,6 +45,7 @@ import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet; @@ -71,7 +72,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.ShortcutQuery; import android.content.pm.ShortcutInfo; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; @@ -79,7 +79,6 @@ import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Handler; import android.os.Looper; -import android.os.Process; import android.os.UserHandle; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; @@ -969,23 +968,34 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // Set up shortcuts. setCaller(CALLING_PACKAGE_1); - final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 5000); - final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 1000); + final ShortcutInfo s1_1 = makeShortcut("s1"); + final ShortcutInfo s1_2 = makeShortcut("s2"); assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2))); + // Because setDynamicShortcuts will update the timestamps when ranks are changing, + // we explicitly set timestamps here. + getCallerShortcut("s1").setTimestamp(5000); + getCallerShortcut("s2").setTimestamp(1000); + setCaller(CALLING_PACKAGE_2); - final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); - final ShortcutInfo s2_3 = makeShortcutWithTimestampWithActivity("s3", 3000, + final ShortcutInfo s2_2 = makeShortcut("s2"); + final ShortcutInfo s2_3 = makeShortcutWithActivity("s3", makeComponent(ShortcutActivity2.class)); - final ShortcutInfo s2_4 = makeShortcutWithTimestampWithActivity("s4", 500, + final ShortcutInfo s2_4 = makeShortcutWithActivity("s4", makeComponent(ShortcutActivity.class)); assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4))); + getCallerShortcut("s2").setTimestamp(1500); + getCallerShortcut("s3").setTimestamp(3000); + getCallerShortcut("s4").setTimestamp(500); + setCaller(CALLING_PACKAGE_3); - final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s3", START_TIME + 5000); + final ShortcutInfo s3_2 = makeShortcut("s3"); assertTrue(mManager.setDynamicShortcuts(list(s3_2))); + getCallerShortcut("s3").setTimestamp(START_TIME + 5000); + setCaller(LAUNCHER_1); // Get dynamic @@ -4606,7 +4616,6 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertNull(mService.getPackageShortcutForTest(LAUNCHER_1, USER_0)); } - public void testManifestShortcut_publishOnBroadcast() { // First, no packages are installed. uninstallPackage(USER_0, CALLING_PACKAGE_1); @@ -4677,6 +4686,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( mManager.getManifestShortcuts()))), "ms1", "ms2", "ms3", "ms4", "ms5"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1", "ms2", "ms3", "ms4", "ms5"); assertEmpty(mManager.getPinnedShortcuts()); }); @@ -4710,6 +4723,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( mManager.getManifestShortcuts()))), "ms1", "ms2"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1", "ms2"); assertShortcutIds(assertAllImmutable(assertAllPinned( mManager.getPinnedShortcuts())), "ms2", "ms3"); @@ -4733,6 +4750,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( mManager.getManifestShortcuts()))), "ms1", "ms2"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1", "ms2"); assertEmpty(mManager.getPinnedShortcuts()); }); @@ -4741,6 +4762,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( mManager.getManifestShortcuts()))), "ms1", "ms2"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1", "ms2"); assertShortcutIds(assertAllImmutable(assertAllPinned( mManager.getPinnedShortcuts())), "ms2", "ms3"); @@ -4749,10 +4774,43 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertAllDisabled(list(getCallerShortcut("ms3"))); }); + // Multiple activities. + // Add shortcuts on activity 2 for package 2. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_5_alt); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName()), + R.xml.shortcut_5_reverse); + + updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_2, USER_0)); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5", + "ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt"); + + // Make sure they have the correct ranks, regardless of their ID's alphabetical order. + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName())) + .haveRanksInOrder("ms5", "ms4", "ms3", "ms2", "ms1"); + }); + // Package 2 now has no manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), R.xml.shortcut_0); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity2.class.getName()), + R.xml.shortcut_0); updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1); mService.mPackageMonitor.onReceive(getTestContext(), genPackageAddIntent(CALLING_PACKAGE_2, USER_0)); @@ -5128,7 +5186,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // ShortcutActivity2 has two shortcuts, ms1 and ms2. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), - R.xml.shortcut_2); + R.xml.shortcut_5); updatePackageVersion(CALLING_PACKAGE_1, 1); mService.mPackageMonitor.onReceive(getTestContext(), genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); @@ -5136,13 +5194,14 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( mManager.getManifestShortcuts()))), - "ms1", "ms2"); + "ms1", "ms2", "ms3", "ms4", "ms5"); // ms1 should belong to ShortcutActivity. ShortcutInfo si = getCallerShortcut("ms1"); assertEquals(R.string.shortcut_title1, si.getTitleResId()); assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), si.getActivity()); + assertEquals(0, si.getRank()); // ms2 should belong to ShortcutActivity*2*. si = getCallerShortcut("ms2"); @@ -5150,8 +5209,18 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), si.getActivity()); + // Also check the ranks + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName())) + .haveRanksInOrder("ms1"); + assertWith(getCallerShortcuts()).selectManifest() + .selectByActivity( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName())) + .haveRanksInOrder("ms2", "ms3", "ms4", "ms5"); + // Make sure there's no other dangling shortcuts. - assertShortcutIds(getCallerShortcuts(), "ms1", "ms2"); + assertShortcutIds(getCallerShortcuts(), "ms1", "ms2", "ms3", "ms4", "ms5"); }); } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index ea44462c94f34..8e32d6a6afb2b 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -533,6 +533,7 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") .setActivity(new ComponentName("x", "y")).build()); assertEquals("text", si.getText()); + assertEquals(123, si.getRank()); assertEquals(new ComponentName("x", "y"), si.getActivity()); si = sorig.clone(/* flags=*/ 0); @@ -627,16 +628,6 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb2).build()); assertEquals("text", si.getText()); assertEquals(99, si.getExtras().getInt("x")); - - // Make sure the timestamp gets updated too. - - final long timestamp = si.getLastChangedTimestamp(); - Thread.sleep(2); - - si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") - .setTitle("xyz").build()); - - assertTrue(si.getLastChangedTimestamp() > timestamp); } public void testShortcutInfoCopyNonNullFieldsFrom_resId() throws InterruptedException { @@ -764,16 +755,6 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb2).build()); assertEquals(11, si.getTextResId()); assertEquals(99, si.getExtras().getInt("x")); - - // Make sure the timestamp gets updated too. - - final long timestamp = si.getLastChangedTimestamp(); - Thread.sleep(2); - - si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") - .setTitle("xyz").build()); - - assertTrue(si.getLastChangedTimestamp() > timestamp); } public void testShortcutInfoSaveAndLoad() throws InterruptedException { @@ -797,7 +778,15 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb) .build(); - mManager.addDynamicShortcuts(list(sorig)); + ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext) + .setId("id2") + .setTitle("x") + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) + .setRank(456) + .build(); + + mManager.addDynamicShortcuts(list(sorig, sorig2)); Thread.sleep(2); final long now = System.currentTimeMillis(); @@ -822,7 +811,7 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories()); assertEquals("action", si.getIntent().getAction()); assertEquals("val", si.getIntent().getStringExtra("key")); - assertEquals(123, si.getRank()); + assertEquals(0, si.getRank()); assertEquals(1, si.getExtras().getInt("k")); assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_FILE @@ -830,6 +819,11 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertNotNull(si.getBitmapPath()); // Something should be set. assertEquals(0, si.getIconResourceId()); assertTrue(si.getLastChangedTimestamp() < now); + + // Make sure ranks are saved too. Because of the auto-adjusting, we need two shortcuts + // to test it. + si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_10); + assertEquals(1, si.getRank()); } public void testShortcutInfoSaveAndLoad_resId() throws InterruptedException { @@ -852,7 +846,15 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb) .build(); - mManager.addDynamicShortcuts(list(sorig)); + ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext) + .setId("id2") + .setTitle("x") + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) + .setRank(456) + .build(); + + mManager.addDynamicShortcuts(list(sorig, sorig2)); Thread.sleep(2); final long now = System.currentTimeMillis(); @@ -880,7 +882,7 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories()); assertEquals("action", si.getIntent().getAction()); assertEquals("val", si.getIntent().getStringExtra("key")); - assertEquals(123, si.getRank()); + assertEquals(0, si.getRank()); assertEquals(1, si.getExtras().getInt("k")); assertEquals(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_HAS_ICON_RES @@ -888,6 +890,11 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertNull(si.getBitmapPath()); assertEquals(R.drawable.black_32x32, si.getIconResourceId()); assertTrue(si.getLastChangedTimestamp() < now); + + // Make sure ranks are saved too. Because of the auto-adjusting, we need two shortcuts + // to test it. + si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_10); + assertEquals(1, si.getRank()); } public void testShortcutInfoSaveAndLoad_forBackup() { @@ -911,11 +918,19 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb) .build(); - mManager.addDynamicShortcuts(list(sorig)); + ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext) + .setId("id2") + .setTitle("x") + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) + .setRank(456) + .build(); + + mManager.addDynamicShortcuts(list(sorig, sorig2)); // Dynamic shortcuts won't be backed up, so we need to pin it. setCaller(LAUNCHER_1, USER_0); - mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id", "id2"), HANDLE_USER_0); // Do backup & restore. backupAndRestore(); @@ -935,12 +950,16 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories()); assertEquals("action", si.getIntent().getAction()); assertEquals("val", si.getIntent().getStringExtra("key")); - assertEquals(123, si.getRank()); + assertEquals(0, si.getRank()); assertEquals(1, si.getExtras().getInt("k")); assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags()); assertNull(si.getBitmapPath()); // No icon. assertEquals(0, si.getIconResourceId()); + + // Note when restored from backup, it's no longer dynamic, so shouldn't have a rank. + si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_0); + assertEquals(0, si.getRank()); } public void testShortcutInfoSaveAndLoad_forBackup_resId() { @@ -963,11 +982,19 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setExtras(pb) .build(); - mManager.addDynamicShortcuts(list(sorig)); + ShortcutInfo sorig2 = new ShortcutInfo.Builder(mClientContext) + .setId("id2") + .setTitle("x") + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) + .setRank(456) + .build(); + + mManager.addDynamicShortcuts(list(sorig, sorig2)); // Dynamic shortcuts won't be backed up, so we need to pin it. setCaller(LAUNCHER_1, USER_0); - mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("id", "id2"), HANDLE_USER_0); // Do backup & restore. backupAndRestore(); @@ -990,13 +1017,17 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz"), si.getCategories()); assertEquals("action", si.getIntent().getAction()); assertEquals("val", si.getIntent().getStringExtra("key")); - assertEquals(123, si.getRank()); + assertEquals(0, si.getRank()); assertEquals(1, si.getExtras().getInt("k")); assertEquals(ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_STRINGS_RESOLVED, si.getFlags()); assertNull(si.getBitmapPath()); // No icon. assertEquals(0, si.getIconResourceId()); assertEquals(null, si.getIconResName()); + + // Note when restored from backup, it's no longer dynamic, so shouldn't have a rank. + si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id2", USER_0); + assertEquals(0, si.getRank()); } public void testThrottling() { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java new file mode 100644 index 0000000000000..eb4db7a0f4af5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest3.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2016 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 android.content.ComponentName; +import android.content.pm.ShortcutInfo; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.frameworks.servicestests.R; +import com.android.server.pm.ShortcutService.ConfigConstants; + +/** + * Tests related to shortcut rank auto-adjustment. + */ +@SmallTest +public class ShortcutManagerTest3 extends BaseShortcutManagerTest { + + private static final String CALLING_PACKAGE = CALLING_PACKAGE_1; + + private static final ComponentName A1 = new ComponentName(CALLING_PACKAGE, + ShortcutActivity.class.getName()); + + private static final ComponentName A2 = new ComponentName(CALLING_PACKAGE, + ShortcutActivity2.class.getName()); + + private static final ComponentName A3 = new ComponentName(CALLING_PACKAGE, + ShortcutActivity3.class.getName()); + + private ShortcutInfo shortcut(String id, ComponentName activity, int rank) { + return makeShortcutWithActivityAndRank(id, activity, rank); + } + + private ShortcutInfo shortcut(String id, ComponentName activity) { + return makeShortcutWithActivityAndRank(id, activity, ShortcutInfo.RANK_NOT_SET); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // We don't need throttling during this test class, and also relax the max cap. + mService.updateConfigurationLocked( + ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999," + + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999" + ); + + setCaller(CALLING_PACKAGE, USER_0); + } + + private void publishManifestShortcuts(ComponentName activity, int resId) { + addManifestShortcutResource(activity, resId); + updatePackageVersion(CALLING_PACKAGE, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE, USER_0)); + } + + public void testSetDynamicShortcuts_noManifestShortcuts() { + mManager.setDynamicShortcuts(list( + shortcut("s1", A1) + )); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s1"); + + assertTrue(mManager.setDynamicShortcuts(list( + shortcut("s5", A1), + shortcut("s4", A1), + shortcut("s3", A1) + ))); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s4", "s3"); + + // RANK_NOT_SET is always the last. + assertTrue(mManager.setDynamicShortcuts(list( + shortcut("s5", A1), + shortcut("s4", A1, 5), + shortcut("s3", A1, 3), + shortcut("s2", A1) + ))); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s3", "s4", "s5", "s2"); + + // Same rank, preserve the argument order. + assertTrue(mManager.setDynamicShortcuts(list( + shortcut("s5", A1, 5), + shortcut("s4", A1, 0), + shortcut("s3", A1, 5) + ))); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s4", "s5", "s3"); + + // Multiple activities. + assertTrue(mManager.setDynamicShortcuts(list( + shortcut("s5", A1), + shortcut("s4", A2), + shortcut("s3", A3) + ))); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5"); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("s4"); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A3) + .haveRanksInOrder("s3"); + + assertTrue(mManager.setDynamicShortcuts(list( + shortcut("s5", A1, 5), + shortcut("s4", A1), + shortcut("s3", A1, 5), + shortcut("x5", A2, 5), + shortcut("x4", A2), + shortcut("x3", A2, 1) + ))); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s4"); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x5", "x4"); + + // Clear. Make sure it wouldn't lead to invalid internals state. + // (ShortcutService.verifyStates() will do so internally.) + assertTrue(mManager.setDynamicShortcuts(list())); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1).isEmpty(); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2).isEmpty(); + } + + private void runTestWithManifestShortcuts(Runnable r) { + publishManifestShortcuts(A1, R.xml.shortcut_5_alt); + publishManifestShortcuts(A2, R.xml.shortcut_1); + + assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A1) + .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt"); + + assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A2) + .haveRanksInOrder("ms1"); + + // Existence of manifest shortcuts shouldn't affect dynamic shortcut ranks, + // so running another test here should pass. + r.run(); + + // And dynamic shortcut tests shouldn't affect manifest shortcuts, so repeat the + // same check. + assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A1) + .haveRanksInOrder("ms1_alt", "ms2_alt", "ms3_alt", "ms4_alt", "ms5_alt"); + + assertWith(getCallerShortcuts()).selectManifest().selectByActivity(A2) + .haveRanksInOrder("ms1"); + } + + public void testSetDynamicShortcuts_withManifestShortcuts() { + runTestWithManifestShortcuts(() -> testSetDynamicShortcuts_noManifestShortcuts()); + } + + public void testAddDynamicShortcuts_noManifestShortcuts() { + mManager.addDynamicShortcuts(list( + shortcut("s1", A1) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s1"); + + //------------------------------------------------------ + long lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.addDynamicShortcuts(list( + shortcut("s5", A1, 0), + shortcut("s4", A1), + shortcut("s2", A1, 3), + shortcut("x1", A2), + shortcut("x3", A2, 2), + shortcut("x2", A2, 2), + shortcut("s3", A1, 0) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s1", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s5", "s3", "s1", "s2", "s4", "x3", "x2", "x1"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.addDynamicShortcuts(list( + shortcut("s1", A1, 1) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s1", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s1", "s3"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.addDynamicShortcuts(list( + shortcut("s1", A1, 1), + + // This is add, not update, so the following means s5 will have NO_RANK, + // which puts it at the end. + shortcut("s5", A1), + shortcut("s3", A1, 0), + + // s10 also has NO_RANK, so it'll be put at the end, even after "s5" as we preserve + // the argument order. + shortcut("s10", A1), + + // Note we're changing the activity for x2. + shortcut("x2", A1, 0), + shortcut("x10", A2) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s3", "x2", "s1", "s2", "s4", "s5", "s10"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x1", "x10"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s3", "x2", "s1", "s5", "s10", "x1", "x10"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + // Change the activities again. + mManager.addDynamicShortcuts(list( + shortcut("s1", A2), + shortcut("s2", A2, 999) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s3", "x2", "s4", "s5", "s10"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x1", "x10", "s2", "s1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s1", "s2", "s4", "s5", "s10"); + } + + public void testAddDynamicShortcuts_withManifestShortcuts() { + runTestWithManifestShortcuts(() -> testAddDynamicShortcuts_noManifestShortcuts()); + } + + public void testUpdateShortcuts_noManifestShortcuts() { + mManager.addDynamicShortcuts(list( + shortcut("s5", A1, 0), + shortcut("s4", A1), + shortcut("s2", A1, 3), + shortcut("x1", A2), + shortcut("x3", A2, 2), + shortcut("x2", A2, 2), + shortcut("s3", A1, 0) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + //------------------------------------------------------ + long lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.updateShortcuts(list()); + // Same order. + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .isEmpty(); + + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE, list("s2", "s4", "x2"), HANDLE_USER_0); + }); + // Still same order. + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.updateShortcuts(list( + shortcut("s4", A1, 1), + + // Rank not changing, should keep the same positions. + // c.f. in case of addDynamicShortcuts, this means "put them at the end". + shortcut("s3", A1), + shortcut("x2", A2) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s4", "s3", "s2"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s4", "s3", "s2", "x2"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.updateShortcuts(list( + shortcut("s4", A1, 0), + + // Change the activity without specifying a rank -> keep the same rank. + shortcut("s5", A2), + + // Change the activity without specifying a rank -> assign a new rank. + shortcut("x2", A1, 2), + + // "xx" doesn't exist, so it'll be ignored. + shortcut("xx", A1, 0) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s4", "x2", "s3", "s2"); + + // Interesting case: both x3 and s5 originally had rank=0, and in this case s5 has moved + // to A2 without changing the rank. So they're tie for the new rank, as well as + // the "rank changed" bit. Also in this case, "s5" won't have an implicit order, since + // its rank isn't changing. So we sort them by ID, thus s5 comes before x3. + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("s5", "x3", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s4", "x2", "s5", "x3"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.updateShortcuts(list( + shortcut("s3", A3))); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s4", "x2", "s2"); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("s5", "x3", "x1"); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A3) + .haveRanksInOrder("s3"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s3", "s2"); + } + + public void testUpdateShortcuts_withManifestShortcuts() { + runTestWithManifestShortcuts(() -> testUpdateShortcuts_noManifestShortcuts()); + } + + public void testDeleteDynamicShortcuts_noManifestShortcuts() { + mManager.addDynamicShortcuts(list( + shortcut("s5", A1, 0), + shortcut("s4", A1), + shortcut("s2", A1, 3), + shortcut("x1", A2), + shortcut("x3", A2, 2), + shortcut("x2", A2, 2), + shortcut("s3", A1, 0) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + //------------------------------------------------------ + long lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.removeDynamicShortcuts(list()); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .isEmpty(); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts( + CALLING_PACKAGE, list("s2", "s4", "x1", "x2"), HANDLE_USER_0); + }); + // Still same order. + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.removeDynamicShortcuts(list("s3", "x1", "xxxx")); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s2", "s4"); + } + + public void testDeleteDynamicShortcuts_withManifestShortcuts() { + runTestWithManifestShortcuts(() -> testDeleteDynamicShortcuts_noManifestShortcuts()); + } + + public void testDisableShortcuts_noManifestShortcuts() { + mManager.addDynamicShortcuts(list( + shortcut("s5", A1, 0), + shortcut("s4", A1), + shortcut("s2", A1, 3), + shortcut("x1", A2), + shortcut("x3", A2, 2), + shortcut("x2", A2, 2), + shortcut("s3", A1, 0) + )); + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + //------------------------------------------------------ + long lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.disableShortcuts(list()); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s3", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2", "x1"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .isEmpty(); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.disableShortcuts(list("s3", "x1", "xxxx")); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s2", "s4"); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE, list("s2", "s4", "x2"), HANDLE_USER_0); + }); + // Still same order. + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s2", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x3", "x2"); + + //------------------------------------------------------ + lastApiTime = ++mInjectedCurrentTimeMillis; + + mManager.disableShortcuts(list("s2", "x3")); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A1) + .haveRanksInOrder("s5", "s4"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByActivity(A2) + .haveRanksInOrder("x2"); + + assertWith(getCallerShortcuts()).selectDynamic().selectByChangedSince(lastApiTime) + .haveIds("s4", "x2"); + } + + public void testDisableShortcuts_withManifestShortcuts() { + runTestWithManifestShortcuts(() -> testDisableShortcuts_noManifestShortcuts()); + } + +} diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java index 04c7a042ef386..7ba4c6830a5c3 100644 --- a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java +++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java @@ -58,9 +58,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Predicate; @@ -72,10 +76,12 @@ import java.util.function.Predicate; public class ShortcutManagerTestUtils { private static final String TAG = "ShortcutManagerUtils"; - private static final boolean ENABLE_DUMPSYS = true; // DO NOT SUBMIT WITH true + private static final boolean ENABLE_DUMPSYS = false; // DO NOT SUBMIT WITH true private static final int STANDARD_TIMEOUT_SEC = 5; + private static final String[] EMPTY_STRINGS = new String[0]; + private ShortcutManagerTestUtils() { } @@ -228,7 +234,7 @@ public class ShortcutManagerTestUtils { } public static Set hashSet(Set in) { - return new HashSet(in); + return new LinkedHashSet<>(in); } public static Set set(T... values) { @@ -240,7 +246,7 @@ public class ShortcutManagerTestUtils { } public static Set set(Function converter, List values) { - final HashSet ret = new HashSet<>(); + final LinkedHashSet ret = new LinkedHashSet<>(); for (V v : values) { ret.add(converter.apply(v)); } @@ -258,15 +264,21 @@ public class ShortcutManagerTestUtils { return list; } + public static List filter(List list, Predicate p) { + final ArrayList ret = new ArrayList<>(list); + ret.removeIf(si -> !p.test(si)); + return ret; + } + public static List filterByActivity(List list, ComponentName activity) { - final ArrayList ret = new ArrayList<>(); - for (ShortcutInfo si : list) { - if (si.getActivity().equals(activity) && (si.isManifestShortcut() || si.isDynamic())) { - ret.add(si); - } - } - return ret; + return filter(list, si -> + (si.getActivity().equals(activity) + && (si.isManifestShortcut() || si.isDynamic()))); + } + + public static List changedSince(List list, long time) { + return filter(list, si -> si.getLastChangedTimestamp() >= time); } public static void assertExpectException(Class expectedExceptionType, @@ -305,8 +317,8 @@ public class ShortcutManagerTestUtils { public static List assertShortcutIds(List actualShortcuts, String... expectedIds) { - final HashSet expected = new HashSet<>(list(expectedIds)); - final HashSet actual = new HashSet<>(); + final SortedSet expected = new TreeSet<>(list(expectedIds)); + final SortedSet actual = new TreeSet<>(); for (ShortcutInfo s : actualShortcuts) { actual.add(s.getId()); } @@ -316,6 +328,17 @@ public class ShortcutManagerTestUtils { return actualShortcuts; } + public static List assertShortcutIdsOrdered(List actualShortcuts, + String... expectedIds) { + final ArrayList expected = new ArrayList<>(list(expectedIds)); + final ArrayList actual = new ArrayList<>(); + for (ShortcutInfo s : actualShortcuts) { + actual.add(s.getId()); + } + assertEquals(expected, actual); + return actualShortcuts; + } + public static List assertAllHaveIntents( List actualShortcuts) { for (ShortcutInfo s : actualShortcuts) { @@ -482,7 +505,7 @@ public class ShortcutManagerTestUtils { } public static void assertAllUnique(Collection list) { - final Set set = new HashSet<>(); + final Set set = new LinkedHashSet<>(); for (T item : list) { if (set.contains(item)) { fail("Duplicate item found: " + item + " (in the list: " + list + ")"); @@ -594,6 +617,15 @@ public class ShortcutManagerTestUtils { return ret; } + private static final Comparator sRankComparator = + (ShortcutInfo a, ShortcutInfo b) -> Integer.compare(a.getRank(), b.getRank()); + + public static List sortedByRank(List shortcuts) { + final ArrayList ret = new ArrayList<>(shortcuts); + Collections.sort(ret, sRankComparator); + return ret; + } + public static void waitUntil(String message, BooleanSupplier condition) { waitUntil(message, condition, STANDARD_TIMEOUT_SEC); } @@ -612,4 +644,78 @@ public class ShortcutManagerTestUtils { } fail("Timed out for: " + message); } + + public static ShortcutListAsserter assertWith(List list) { + return new ShortcutListAsserter(list); + } + + /** + * New style assertion that allows chained calls. + */ + public static class ShortcutListAsserter { + private final List mList; + + ShortcutListAsserter(List list) { + mList = new ArrayList<>(list); + } + + public ShortcutListAsserter selectDynamic() { + return new ShortcutListAsserter( + filter(mList, ShortcutInfo::isDynamic)); + } + + public ShortcutListAsserter selectManifest() { + return new ShortcutListAsserter( + filter(mList, ShortcutInfo::isManifestShortcut)); + } + + public ShortcutListAsserter selectPinned() { + return new ShortcutListAsserter( + filter(mList, ShortcutInfo::isPinned)); + } + + public ShortcutListAsserter selectByActivity(ComponentName activity) { + return new ShortcutListAsserter( + ShortcutManagerTestUtils.filterByActivity(mList, activity)); + } + + public ShortcutListAsserter selectByChangedSince(long time) { + return new ShortcutListAsserter( + ShortcutManagerTestUtils.changedSince(mList, time)); + } + + public ShortcutListAsserter toSortByRank() { + return new ShortcutListAsserter( + ShortcutManagerTestUtils.sortedByRank(mList)); + } + + public ShortcutListAsserter haveIds(String... expectedIds) { + assertShortcutIds(mList, expectedIds); + return this; + } + + public ShortcutListAsserter haveIdsOrdered(String... expectedIds) { + assertShortcutIdsOrdered(mList, expectedIds); + return this; + } + + private ShortcutListAsserter haveSequentialRanks() { + for (int i = 0; i < mList.size(); i++) { + assertEquals("Rank not sequential", i, mList.get(i).getRank()); + } + return this; + } + + public ShortcutListAsserter haveRanksInOrder(String... expectedIds) { + toSortByRank() + .haveSequentialRanks() + .haveIdsOrdered(expectedIds); + return this; + } + + public ShortcutListAsserter isEmpty() { + assertEquals(0, mList.size()); + return this; + } + } }