diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index b9b2d3d0cea73..e0c28fa467569 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -26,11 +26,13 @@ import android.os.PersistableBundle; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; +import com.android.server.pm.ShortcutService.ShortcutOperation; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -40,6 +42,8 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -126,7 +130,8 @@ class ShortcutPackage extends ShortcutPackageItem { } /** - * Called when a shortcut is about to be published. At this point we know the publisher package + * Called when a shortcut is about to be published. At this point we know the publisher + * package * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so * we do some initialization for the package. */ @@ -292,12 +297,17 @@ class ShortcutPackage extends ShortcutPackageItem { } /** - * Remove a dynamic shortcut by ID. + * Remove 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, and is still enabled. */ public void deleteDynamicWithId(@NonNull String shortcutId) { deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false); } + /** + * 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. + */ public void disableWithId(@NonNull String shortcutId, String disabledMessage, int disabledMessageResId, boolean overrideImmutable) { final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true, @@ -625,6 +635,10 @@ class ShortcutPackage extends ShortcutPackageItem { // (Re-)publish manifest shortcut. changed |= publishManifestShortcuts(newManifestShortcutList); + if (newManifestShortcutList != null) { + changed |= pushOutExcessShortcuts(); + } + if (changed) { // This will send a notification to the launcher, and also save . s.packageShortcutsChanged(getPackageName(), getPackageUserId()); @@ -642,10 +656,6 @@ class ShortcutPackage extends ShortcutPackageItem { } boolean changed = false; - // TODO: Check dynamic count - - // TODO: Kick out dynamic if too many - // Keep the previous IDs. ArraySet toDisableList = null; for (int i = mShortcuts.size() - 1; i >= 0; i--) { @@ -669,7 +679,7 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutInfo newShortcut = newManifestShortcutList.get(i); final boolean newDisabled = !newShortcut.isEnabled(); - final String id = newShortcut.getId(); + final String id = newShortcut.getId(); final ShortcutInfo oldShortcut = mShortcuts.get(id); boolean wasPinned = false; @@ -692,7 +702,6 @@ class ShortcutPackage extends ShortcutPackageItem { // Just keep it in toDisableList, so the previous one would be removed. continue; } - // TODO: Check dynamic count // Note even if enabled=false, we still need to update all fields, so do it // regardless. @@ -725,6 +734,179 @@ class ShortcutPackage extends ShortcutPackageItem { return changed; } + /** + * For each target activity, make sure # of dynamic + manifest shortcuts <= max. + * If too many, we'll remove the dynamic with the lowest ranks. + */ + private boolean pushOutExcessShortcuts() { + final ShortcutService service = mShortcutUser.mService; + final int maxShortcuts = service.getMaxActivityShortcuts(); + + boolean changed = false; + + final ArrayMap> all = + sortShortcutsToActivities(); + for (int outer = all.size() - 1; outer >= 0; outer--) { + final ArrayList list = all.valueAt(outer); + if (list.size() <= maxShortcuts) { + continue; + } + // Sort by isManifestShortcut() and getRank(). + Collections.sort(list, mShortcutTypeAndRankComparator); + + // Keep [0 .. max), and remove (as dynamic) [max .. size) + for (int inner = list.size() - 1; inner >= maxShortcuts; inner--) { + final ShortcutInfo shortcut = list.get(inner); + + if (shortcut.isManifestShortcut()) { + // This shouldn't happen -- excess shortcuts should all be non-manifest. + // But just in case. + service.wtf("Found manifest shortcuts in excess list."); + continue; + } + deleteDynamicWithId(shortcut.getId()); + } + } + service.verifyStates(); + + return changed; + } + + /** + * To sort by isManifestShortcut() and getRank(). i.e. manifest shortcuts come before + * non-manifest shortcuts, then sort by rank. + * + * This is used to decide which dynamic shortcuts to remove when an upgraded version has more + * manifest shortcuts than before and as a result we need to remove some of the dynamic + * shortcuts. We sort manifest + dynamic shortcuts by this order, and remove the ones with + * the last ones. + * + * (Note the number of manifest shortcuts is always <= the max number, because if there are + * more, ShortcutParser would ignore the rest.) + */ + final Comparator mShortcutTypeAndRankComparator = (ShortcutInfo a, + ShortcutInfo b) -> { + if (a.isManifestShortcut() && !b.isManifestShortcut()) { + return -1; + } + if (!a.isManifestShortcut() && b.isManifestShortcut()) { + return 1; + } + return a.getRank() - b.getRank(); + }; + + /** + * Build a list of shortcuts for each target activity and return as a map. The result won't + * 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--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + if (si.isFloating()) { + continue; // Ignore floating shortcuts, which are not tied to any activities. + } + + final ComponentName activity = si.getActivity(); + + ArrayList list = activitiesToShortcuts.get(activity); + if (list == null) { + list = new ArrayList<>(maxShortcuts * 2); + activitiesToShortcuts.put(activity, list); + } + list.add(si); + } + return activitiesToShortcuts; + } + + /** Used by {@link #enforceShortcutCountsBeforeOperation} */ + private void incrementCountForActivity(ArrayMap counts, + ComponentName cn, int increment) { + Integer oldValue = counts.get(cn); + if (oldValue == null) { + oldValue = 0; + } + + counts.put(cn, oldValue + increment); + } + + /** + * Called by + * {@link android.content.pm.ShortcutManager#setDynamicShortcuts}, + * {@link android.content.pm.ShortcutManager#addDynamicShortcuts}, and + * {@link android.content.pm.ShortcutManager#updateShortcuts} before actually performing + * the operation to make sure the operation wouldn't result in the target activities having + * more than the allowed number of dynamic/manifest shortcuts. + * + * @param newList shortcut list passed to set, add or updateShortcuts(). + * @param operation add, set or update. + * @throws IllegalArgumentException if the operation would result in going over the max + * shortcut count for any activity. + */ + public void enforceShortcutCountsBeforeOperation(List newList, + @ShortcutOperation int operation) { + final ShortcutService service = mShortcutUser.mService; + + // Current # of dynamic / manifest shortcuts for each activity. + // (If it's for update, then don't count dynamic shortcuts, since they'll be replaced + // anyway.) + final ArrayMap counts = new ArrayMap<>(4); + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo shortcut = mShortcuts.valueAt(i); + + if (shortcut.isManifestShortcut()) { + incrementCountForActivity(counts, shortcut.getActivity(), 1); + } else if (shortcut.isDynamic() && (operation != ShortcutService.OPERATION_SET)) { + incrementCountForActivity(counts, shortcut.getActivity(), 1); + } + } + + for (int i = newList.size() - 1; i >= 0; i--) { + final ShortcutInfo newShortcut = newList.get(i); + final ComponentName newActivity = newShortcut.getActivity(); + if (newActivity == null) { + if (operation != ShortcutService.OPERATION_UPDATE) { + service.wtf("null Activity found for non-update"); + } + continue; // Activity can be null for update. + } + + final ShortcutInfo original = mShortcuts.get(newShortcut.getId()); + if (original == null) { + if (operation == ShortcutService.OPERATION_UPDATE) { + continue; // When updating, ignore if there's no target. + } + // Add() or set(), and there's no existing shortcut with the same ID. We're + // simply publishing (as opposed to updating) this shortcut, so just +1. + incrementCountForActivity(counts, newActivity, 1); + continue; + } + if (original.isFloating() && (operation == ShortcutService.OPERATION_UPDATE)) { + // Updating floating shortcuts doesn't affect the count, so ignore. + continue; + } + + // If it's add() or update(), then need to decrement for the previous activity. + // Skip it for set() since it's already been taken care of by not counting the original + // dynamic shortcuts in the first loop. + if (operation != ShortcutService.OPERATION_SET) { + final ComponentName oldActivity = original.getActivity(); + if (!original.isFloating()) { + incrementCountForActivity(counts, oldActivity, -1); + } + } + incrementCountForActivity(counts, newActivity, 1); + } + + // Then make sure none of the activities have more than the max number of shortcuts. + for (int i = counts.size() - 1; i >= 0; i--) { + service.enforceMaxActivityShortcuts(counts.valueAt(i)); + } + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(); @@ -995,4 +1177,46 @@ class ShortcutPackage extends ShortcutPackageItem { List getAllShortcutsForTest() { return new ArrayList<>(mShortcuts.values()); } + + @Override + public void verifyStates() { + super.verifyStates(); + + boolean failed = false; + + final ArrayMap> all = + 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()) { + failed = true; + Log.e(TAG, "Package " + getPackageName() + ": activity " + all.keyAt(i) + + " has " + all.valueAt(i).size() + " shortcuts."); + } + } + + 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() + + " is not manifest, dynamic or pinned."); + } + if (si.getActivity() == null) { + failed = true; + Log.e(TAG, "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() + + " is not floating, but is disabled."); + } + } + + if (failed) { + throw new IllegalStateException("See logcat for errors"); + } + } } diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java index d08b97405b94c..757dd19172719 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java @@ -132,4 +132,10 @@ abstract class ShortcutPackageItem { public abstract void saveToXml(@NonNull XmlSerializer out, boolean forBackup) throws IOException, XmlPullParserException; + + /** + * Verify various internal states. + */ + public void verifyStates() { + } } diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java index 1e396b3bd8b1b..3eda13ee79b9d 100644 --- a/services/core/java/com/android/server/pm/ShortcutParser.java +++ b/services/core/java/com/android/server/pm/ShortcutParser.java @@ -86,6 +86,8 @@ public class ShortcutParser { int type; int rank = 0; + final int maxShortcuts = service.getMaxActivityShortcuts(); + int numShortcuts = 0; outer: while ((type = parser.next()) != XmlPullParser.END_DOCUMENT @@ -115,10 +117,17 @@ public class ShortcutParser { } if (si != null) { + if (numShortcuts >= maxShortcuts) { + Slog.w(TAG, "More than " + maxShortcuts + " shortcuts found for " + + activityInfo.getComponentName() + ", ignoring the rest."); + return result; + } + if (result == null) { result = new ArrayList<>(); } result.add(si); + numShortcuts++; } continue; } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index dc0404c9546cb..6f1274170b716 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -15,6 +15,7 @@ */ package com.android.server.pm; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -102,6 +103,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -114,8 +117,6 @@ import java.util.function.Predicate; /** * TODO: - * - Implement # of dynamic shortcuts cap. - * * - HandleUnlockUser needs to be async. Wait on it in onCleanupUser. * * - Implement reportShortcutUsed(). @@ -137,12 +138,12 @@ import java.util.function.Predicate; * * - Add more call stats. * - * - Rename getMaxDynamicShortcutCount and mMaxDynamicShortcuts + * - Rename mMaxDynamicShortcuts, because it includes manifest shortcuts too. */ public class ShortcutService extends IShortcutService.Stub { static final String TAG = "ShortcutService"; - static final boolean DEBUG = true; // STOPSHIP if true + static final boolean DEBUG = false; // STOPSHIP if true static final boolean DEBUG_LOAD = false; // STOPSHIP if true static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true @@ -329,6 +330,19 @@ public class ShortcutService extends IShortcutService.Stub { private static final int PROCESS_STATE_FOREGROUND_THRESHOLD = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + static final int OPERATION_SET = 0; + static final int OPERATION_ADD = 1; + static final int OPERATION_UPDATE = 2; + + /** @hide */ + @IntDef(value = { + OPERATION_SET, + OPERATION_ADD, + OPERATION_UPDATE + }) + @Retention(RetentionPolicy.SOURCE) + @interface ShortcutOperation {} + public ShortcutService(Context context) { this(context, BackgroundThread.get().getLooper()); } @@ -1312,14 +1326,22 @@ public class ShortcutService extends IShortcutService.Stub { } /** - * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}. + * @throws IllegalArgumentException if {@code numShortcuts} is bigger than + * {@link #getMaxActivityShortcuts()}. */ - void enforceMaxDynamicShortcuts(int numShortcuts) { + void enforceMaxActivityShortcuts(int numShortcuts) { if (numShortcuts > mMaxDynamicShortcuts) { throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded"); } } + /** + * Return the max number of dynamic + manifest shortcuts for each launcher icon. + */ + int getMaxActivityShortcuts() { + return mMaxDynamicShortcuts; + } + /** * - Sends a notification to LauncherApps * - Write to file @@ -1440,11 +1462,12 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET); + // Throttling. if (!ps.tryApiCall()) { return false; } - enforceMaxDynamicShortcuts(size); // Validate the shortcuts. for (int i = 0; i < size; i++) { @@ -1461,6 +1484,9 @@ public class ShortcutService extends IShortcutService.Stub { } } packageShortcutsChanged(packageName, userId); + + verifyStates(); + return true; } @@ -1477,6 +1503,8 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1514,6 +1542,8 @@ public class ShortcutService extends IShortcutService.Stub { } packageShortcutsChanged(packageName, userId); + verifyStates(); + return true; } @@ -1530,6 +1560,8 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1546,6 +1578,8 @@ public class ShortcutService extends IShortcutService.Stub { } packageShortcutsChanged(packageName, userId); + verifyStates(); + return true; } @@ -1567,6 +1601,8 @@ public class ShortcutService extends IShortcutService.Stub { } } packageShortcutsChanged(packageName, userId); + + verifyStates(); } @Override @@ -1584,6 +1620,8 @@ public class ShortcutService extends IShortcutService.Stub { } } packageShortcutsChanged(packageName, userId); + + verifyStates(); } @Override @@ -1603,6 +1641,8 @@ public class ShortcutService extends IShortcutService.Stub { } } packageShortcutsChanged(packageName, userId); + + verifyStates(); } @Override @@ -1613,6 +1653,8 @@ public class ShortcutService extends IShortcutService.Stub { getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(); } packageShortcutsChanged(packageName, userId); + + verifyStates(); } @Override @@ -2023,6 +2065,8 @@ public class ShortcutService extends IShortcutService.Stub { launcher.pinShortcuts(userId, packageName, shortcutIds); } packageShortcutsChanged(packageName, userId); + + verifyStates(); } @Override @@ -2940,4 +2984,29 @@ public class ShortcutService extends IShortcutService.Stub { return pkg.findShortcutById(shortcutId); } } + + /** + * Control whether {@link #verifyStates} should be performed. We always perform it during unit + * tests. + */ + @VisibleForTesting + boolean injectShouldPerformVerification() { + return DEBUG; + } + + /** + * Check various internal states and throws if there's any inconsistency. + * This is normally only enabled during unit tests. + */ + final void verifyStates() { + if (injectShouldPerformVerification()) { + verifyStatesInner(); + } + } + + private void verifyStatesInner() { + synchronized (this) { + forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates)); + } + } } diff --git a/services/tests/servicestests/res/xml/shortcut_1_alt.xml b/services/tests/servicestests/res/xml/shortcut_1_alt.xml new file mode 100644 index 0000000000000..bf14f49cae9c4 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_1_alt.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_1_disable.xml b/services/tests/servicestests/res/xml/shortcut_1_disable.xml index 6bc3939a52a59..81a84b40a8bbe 100644 --- a/services/tests/servicestests/res/xml/shortcut_1_disable.xml +++ b/services/tests/servicestests/res/xml/shortcut_1_disable.xml @@ -1,3 +1,18 @@ + + + + + + + + + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_1.xml b/services/tests/servicestests/res/xml/shortcut_error_1.xml index e45d93c94911b..5822496fac113 100644 --- a/services/tests/servicestests/res/xml/shortcut_error_1.xml +++ b/services/tests/servicestests/res/xml/shortcut_error_1.xml @@ -1,3 +1,18 @@ + + + + launcher callback. - * Also locale change should trigger launcehr callbacks too, when they use strign resoucres. - * (not implemented yet.) + * Also locale change should trigger launcher callbacks too, when they use strign resoucres. + * (not implemented yet.) * TODO: Add checks with assertAllNotHaveIcon() * TODO: Detailed test for hasShortcutPermissionInner(). * TODO: Add tests for the command line functions too. @@ -169,9 +189,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { * Whether to enable dump or not. Should be only true when debugging to avoid bugs where * dump affecting the behavior. */ - private static final boolean ENABLE_DUMP = true; // DO NOT SUBMIT WITH true + private static final boolean ENABLE_DUMP = false; // DO NOT SUBMIT WITH true - private static final boolean DUMP_IN_TEARDOWN = true; // DO NOT SUBMIT WITH true + private static final boolean DUMP_IN_TEARDOWN = false; // DO NOT SUBMIT WITH true private static final String[] EMPTY_STRINGS = new String[0]; // Just for readability. @@ -264,6 +284,11 @@ public class ShortcutManagerTest extends InstrumentationTestCase { mContext = context; } + @Override + boolean injectShouldPerformVerification() { + return true; // Always verify during unit tests. + } + @Override String injectShortcutManagerConstants() { return ConfigConstants.KEY_RESET_INTERVAL_SEC + "=" + (INTERVAL / 1000) + "," @@ -407,6 +432,25 @@ public class ShortcutManagerTest extends InstrumentationTestCase { protected int injectMyUserId() { return UserHandle.getUserId(mInjectedCallingUid); } + + @Override + public boolean setDynamicShortcuts(@NonNull List shortcutInfoList) { + // Note to simulate the binder RPC, we need to clone the incoming arguments. + // Otherwise bad things will happen because they're mutable. + return super.setDynamicShortcuts(cloneShortcutList(shortcutInfoList)); + } + + @Override + public boolean addDynamicShortcuts(@NonNull List shortcutInfoList) { + // Note to simulate the binder RPC, we need to clone the incoming arguments. + return super.addDynamicShortcuts(cloneShortcutList(shortcutInfoList)); + } + + @Override + public boolean updateShortcuts(List shortcutInfoList) { + // Note to simulate the binder RPC, we need to clone the incoming arguments. + return super.updateShortcuts(cloneShortcutList(shortcutInfoList)); + } } private class LauncherAppImplTestable extends LauncherAppsImpl { @@ -585,7 +629,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { mInjectedCurrentTimeLillis = START_TIME; - mInjectedPackages = new HashMap<>();; + mInjectedPackages = new HashMap<>(); addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 1); addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 2); addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 3); @@ -929,9 +973,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { mService.dumpInner(pw, null); pw.close(); - Log.e(TAG, "Dumping ShortcutService: " + message); + Log.v(TAG, "Dumping ShortcutService: " + message); for (String line : out.toString().split("\n")) { - Log.e(TAG, line); + Log.v(TAG, line); } } @@ -945,7 +989,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { private void dumpFileOnLogcat(String path, String message) { if (!ENABLE_DUMP) return; - Log.i(TAG, "Dumping file: " + path + " " + message); + Log.v(TAG, "Dumping file: " + path + " " + message); final StringBuilder sb = new StringBuilder(); try (BufferedReader br = new BufferedReader(new FileReader(path))) { String line; @@ -953,7 +997,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { Log.i(TAG, line); } } catch (Exception e) { - Log.e(TAG, "Couldn't read file", e); + Log.v(TAG, "Couldn't read file", e); fail("Exception " + e); } } @@ -982,7 +1026,8 @@ public class ShortcutManagerTest extends InstrumentationTestCase { } private void waitOnMainThread() throws Throwable { - runTestOnUiThread(() -> {}); + runTestOnUiThread(() -> { + }); } /** @@ -991,13 +1036,13 @@ public class ShortcutManagerTest extends InstrumentationTestCase { private ShortcutInfo makeShortcut(String id) { return makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); } private ShortcutInfo makeShortcutWithTitle(String id, String title) { return makeShortcut( id, title, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); } /** @@ -1006,7 +1051,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { private ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) { final ShortcutInfo s = makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); s.setTimestamp(timestamp); return s; } @@ -1018,7 +1063,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { ComponentName activity) { final ShortcutInfo s = makeShortcut( id, "Title-" + id, activity, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); s.setTimestamp(timestamp); return s; } @@ -1029,7 +1074,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { private ShortcutInfo makeShortcutWithIcon(String id, Icon icon) { return makeShortcut( id, "Title-" + id, /* activity =*/ null, icon, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); } private ShortcutInfo makePackageShortcut(String packageName, String id) { @@ -1038,7 +1083,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { setCaller(packageName); ShortcutInfo s = makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); setCaller(origCaller); // restore the caller return s; @@ -1059,6 +1104,26 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return new ShortcutInfo.Builder(mClientContext); } + private ShortcutInfo makeShortcutWithActivity(String id, ComponentName activity) { + return makeShortcut( + id, "Title-" + id, activity, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + } + + private ShortcutInfo makeShortcutWithActivityAndTitle(String id, ComponentName activity, + String title) { + return makeShortcut( + id, title, activity, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + } + + private ShortcutInfo makeShortcutWithActivityAndRank(String id, ComponentName activity, + int rank) { + return makeShortcut( + id, "Title-" + id, activity, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), rank); + } + /** * Make a shortcut with details. */ @@ -1297,6 +1362,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { i.putExtra(Intent.EXTRA_REPLACING, true); return i; } + private Intent genPackageDataClear(String packageName, int userId) { Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED); i.setData(Uri.parse("package:" + packageName)); @@ -1304,15 +1370,6 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return i; } - private ShortcutInfo parceled(ShortcutInfo si) { - Parcel p = Parcel.obtain(); - p.writeParcelable(si, 0); - p.setDataPosition(0); - ShortcutInfo si2 = p.readParcelable(getClass().getClassLoader()); - p.recycle(); - return si2; - } - /** * Test for the first launch path, no settings file available. */ @@ -4509,7 +4566,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10)); mService.checkPackageChanges(USER_P0); - + assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0)); assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0)); assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_0)); @@ -7905,4 +7962,434 @@ public class ShortcutManagerTest extends InstrumentationTestCase { }); checkManifestShortcuts_immutable_verify(); } + + /** + * Make sure the APIs won't work on manifest shortcuts. + */ + public void testManifestShortcuts_tooMany() { + // Change the max number of shortcuts. + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); + + mService.handleUnlockUser(USER_0); + + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_5); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + // Only the first 3 should be published. + assertShortcutIds(mManager.getManifestShortcuts(), "ms1", "ms2", "ms3"); + }); + } + + public void testMaxShortcutCount_set() { + // Change the max number of shortcuts. + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class); + final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class); + final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1); + final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1); + final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1); + final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1); + final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1); + final ShortcutInfo s1_6 = makeShortcutWithActivity("s16", a1); + final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2); + final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2); + final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2); + final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2); + + // 3 shortcuts for 2 activities -> okay + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + mManager.removeAllDynamicShortcuts(); + + // 4 shortcut for activity 1 -> too many. + assertDynamicShortcutCountExceeded(() -> { + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3)); + }); + assertEmpty(mManager.getDynamicShortcuts()); + + // 4 shortcut for activity 2 -> too many. + assertDynamicShortcutCountExceeded(() -> { + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4)); + }); + assertEmpty(mManager.getDynamicShortcuts()); + + // First, set 3. Then set 4, which should be ignored. + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13"); + assertDynamicShortcutCountExceeded(() -> { + mManager.setDynamicShortcuts(list(s2_1, s2_2, s2_3, s2_4)); + }); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13"); + + // Set will remove the old dynamic set, unlike add, so the following should pass. + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13"); + mManager.setDynamicShortcuts(list(s1_4, s1_5, s1_6)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s14", "s15", "s16"); + + // Now, test with 2 manifest shortcuts. + mManager.removeAllDynamicShortcuts(); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + assertEquals(2, mManager.getManifestShortcuts().size()); + + // Setting 1 to activity 1 will work. + mManager.setDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s21", "s22", "s23"); + assertEquals(2, mManager.getManifestShortcuts().size()); + + // But setting 2 will not. + mManager.removeAllDynamicShortcuts(); + assertDynamicShortcutCountExceeded(() -> { + mManager.setDynamicShortcuts(list(s1_1, s1_2, s2_1, s2_2, s2_3)); + }); + assertEmpty(mManager.getDynamicShortcuts()); + assertEquals(2, mManager.getManifestShortcuts().size()); + }); + } + + public void testMaxShortcutCount_add() { + // Change the max number of shortcuts. + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class); + final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class); + final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1); + final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1); + final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1); + final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1); + final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2); + final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2); + final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2); + final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2); + + // 3 shortcuts for 2 activities -> okay + mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + mManager.removeAllDynamicShortcuts(); + ; + + // 4 shortcut for activity 1 -> too many. + assertDynamicShortcutCountExceeded(() -> { + mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s1_4, s2_1, s2_2, s2_3)); + }); + assertEmpty(mManager.getDynamicShortcuts()); + + // 4 shortcut for activity 2 -> too many. + assertDynamicShortcutCountExceeded(() -> { + mManager.addDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3, s2_4)); + }); + assertEmpty(mManager.getDynamicShortcuts()); + + // First, set 3. Then add 1 more, which should be ignored. + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13"); + assertDynamicShortcutCountExceeded(() -> { + mManager.addDynamicShortcuts(list(s1_4, s2_1)); + }); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13"); + + // Update existing one, which should work. + mManager.addDynamicShortcuts(list(makeShortcutWithActivityAndTitle( + "s11", a1, "xxx"), s2_1)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21"); + assertEquals("xxx", getCallerShortcut("s11").getTitle()); + + // Make sure pinned shortcuts won't affect. + // - Pin s11 - s13, and remove all dynamic. + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"), + HANDLE_USER_0); + }); + mManager.removeAllDynamicShortcuts(); + + assertEmpty(mManager.getDynamicShortcuts()); + assertShortcutIds(mManager.getPinnedShortcuts(), + "s11", "s12", "s13"); + + // Then add dynamic. + mManager.addDynamicShortcuts(list(s1_4, s2_1, s2_2, s2_3)); + + assertShortcutIds(mManager.getDynamicShortcuts(), + "s14", "s21", "s22", "s23"); + assertShortcutIds(mManager.getPinnedShortcuts(), + "s11", "s12", "s13"); + + // Adding "s11" and "s12" back, should work + mManager.addDynamicShortcuts(list(s1_1, s1_2)); + + assertShortcutIds(mManager.getDynamicShortcuts(), + "s14", "s11", "s12", "s21", "s22", "s23"); + assertShortcutIds(mManager.getPinnedShortcuts(), + "s11", "s12", "s13"); + + // Adding back s13 doesn't work. + assertDynamicShortcutCountExceeded(() -> { + mManager.addDynamicShortcuts(list(s1_3)); + }); + + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1), + "s11", "s12", "s14"); + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2), + "s21", "s22", "s23"); + + // Now swap the activities. + mManager.updateShortcuts(list( + makeShortcutWithActivity("s11", a2), + makeShortcutWithActivity("s21", a1))); + + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1), + "s21", "s12", "s14"); + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2), + "s11", "s22", "s23"); + + // Now, test with 2 manifest shortcuts. + mManager.removeAllDynamicShortcuts(); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + assertEquals(2, mManager.getManifestShortcuts().size()); + + // Adding one shortcut to activity 1 works fine. + mManager.addDynamicShortcuts(list(s1_1, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s21", "s22", "s23"); + assertEquals(2, mManager.getManifestShortcuts().size()); + + // But adding one more doesn't. + assertDynamicShortcutCountExceeded(() -> { + mManager.addDynamicShortcuts(list(s1_4, s2_1)); + }); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s21", "s22", "s23"); + assertEquals(2, mManager.getManifestShortcuts().size()); + }); + } + + public void testMaxShortcutCount_update() { + // Change the max number of shortcuts. + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class); + final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class); + final ShortcutInfo s1_1 = makeShortcutWithActivity("s11", a1); + final ShortcutInfo s1_2 = makeShortcutWithActivity("s12", a1); + final ShortcutInfo s1_3 = makeShortcutWithActivity("s13", a1); + final ShortcutInfo s1_4 = makeShortcutWithActivity("s14", a1); + final ShortcutInfo s1_5 = makeShortcutWithActivity("s15", a1); + final ShortcutInfo s2_1 = makeShortcutWithActivity("s21", a2); + final ShortcutInfo s2_2 = makeShortcutWithActivity("s22", a2); + final ShortcutInfo s2_3 = makeShortcutWithActivity("s23", a2); + final ShortcutInfo s2_4 = makeShortcutWithActivity("s24", a2); + + // 3 shortcuts for 2 activities -> okay + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + // Trying to move s11 from a1 to a2 should fail. + assertDynamicShortcutCountExceeded(() -> { + mManager.updateShortcuts(list(makeShortcutWithActivity("s11", a2))); + }); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + // Trying to move s21 from a2 to a1 should also fail. + assertDynamicShortcutCountExceeded(() -> { + mManager.updateShortcuts(list(makeShortcutWithActivity("s21", a1))); + }); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + // But, if we do these two at the same time, it should work. + mManager.updateShortcuts(list( + makeShortcutWithActivity("s11", a2), + makeShortcutWithActivity("s21", a1))); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1), + "s21", "s12", "s13"); + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2), + "s11", "s22", "s23"); + + // Then reset. + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s11", "s12", "s13", "s21", "s22", "s23"); + + // Pin some to have more shortcuts for a1. + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s13"), + HANDLE_USER_0); + }); + mManager.setDynamicShortcuts(list(s1_4, s1_5, s2_1, s2_2, s2_3)); + assertShortcutIds(mManager.getDynamicShortcuts(), + "s14", "s15", "s21", "s22", "s23"); + assertShortcutIds(mManager.getPinnedShortcuts(), + "s11", "s12", "s13"); + + // a1 already has 2 dynamic shortcuts (and 3 pinned shortcuts that used to belong on it) + // But that doesn't matter for update -- the following should still work. + mManager.updateShortcuts(list( + makeShortcutWithActivityAndTitle("s11", a1, "xxx1"), + makeShortcutWithActivityAndTitle("s12", a1, "xxx2"), + makeShortcutWithActivityAndTitle("s13", a1, "xxx3"), + makeShortcutWithActivityAndTitle("s14", a1, "xxx4"), + makeShortcutWithActivityAndTitle("s15", a1, "xxx5"))); + // All the shortcuts should still exist they all belong on same activities, + // with the updated titles. + assertShortcutIds(mManager.getDynamicShortcuts(), + "s14", "s15", "s21", "s22", "s23"); + assertShortcutIds(mManager.getPinnedShortcuts(), + "s11", "s12", "s13"); + + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a1), + "s14", "s15"); + assertShortcutIds(filterByActivity(mManager.getDynamicShortcuts(), a2), + "s21", "s22", "s23"); + + assertEquals("xxx1", getCallerShortcut("s11").getTitle()); + assertEquals("xxx2", getCallerShortcut("s12").getTitle()); + assertEquals("xxx3", getCallerShortcut("s13").getTitle()); + assertEquals("xxx4", getCallerShortcut("s14").getTitle()); + assertEquals("xxx5", getCallerShortcut("s15").getTitle()); + }); + } + + public void testShortcutsPushedOutByManifest() { + // Change the max number of shortcuts. + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + final ComponentName a1 = new ComponentName(mClientContext, ShortcutActivity.class); + final ComponentName a2 = new ComponentName(mClientContext, ShortcutActivity2.class); + final ShortcutInfo s1_1 = makeShortcutWithActivityAndRank("s11", a1, 4); + final ShortcutInfo s1_2 = makeShortcutWithActivityAndRank("s12", a1, 3); + final ShortcutInfo s1_3 = makeShortcutWithActivityAndRank("s13", a1, 2); + final ShortcutInfo s1_4 = makeShortcutWithActivityAndRank("s14", a1, 1); + final ShortcutInfo s1_5 = makeShortcutWithActivityAndRank("s15", a1, 0); + final ShortcutInfo s2_1 = makeShortcutWithActivityAndRank("s21", a2, 0); + final ShortcutInfo s2_2 = makeShortcutWithActivityAndRank("s22", a2, 1); + final ShortcutInfo s2_3 = makeShortcutWithActivityAndRank("s23", a2, 2); + final ShortcutInfo s2_4 = makeShortcutWithActivityAndRank("s24", a2, 3); + final ShortcutInfo s2_5 = makeShortcutWithActivityAndRank("s25", a2, 4); + + // Initial state. + mManager.setDynamicShortcuts(list(s1_1, s1_2, s1_3, s2_1, s2_2, s2_3)); + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s11", "s12", "s21", "s22"), + HANDLE_USER_0); + }); + mManager.setDynamicShortcuts(list(s1_2, s1_3, s1_4, s2_2, s2_3, s2_4)); + assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()), + "s12", "s13", "s14", + "s22", "s23", "s24"); + assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()), + "s11", "s12", + "s21", "s22"); + + // Add 1 manifest shortcut to a1. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_1); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + assertEquals(1, mManager.getManifestShortcuts().size()); + + // s12 removed. + assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()), + "s13", "s14", + "s22", "s23", "s24"); + assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()), + "s11", "s12", + "s21", "s22"); + + // Add more manifest shortcuts. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), + R.xml.shortcut_1_alt); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + assertEquals(3, mManager.getManifestShortcuts().size()); + + // Note the ones with the highest rank values (== least important) will be removed. + assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()), + "s14", + "s22", "s23"); + assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()), + "s11", "s12", + "s21", "s22"); + + // Add more manifest shortcuts. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), + R.xml.shortcut_5_alt); // manifest has 5, but max is 3, so a2 will have 3. + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + assertEquals(5, mManager.getManifestShortcuts().size()); + + assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()), + "s14" // a1 has 1 dynamic + ); // a2 has no dynamic + assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()), + "s11", "s12", + "s21", "s22"); + + // Update, no manifest shortucts. This doesn't affect anything. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_0); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), + R.xml.shortcut_0); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + assertEquals(0, mManager.getManifestShortcuts().size()); + + assertShortcutIds(assertAllEnabled(mManager.getDynamicShortcuts()), + "s14"); + assertShortcutIds(assertAllEnabled(mManager.getPinnedShortcuts()), + "s11", "s12", + "s21", "s22"); + }); + } } 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 e02e98f2329f3..c91505e70e7a3 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 @@ -31,6 +31,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; @@ -38,6 +39,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.BaseBundle; import android.os.Bundle; +import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.test.MoreAsserts; @@ -252,6 +254,17 @@ public class ShortcutManagerTestUtils { return list; } + 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; + } + public static void assertExpectException(Class expectedExceptionType, String expectedExceptionMessageRegex, Runnable r) { assertExpectException("", expectedExceptionType, expectedExceptionMessageRegex, r); @@ -555,6 +568,27 @@ public class ShortcutManagerTestUtils { }, "Shortcut IDs=[" + Arrays.toString(ids) + "]", null); } + public static ShortcutInfo parceled(ShortcutInfo si) { + Parcel p = Parcel.obtain(); + p.writeParcelable(si, 0); + p.setDataPosition(0); + ShortcutInfo si2 = p.readParcelable(ShortcutManagerTestUtils.class.getClassLoader()); + p.recycle(); + return si2; + } + + public static List cloneShortcutList(List list) { + if (list == null) { + return null; + } + final List ret = new ArrayList<>(list.size()); + for (ShortcutInfo si : list) { + ret.add(parceled(si)); + } + + return ret; + } + public static void waitUntil(String message, BooleanSupplier condition) { waitUntil(message, condition, STANDARD_TIMEOUT_SEC); }