diff --git a/api/current.txt b/api/current.txt index 1f7fd3252cee3..f2102066b7d10 100644 --- a/api/current.txt +++ b/api/current.txt @@ -420,9 +420,9 @@ package android { field public static final int contentInsetStart = 16843859; // 0x1010453 field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522 field public static final int contextClickable = 16844007; // 0x10104e7 - field public static final int contextDescription = 16844082; // 0x1010532 + field public static final int contextDescription = 16844078; // 0x101052e field public static final int contextPopupMenuStyle = 16844033; // 0x1010501 - field public static final int contextUri = 16844081; // 0x1010531 + field public static final int contextUri = 16844077; // 0x101052d field public static final int controlX1 = 16843772; // 0x10103fc field public static final int controlX2 = 16843774; // 0x10103fe field public static final int controlY1 = 16843773; // 0x10103fd @@ -1041,7 +1041,7 @@ package android { field public static final int rotation = 16843558; // 0x1010326 field public static final int rotationX = 16843559; // 0x1010327 field public static final int rotationY = 16843560; // 0x1010328 - field public static final int roundIcon = 16844080; // 0x1010530 + field public static final int roundIcon = 16844076; // 0x101052c field public static final int rowCount = 16843637; // 0x1010375 field public static final int rowDelay = 16843216; // 0x10101d0 field public static final int rowEdgeFlags = 16843329; // 0x1010241 @@ -1109,20 +1109,16 @@ package android { field public static final int shareInterpolator = 16843195; // 0x10101bb field public static final int sharedUserId = 16842763; // 0x101000b field public static final int sharedUserLabel = 16843361; // 0x1010261 - field public static final int shortcutCategories = 16844077; // 0x101052d - field public static final int shortcutDisabledMessage = 16844076; // 0x101052c - field public static final int shortcutIcon = 16844073; // 0x1010529 + field public static final int shortcutDisabledMessage = 16844075; // 0x101052b field public static final int shortcutId = 16844072; // 0x1010528 - field public static final int shortcutIntentAction = 16844078; // 0x101052e - field public static final int shortcutIntentData = 16844079; // 0x101052f - field public static final int shortcutLongLabel = 16844075; // 0x101052b - field public static final int shortcutShortLabel = 16844074; // 0x101052a + field public static final int shortcutLongLabel = 16844074; // 0x101052a + field public static final int shortcutShortLabel = 16844073; // 0x1010529 field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa field public static final int showDividers = 16843561; // 0x1010329 field public static final int showForAllUsers = 16844015; // 0x10104ef - field public static final int showMetadataInPreview = 16844083; // 0x1010533 + field public static final int showMetadataInPreview = 16844079; // 0x101052f field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9 field public static final int showSilent = 16843259; // 0x10101fb field public static final int showText = 16843949; // 0x10104ad @@ -10104,14 +10100,14 @@ package android.content.pm { method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); - method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); - method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setRank(int); - method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence); } public class ShortcutManager { diff --git a/api/system-current.txt b/api/system-current.txt index a837dea7da0d5..e38a110b37351 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -527,9 +527,9 @@ package android { field public static final int contentInsetStart = 16843859; // 0x1010453 field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522 field public static final int contextClickable = 16844007; // 0x10104e7 - field public static final int contextDescription = 16844082; // 0x1010532 + field public static final int contextDescription = 16844078; // 0x101052e field public static final int contextPopupMenuStyle = 16844033; // 0x1010501 - field public static final int contextUri = 16844081; // 0x1010531 + field public static final int contextUri = 16844077; // 0x101052d field public static final int controlX1 = 16843772; // 0x10103fc field public static final int controlX2 = 16843774; // 0x10103fe field public static final int controlY1 = 16843773; // 0x10103fd @@ -1148,7 +1148,7 @@ package android { field public static final int rotation = 16843558; // 0x1010326 field public static final int rotationX = 16843559; // 0x1010327 field public static final int rotationY = 16843560; // 0x1010328 - field public static final int roundIcon = 16844080; // 0x1010530 + field public static final int roundIcon = 16844076; // 0x101052c field public static final int rowCount = 16843637; // 0x1010375 field public static final int rowDelay = 16843216; // 0x10101d0 field public static final int rowEdgeFlags = 16843329; // 0x1010241 @@ -1220,20 +1220,16 @@ package android { field public static final int shareInterpolator = 16843195; // 0x10101bb field public static final int sharedUserId = 16842763; // 0x101000b field public static final int sharedUserLabel = 16843361; // 0x1010261 - field public static final int shortcutCategories = 16844077; // 0x101052d - field public static final int shortcutDisabledMessage = 16844076; // 0x101052c - field public static final int shortcutIcon = 16844073; // 0x1010529 + field public static final int shortcutDisabledMessage = 16844075; // 0x101052b field public static final int shortcutId = 16844072; // 0x1010528 - field public static final int shortcutIntentAction = 16844078; // 0x101052e - field public static final int shortcutIntentData = 16844079; // 0x101052f - field public static final int shortcutLongLabel = 16844075; // 0x101052b - field public static final int shortcutShortLabel = 16844074; // 0x101052a + field public static final int shortcutLongLabel = 16844074; // 0x101052a + field public static final int shortcutShortLabel = 16844073; // 0x1010529 field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa field public static final int showDividers = 16843561; // 0x1010329 field public static final int showForAllUsers = 16844015; // 0x10104ef - field public static final int showMetadataInPreview = 16844083; // 0x1010533 + field public static final int showMetadataInPreview = 16844079; // 0x101052f field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9 field public static final int showSilent = 16843259; // 0x10101fb field public static final int showText = 16843949; // 0x10104ad @@ -10528,14 +10524,14 @@ package android.content.pm { method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); - method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); - method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setRank(int); - method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence); } public class ShortcutManager { diff --git a/api/test-current.txt b/api/test-current.txt index 340f6f345dba9..2173aedb3c3f7 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -420,9 +420,9 @@ package android { field public static final int contentInsetStart = 16843859; // 0x1010453 field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522 field public static final int contextClickable = 16844007; // 0x10104e7 - field public static final int contextDescription = 16844082; // 0x1010532 + field public static final int contextDescription = 16844078; // 0x101052e field public static final int contextPopupMenuStyle = 16844033; // 0x1010501 - field public static final int contextUri = 16844081; // 0x1010531 + field public static final int contextUri = 16844077; // 0x101052d field public static final int controlX1 = 16843772; // 0x10103fc field public static final int controlX2 = 16843774; // 0x10103fe field public static final int controlY1 = 16843773; // 0x10103fd @@ -1041,7 +1041,7 @@ package android { field public static final int rotation = 16843558; // 0x1010326 field public static final int rotationX = 16843559; // 0x1010327 field public static final int rotationY = 16843560; // 0x1010328 - field public static final int roundIcon = 16844080; // 0x1010530 + field public static final int roundIcon = 16844076; // 0x101052c field public static final int rowCount = 16843637; // 0x1010375 field public static final int rowDelay = 16843216; // 0x10101d0 field public static final int rowEdgeFlags = 16843329; // 0x1010241 @@ -1109,20 +1109,16 @@ package android { field public static final int shareInterpolator = 16843195; // 0x10101bb field public static final int sharedUserId = 16842763; // 0x101000b field public static final int sharedUserLabel = 16843361; // 0x1010261 - field public static final int shortcutCategories = 16844077; // 0x101052d - field public static final int shortcutDisabledMessage = 16844076; // 0x101052c - field public static final int shortcutIcon = 16844073; // 0x1010529 + field public static final int shortcutDisabledMessage = 16844075; // 0x101052b field public static final int shortcutId = 16844072; // 0x1010528 - field public static final int shortcutIntentAction = 16844078; // 0x101052e - field public static final int shortcutIntentData = 16844079; // 0x101052f - field public static final int shortcutLongLabel = 16844075; // 0x101052b - field public static final int shortcutShortLabel = 16844074; // 0x101052a + field public static final int shortcutLongLabel = 16844074; // 0x101052a + field public static final int shortcutShortLabel = 16844073; // 0x1010529 field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa field public static final int showDividers = 16843561; // 0x1010329 field public static final int showForAllUsers = 16844015; // 0x10104ef - field public static final int showMetadataInPreview = 16844083; // 0x1010533 + field public static final int showMetadataInPreview = 16844079; // 0x101052f field public static final deprecated int showOnLockScreen = 16843721; // 0x10103c9 field public static final int showSilent = 16843259; // 0x10101fb field public static final int showText = 16843949; // 0x10104ad @@ -10117,14 +10113,14 @@ package android.content.pm { method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); - method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); method public android.content.pm.ShortcutInfo.Builder setId(java.lang.String); method public android.content.pm.ShortcutInfo.Builder setIntent(android.content.Intent); - method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setLongLabel(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setRank(int); - method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.String); + method public android.content.pm.ShortcutInfo.Builder setShortLabel(java.lang.CharSequence); } public class ShortcutManager { diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 896fa43cd11b1..064b9090c4448 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -30,6 +30,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -245,7 +246,7 @@ public final class ShortcutInfo implements Parcelable { mTextResId = b.mTextResId; mDisabledMessage = b.mDisabledMessage; mDisabledMessageResId = b.mDisabledMessageResId; - mCategories = clone(b.mCategories); + mCategories = cloneCategories(b.mCategories); mIntent = b.mIntent; if (mIntent != null) { final Bundle intentExtras = mIntent.getExtras(); @@ -259,8 +260,17 @@ public final class ShortcutInfo implements Parcelable { updateTimestamp(); } - private ArraySet clone(Set source) { - return (source == null) ? null : new ArraySet<>(source); + private ArraySet cloneCategories(Set source) { + if (source == null) { + return null; + } + final ArraySet ret = new ArraySet<>(source.size()); + for (CharSequence s : source) { + if (!TextUtils.isEmpty(s)) { + ret.add(s.toString().intern()); + } + } + return ret; } /** @@ -304,7 +314,7 @@ public final class ShortcutInfo implements Parcelable { mTextResId = source.mTextResId; mDisabledMessage = source.mDisabledMessage; mDisabledMessageResId = source.mDisabledMessageResId; - mCategories = clone(source.mCategories); + mCategories = cloneCategories(source.mCategories); if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) { mIntent = source.mIntent; mIntentPersistableExtras = source.mIntentPersistableExtras; @@ -614,7 +624,7 @@ public final class ShortcutInfo implements Parcelable { mDisabledMessageResName = null; } if (source.mCategories != null) { - mCategories = clone(source.mCategories); + mCategories = cloneCategories(source.mCategories); } if (source.mIntent != null) { mIntent = source.mIntent; @@ -752,7 +762,7 @@ public final class ShortcutInfo implements Parcelable { * an icon. The recommend max length is 10 characters. */ @NonNull - public Builder setShortLabel(@NonNull String shortLabel) { + public Builder setShortLabel(@NonNull CharSequence shortLabel) { Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set"); mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel"); return this; @@ -776,14 +786,14 @@ public final class ShortcutInfo implements Parcelable { * The recommend max length is 25 characters. */ @NonNull - public Builder setLongLabel(@NonNull String longLabel) { + public Builder setLongLabel(@NonNull CharSequence longLabel) { Preconditions.checkState(mTextResId == 0, "longLabelResId already set"); mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel"); return this; } /** @hide -- old signature, the internal code still uses it. */ - public Builder setTitle(@NonNull String value) { + public Builder setTitle(@NonNull CharSequence value) { return setShortLabel(value); } @@ -793,7 +803,7 @@ public final class ShortcutInfo implements Parcelable { } /** @hide -- old signature, the internal code still uses it. */ - public Builder setText(@NonNull String value) { + public Builder setText(@NonNull CharSequence value) { return setLongLabel(value); } @@ -813,7 +823,7 @@ public final class ShortcutInfo implements Parcelable { } @NonNull - public Builder setDisabledMessage(@NonNull String disabledMessage) { + public Builder setDisabledMessage(@NonNull CharSequence disabledMessage) { Preconditions.checkState( mDisabledMessageResId == 0, "disabledMessageResId already set"); mDisabledMessage = @@ -1355,6 +1365,37 @@ public final class ShortcutInfo implements Parcelable { mIconResName = iconResName; } + /** + * Replaces the intent + * + * @throws IllegalArgumentException when extra is not compatible with {@link PersistableBundle}. + * + * @hide + */ + public void setIntent(Intent intent) throws IllegalArgumentException { + Preconditions.checkNotNull(intent); + + final Bundle intentExtras = intent.getExtras(); + + mIntent = intent; + + if (intentExtras != null) { + intent.replaceExtras((Bundle) null); + mIntentPersistableExtras = new PersistableBundle(intentExtras); + } else { + mIntentPersistableExtras = null; + } + } + + /** + * Replaces the categories. + * + * @hide + */ + public void setCategories(Set categories) { + mCategories = cloneCategories(categories); + } + private ShortcutInfo(Parcel source) { final ClassLoader cl = getClass().getClassLoader(); @@ -1591,7 +1632,7 @@ public final class ShortcutInfo implements Parcelable { mDisabledMessage = disabledMessage; mDisabledMessageResId = disabledMessageResId; mDisabledMessageResName = disabledMessageResName; - mCategories = clone(categories); + mCategories = cloneCategories(categories); mIntent = intent; mIntentPersistableExtras = intentPersistableExtras; mRank = rank; diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index f0e0ea84a9956..369113c6b1e5a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8253,12 +8253,13 @@ i - + - - - + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index da31767b79726..c187d2ca7d880 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2733,13 +2733,9 @@ =============================================================== --> - - - - diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index ace14acda16df..d637586c8def7 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -682,6 +682,8 @@ class ShortcutPackage extends ShortcutPackageItem { changed |= pushOutExcessShortcuts(); } + s.verifyStates(); + if (changed) { // This will send a notification to the launcher, and also save . s.packageShortcutsChanged(getPackageName(), getPackageUserId()); @@ -774,6 +776,7 @@ class ShortcutPackage extends ShortcutPackageItem { } removeOrphans(); } + adjustRanks(); return changed; } @@ -810,7 +813,6 @@ class ShortcutPackage extends ShortcutPackageItem { deleteDynamicWithId(shortcut.getId()); } } - service.verifyStates(); return changed; } diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java index ec19927ad75a8..c349b758b0cee 100644 --- a/services/core/java/com/android/server/pm/ShortcutParser.java +++ b/services/core/java/com/android/server/pm/ShortcutParser.java @@ -24,10 +24,10 @@ import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; -import android.net.Uri; import android.text.TextUtils; import android.util.ArraySet; import android.util.AttributeSet; +import android.util.Log; import android.util.Slog; import android.util.Xml; @@ -48,10 +48,12 @@ public class ShortcutParser { private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE @VisibleForTesting - static final String METADATA_KEY = "android.pm.Shortcuts"; + static final String METADATA_KEY = "android.app.shortcuts"; private static final String TAG_SHORTCUTS = "shortcuts"; private static final String TAG_SHORTCUT = "shortcut"; + private static final String TAG_INTENT = "intent"; + private static final String TAG_CATEGORIES = "categories"; @Nullable public static List parseShortcuts(ShortcutService service, @@ -60,10 +62,17 @@ public class ShortcutParser { List result = null; - if (pi != null && pi.activities != null) { - for (ActivityInfo activityInfo : pi.activities) { - result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result); + try { + if (pi != null && pi.activities != null) { + for (ActivityInfo activityInfo : pi.activities) { + result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result); + } } + } catch (RuntimeException e) { + // Resource ID mismatch may cause various runtime exceptions when parsing XMLs. + service.wtf( + "Exception caught while parsing shortcut XML for package=" + packageName, e); + return null; } return result; } @@ -89,14 +98,58 @@ public class ShortcutParser { final int maxShortcuts = service.getMaxActivityShortcuts(); int numShortcuts = 0; + // We instantiate ShortcutInfo at , but we add it to the list at , + // after parsing . We keep the current one in here. + ShortcutInfo currentShortcut = null; + + Set categories = null; + outer: while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { + final int depth = parser.getDepth(); + final String tag = parser.getName(); + + // When a shortcut tag is closing, publish. + if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) { + if (currentShortcut == null) { + // Shortcut was invalid. + continue; + } + final ShortcutInfo si = currentShortcut; + currentShortcut = null; // Make sure to null out for the next iteration. + + if (si.getIntent() == null) { + Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it."); + continue; + } + + if (numShortcuts >= maxShortcuts) { + Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for " + + activityInfo.getComponentName() + ". Skipping the rest."); + return result; + } + if (categories != null) { + si.setCategories(categories); + categories = null; + } + + if (result == null) { + result = new ArrayList<>(); + } + result.add(si); + numShortcuts++; + rank++; + if (ShortcutService.DEBUG) { + Slog.d(TAG, "Shortcut added: " + si.toInsecureString()); + } + continue; + } + + // Otherwise, just look at start tags. if (type != XmlPullParser.START_TAG) { continue; } - final int depth = parser.getDepth(); - final String tag = parser.getName(); if (depth == 1 && TAG_SHORTCUTS.equals(tag)) { continue; // Root tag. @@ -104,35 +157,73 @@ public class ShortcutParser { if (depth == 2 && TAG_SHORTCUT.equals(tag)) { final ShortcutInfo si = parseShortcutAttributes( service, attrs, packageName, activity, userId, rank); + if (si == null) { + // Shortcut was invalid. + continue; + } if (ShortcutService.DEBUG) { - Slog.d(TAG, "Shortcut=" + si); + Slog.d(TAG, "Shortcut found: " + si.toInsecureString()); } if (result != null) { for (int i = result.size() - 1; i >= 0; i--) { if (si.getId().equals(result.get(i).getId())) { - Slog.w(TAG, "Duplicate shortcut ID detected, skipping."); + Log.e(TAG, "Duplicate shortcut ID detected. Skipping it."); continue outer; } } } + if (!si.isEnabled()) { + // Just set the default intent to disabled shortcuts. + si.setIntent(new Intent(Intent.ACTION_VIEW)); + } + currentShortcut = si; + categories = null; + continue; + } + if (depth == 3 && TAG_INTENT.equals(tag)) { + if ((currentShortcut == null) + || (currentShortcut.getIntentNoExtras() != null) + || !currentShortcut.isEnabled()) { + Log.e(TAG, "Ignoring excessive intent tag."); + continue; + } - 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++; - rank++; + final Intent intent = Intent.parseIntent(service.mContext.getResources(), + parser, attrs); + if (TextUtils.isEmpty(intent.getAction())) { + Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity); + continue; + } + try { + currentShortcut.setIntent(intent); + } catch (RuntimeException e) { + // This shouldn't happen because intents in XML can't have complicated + // extras, but just in case Intent.parseIntent() supports such a thing one + // day. + Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it."); + continue; } continue; } - Slog.w(TAG, "Unknown tag " + tag); + if (depth == 3 && TAG_CATEGORIES.equals(tag)) { + if ((currentShortcut == null) + || (currentShortcut.getCategories() != null)) { + continue; + } + final String name = parseCategories(service, attrs); + if (TextUtils.isEmpty(name)) { + Log.e(TAG, "Empty category found. activity=" + activity); + continue; + } + + if (categories == null) { + categories = new ArraySet<>(); + } + categories.add(name); + continue; + } + + Log.w(TAG, "Unknown tag " + tag + " at depth " + depth); } } finally { if (parser != null) { @@ -142,6 +233,16 @@ public class ShortcutParser { return result; } + private static String parseCategories(ShortcutService service, AttributeSet attrs) { + final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, + R.styleable.ShortcutCategories); + try { + return sa.getString(R.styleable.ShortcutCategories_name); + } finally { + sa.recycle(); + } + } + private static ShortcutInfo parseShortcutAttributes(ShortcutService service, AttributeSet attrs, String packageName, ComponentName activity, @UserIdInt int userId, int rank) { @@ -150,14 +251,11 @@ public class ShortcutParser { try { final String id = sa.getString(R.styleable.Shortcut_shortcutId); final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); - final int iconResId = sa.getResourceId(R.styleable.Shortcut_shortcutIcon, 0); + final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0); final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0); final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); final int disabledMessageResId = sa.getResourceId( R.styleable.Shortcut_shortcutDisabledMessage, 0); - final String categories = sa.getString(R.styleable.Shortcut_shortcutCategories); - String intentAction = sa.getString(R.styleable.Shortcut_shortcutIntentAction); - final String intentData = sa.getString(R.styleable.Shortcut_shortcutIntentData); if (TextUtils.isEmpty(id)) { Slog.w(TAG, "Shortcut ID must be provided. activity=" + activity); @@ -167,31 +265,6 @@ public class ShortcutParser { Slog.w(TAG, "Shortcut title must be provided. activity=" + activity); return null; } - if (TextUtils.isEmpty(intentAction)) { - if (enabled) { - Slog.w(TAG, "Shortcut intent action must be provided. activity=" + activity); - return null; - } else { - // Disabled shortcut doesn't have to have an action, but just set VIEW as the - // default. - intentAction = Intent.ACTION_VIEW; - } - } - - final ArraySet categoriesSet; - if (categories == null) { - categoriesSet = null; - } else { - final String[] arr = categories.split(":"); - categoriesSet = new ArraySet<>(arr.length); - for (String v : arr) { - categoriesSet.add(v); - } - } - final Intent intent = new Intent(intentAction); - if (!TextUtils.isEmpty(intentData)) { - intent.setData(Uri.parse(intentData)); - } return createShortcutFromManifest( service, @@ -202,8 +275,6 @@ public class ShortcutParser { titleResId, textResId, disabledMessageResId, - categoriesSet, - intent, rank, iconResId, enabled); @@ -214,8 +285,8 @@ public class ShortcutParser { private static ShortcutInfo createShortcutFromManifest(ShortcutService service, @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, - int titleResId, int textResId, int disabledMessageResId, Set categories, - Intent intent, int rank, int iconResId, boolean enabled) { + int titleResId, int textResId, int disabledMessageResId, + int rank, int iconResId, boolean enabled) { final int flags = (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) @@ -239,8 +310,8 @@ public class ShortcutParser { null, // disabled message string disabledMessageResId, null, // disabled message res name - categories, - intent, + null, // categories + null, // intent null, // intent extras rank, null, // extras diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 16191687770c4..be8eeed728259 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1510,6 +1510,10 @@ public class ShortcutService extends IShortcutService.Stub { ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); + for (int i = 0; i < size; i++) { + fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false); + } + // First, remove all un-pinned; dynamic shortcuts ps.deleteAllDynamicShortcuts(); diff --git a/services/tests/servicestests/res/drawable/icon3.png b/services/tests/servicestests/res/drawable/icon3.png new file mode 100644 index 0000000000000..64eb294363d27 Binary files /dev/null and b/services/tests/servicestests/res/drawable/icon3.png differ diff --git a/services/tests/servicestests/res/xml/shortcut_1.xml b/services/tests/servicestests/res/xml/shortcut_1.xml index f6d54fce84482..e3f9172cc23a6 100644 --- a/services/tests/servicestests/res/xml/shortcut_1.xml +++ b/services/tests/servicestests/res/xml/shortcut_1.xml @@ -2,12 +2,17 @@ + > + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_1_alt.xml b/services/tests/servicestests/res/xml/shortcut_1_alt.xml index bf14f49cae9c4..2d5e8e7cbd77c 100644 --- a/services/tests/servicestests/res/xml/shortcut_1_alt.xml +++ b/services/tests/servicestests/res/xml/shortcut_1_alt.xml @@ -17,12 +17,17 @@ + > + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_1_disable.xml b/services/tests/servicestests/res/xml/shortcut_1_disable.xml index 81a84b40a8bbe..e3ee3a0786e52 100644 --- a/services/tests/servicestests/res/xml/shortcut_1_disable.xml +++ b/services/tests/servicestests/res/xml/shortcut_1_disable.xml @@ -17,7 +17,7 @@ + > + + + + + + > + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml index 2f814b78cc313..b00ec60e0ff57 100644 --- a/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml +++ b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml @@ -17,11 +17,19 @@ + > + + + + > + + + diff --git a/services/tests/servicestests/res/xml/shortcut_5.xml b/services/tests/servicestests/res/xml/shortcut_5.xml index 56dba0ef52970..9551100cefe1f 100644 --- a/services/tests/servicestests/res/xml/shortcut_5.xml +++ b/services/tests/servicestests/res/xml/shortcut_5.xml @@ -17,37 +17,69 @@ + > + + + + + + > + + + + + > + + + + android:shortcutShortLabel="@string/shortcut_title2" + > + + + + + + + > + + + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_5_alt.xml b/services/tests/servicestests/res/xml/shortcut_5_alt.xml index 74085d9ee1e18..f79cd6f15737a 100644 --- a/services/tests/servicestests/res/xml/shortcut_5_alt.xml +++ b/services/tests/servicestests/res/xml/shortcut_5_alt.xml @@ -17,37 +17,58 @@ + > + + + + + + > + + + + + > + + + + > + + + + > + + + diff --git a/services/tests/servicestests/res/xml/shortcut_5_reverse.xml b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml index 3d6eb222ce5b4..d5b7c8fde0bfe 100644 --- a/services/tests/servicestests/res/xml/shortcut_5_reverse.xml +++ b/services/tests/servicestests/res/xml/shortcut_5_reverse.xml @@ -17,37 +17,62 @@ + > + + + + + + > + + + + + > + + + + > + + + + > + + + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_1.xml b/services/tests/servicestests/res/xml/shortcut_error_1.xml index 5822496fac113..3990d0270506f 100644 --- a/services/tests/servicestests/res/xml/shortcut_error_1.xml +++ b/services/tests/servicestests/res/xml/shortcut_error_1.xml @@ -16,11 +16,19 @@ + > + + + + > + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_2.xml b/services/tests/servicestests/res/xml/shortcut_error_2.xml index ca67ec70af2fe..a6f715025cc67 100644 --- a/services/tests/servicestests/res/xml/shortcut_error_2.xml +++ b/services/tests/servicestests/res/xml/shortcut_error_2.xml @@ -16,11 +16,19 @@ + > + + + + > + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_3.xml b/services/tests/servicestests/res/xml/shortcut_error_3.xml index fb7b31cf2e1c1..a7b9b84a9e3cc 100644 --- a/services/tests/servicestests/res/xml/shortcut_error_3.xml +++ b/services/tests/servicestests/res/xml/shortcut_error_3.xml @@ -21,6 +21,10 @@ + > + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_4.xml b/services/tests/servicestests/res/xml/shortcut_error_4.xml new file mode 100644 index 0000000000000..3697bb4144d6e --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_error_4.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 de06047cf14e0..56232c03ea4d1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -4884,6 +4884,117 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } + public void testManifestShortcuts_intentDefinitions() { + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_error_4); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + // Make sure invalid ones are not published. + // Note that at this point disabled ones don't show up because they weren't pinned. + assertWith(getCallerShortcuts()) + .haveIds("ms1", "ms2") + .areAllManifest() + .areAllNotDynamic() + .areAllNotPinned() + .areAllImmutable() + .areAllEnabled() + .forShortcutWithId("ms1", si -> { + assertTrue(si.isEnabled()); + assertEquals("action1", si.getIntent().getAction()); + }) + .forShortcutWithId("ms2", si -> { + assertTrue(si.isEnabled()); + assertEquals("action2_1", si.getIntent().getAction()); + }); + }); + + // Publish 5 enabled to pin some, so we can later test disabled manfiest shortcuts.. + 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, () -> { + // Make sure 5 manifest shortcuts are published. + assertWith(getCallerShortcuts()) + .haveIds("ms1", "ms2", "ms3", "ms4", "ms5") + .areAllManifest() + .areAllNotDynamic() + .areAllNotPinned() + .areAllImmutable() + .areAllEnabled(); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, + list("ms3", "ms4", "ms5"), HANDLE_USER_0); + }); + + // Make sure they're pinned. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertWith(getCallerShortcuts()) + .haveIds("ms1", "ms2", "ms3", "ms4", "ms5") + .selectByIds("ms1", "ms2") + .areAllNotPinned() + .areAllEnabled() + + .revertToOriginalList() + .selectByIds("ms3", "ms4", "ms5") + .areAllPinned() + .areAllEnabled(); + }); + + // Update the app. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_error_4); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Make sure 3, 4 and 5 still exist but disabled. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertWith(getCallerShortcuts()) + .haveIds("ms1", "ms2", "ms3", "ms4", "ms5") + .areAllNotDynamic() + .areAllImmutable() + + .selectByIds("ms1", "ms2") + .areAllManifest() + .areAllNotPinned() + .areAllEnabled() + + .revertToOriginalList() + .selectByIds("ms3", "ms4", "ms5") + .areAllNotManifest() + .areAllPinned() + .areAllDisabled() + + .revertToOriginalList() + .forShortcutWithId("ms1", si -> { + assertEquals(si.getId(), "action1", si.getIntent().getAction()); + }) + .forShortcutWithId("ms2", si -> { + assertEquals(si.getId(), "action2_1", si.getIntent().getAction()); + }) + .forShortcutWithId("ms3", si -> { + assertEquals(si.getId(), Intent.ACTION_VIEW, si.getIntent().getAction()); + }) + .forShortcutWithId("ms4", si -> { + assertEquals(si.getId(), Intent.ACTION_VIEW, si.getIntent().getAction()); + }) + .forShortcutWithId("ms5", si -> { + assertEquals(si.getId(), "action", si.getIntent().getAction()); + }); + }); + } + public void testManifestShortcuts_checkAllFields() { mService.handleUnlockUser(USER_0); @@ -4897,63 +5008,95 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // Only the valid one is published. runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { - assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( - mManager.getManifestShortcuts()))), - "ms1", "ms2", "ms3", "ms4", "ms5"); + assertWith(getCallerShortcuts()) + .haveIds("ms1", "ms2", "ms3", "ms4", "ms5") + .areAllManifest() + .areAllImmutable() + .areAllEnabled() + .areAllNotPinned() + .areAllNotDynamic() - // check first shortcut. - ShortcutInfo si = getCallerShortcut("ms1"); + .forShortcutWithId("ms1", si -> { + assertEquals(R.drawable.icon1, si.getIconResourceId()); + assertEquals(new ComponentName(CALLING_PACKAGE_1, + ShortcutActivity.class.getName()), + si.getActivity()); - assertEquals("ms1", si.getId()); - assertEquals(R.drawable.icon1, si.getIconResourceId()); - assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), - si.getActivity()); + assertEquals(R.string.shortcut_title1, si.getTitleResId()); + assertEquals("r" + R.string.shortcut_title1, si.getTitleResName()); + assertEquals(R.string.shortcut_text1, si.getTextResId()); + assertEquals("r" + R.string.shortcut_text1, si.getTextResName()); + assertEquals(R.string.shortcut_disabled_message1, + si.getDisabledMessageResourceId()); + assertEquals("r" + R.string.shortcut_disabled_message1, + si.getDisabledMessageResName()); - assertEquals(R.string.shortcut_title1, si.getTitleResId()); - assertEquals("r" + R.string.shortcut_title1, si.getTitleResName()); - assertEquals(R.string.shortcut_text1, si.getTextResId()); - assertEquals("r" + R.string.shortcut_text1, si.getTextResName()); - assertEquals(R.string.shortcut_disabled_message1, si.getDisabledMessageResourceId()); - assertEquals("r" + R.string.shortcut_disabled_message1, si.getDisabledMessageResName()); + assertEquals(set("android.shortcut.conversation", "android.shortcut.media"), + si.getCategories()); + assertEquals("action1", si.getIntent().getAction()); + assertEquals(Uri.parse("http://a.b.c/1"), si.getIntent().getData()); + }) - assertEquals(set("android.shortcut.conversation", "android.shortcut.media"), - si.getCategories()); - assertEquals("action1", si.getIntent().getAction()); - assertEquals(Uri.parse("http://a.b.c/1"), si.getIntent().getData()); + .forShortcutWithId("ms2", si -> { + assertEquals("ms2", si.getId()); + assertEquals(R.drawable.icon2, si.getIconResourceId()); - // check another - si = getCallerShortcut("ms2"); + assertEquals(R.string.shortcut_title2, si.getTitleResId()); + assertEquals("r" + R.string.shortcut_title2, si.getTitleResName()); + assertEquals(R.string.shortcut_text2, si.getTextResId()); + assertEquals("r" + R.string.shortcut_text2, si.getTextResName()); + assertEquals(R.string.shortcut_disabled_message2, + si.getDisabledMessageResourceId()); + assertEquals("r" + R.string.shortcut_disabled_message2, + si.getDisabledMessageResName()); - assertEquals("ms2", si.getId()); - assertEquals(R.drawable.icon2, si.getIconResourceId()); + assertEquals(set("android.shortcut.conversation"), si.getCategories()); + assertEquals("action2", si.getIntent().getAction()); + assertEquals(null, si.getIntent().getData()); + }) - assertEquals(R.string.shortcut_title2, si.getTitleResId()); - assertEquals("r" + R.string.shortcut_title2, si.getTitleResName()); - assertEquals(R.string.shortcut_text2, si.getTextResId()); - assertEquals("r" + R.string.shortcut_text2, si.getTextResName()); - assertEquals(R.string.shortcut_disabled_message2, si.getDisabledMessageResourceId()); - assertEquals("r" + R.string.shortcut_disabled_message2, si.getDisabledMessageResName()); + .forShortcutWithId("ms3", si -> { + assertEquals(0, si.getIconResourceId()); + assertEquals(R.string.shortcut_title1, si.getTitleResId()); + assertEquals("r" + R.string.shortcut_title1, si.getTitleResName()); - assertEquals(set("android.shortcut.conversation"), si.getCategories()); - assertEquals("action2", si.getIntent().getAction()); - assertEquals(null, si.getIntent().getData()); + assertEquals(0, si.getTextResId()); + assertEquals(null, si.getTextResName()); + assertEquals(0, si.getDisabledMessageResourceId()); + assertEquals(null, si.getDisabledMessageResName()); - // check another - si = getCallerShortcut("ms3"); + assertEmpty(si.getCategories()); + assertEquals("android.intent.action.VIEW", si.getIntent().getAction()); + assertEquals(null, si.getIntent().getData()); + }) - assertEquals("ms3", si.getId()); - assertEquals(0, si.getIconResourceId()); - assertEquals(R.string.shortcut_title1, si.getTitleResId()); - assertEquals("r" + R.string.shortcut_title1, si.getTitleResName()); + .forShortcutWithId("ms4", si -> { + assertEquals(0, si.getIconResourceId()); + assertEquals(R.string.shortcut_title2, si.getTitleResId()); + assertEquals("r" + R.string.shortcut_title2, si.getTitleResName()); - assertEquals(0, si.getTextResId()); - assertEquals(null, si.getTextResName()); - assertEquals(0, si.getDisabledMessageResourceId()); - assertEquals(null, si.getDisabledMessageResName()); + assertEquals(0, si.getTextResId()); + assertEquals(null, si.getTextResName()); + assertEquals(0, si.getDisabledMessageResourceId()); + assertEquals(null, si.getDisabledMessageResName()); - assertEquals(null, si.getCategories()); - assertEquals("android.intent.action.VIEW", si.getIntent().getAction()); - assertEquals(null, si.getIntent().getData()); + assertEquals(set("cat"), si.getCategories()); + assertEquals("android.intent.action.VIEW2", si.getIntent().getAction()); + assertEquals(null, si.getIntent().getData()); + }) + + .forShortcutWithId("ms5", si -> { + si = getCallerShortcut("ms5"); + assertEquals("action", si.getIntent().getAction()); + assertEquals("http://www/", si.getIntent().getData().toString()); + assertEquals("foo/bar", si.getIntent().getType()); + assertEquals( + new ComponentName("abc", ".xyz"), si.getIntent().getComponent()); + + assertEquals(set("cat1", "cat2"), si.getIntent().getCategories()); + assertEquals("value1", si.getIntent().getStringExtra("key1")); + assertEquals("value2", si.getIntent().getStringExtra("key2")); + }); }); } 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 7ba4c6830a5c3..712bc1e8c3011 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 @@ -43,9 +43,11 @@ import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.test.MoreAsserts; +import android.util.ArraySet; import android.util.Log; import junit.framework.Assert; +import junit.framework.AssertionFailedError; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -66,6 +68,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.BooleanSupplier; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -259,9 +262,12 @@ public class ShortcutManagerTestUtils { } } - public static List assertEmpty(List list) { - assertEquals(0, list.size()); - return list; + public static > T assertEmpty(T collection) { + if (collection == null) { + return collection; // okay. + } + assertEquals(0, collection.size()); + return collection; } public static List filter(List list, Predicate p) { @@ -653,42 +659,73 @@ public class ShortcutManagerTestUtils { * New style assertion that allows chained calls. */ public static class ShortcutListAsserter { + private final ShortcutListAsserter mOriginal; private final List mList; ShortcutListAsserter(List list) { + this(null, list); + } + + private ShortcutListAsserter(ShortcutListAsserter original, List list) { + mOriginal = original == null ? this : original; mList = new ArrayList<>(list); } + public ShortcutListAsserter revertToOriginalList() { + return mOriginal; + } + public ShortcutListAsserter selectDynamic() { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, filter(mList, ShortcutInfo::isDynamic)); } public ShortcutListAsserter selectManifest() { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, filter(mList, ShortcutInfo::isManifestShortcut)); } public ShortcutListAsserter selectPinned() { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, filter(mList, ShortcutInfo::isPinned)); } public ShortcutListAsserter selectByActivity(ComponentName activity) { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, ShortcutManagerTestUtils.filterByActivity(mList, activity)); } public ShortcutListAsserter selectByChangedSince(long time) { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, ShortcutManagerTestUtils.changedSince(mList, time)); } + public ShortcutListAsserter selectByIds(String... ids) { + final Set idSet = set(ids); + final ArrayList selected = new ArrayList<>(); + for (ShortcutInfo si : mList) { + if (idSet.contains(si.getId())) { + selected.add(si); + idSet.remove(si.getId()); + } + } + if (idSet.size() > 0) { + fail("Shortcuts not found for IDs=" + idSet); + } + + return new ShortcutListAsserter(this, selected); + } + public ShortcutListAsserter toSortByRank() { - return new ShortcutListAsserter( + return new ShortcutListAsserter(this, ShortcutManagerTestUtils.sortedByRank(mList)); } + public ShortcutListAsserter call(Consumer> c) { + c.accept(mList); + return this; + } + public ShortcutListAsserter haveIds(String... expectedIds) { assertShortcutIds(mList, expectedIds); return this; @@ -701,7 +738,8 @@ public class ShortcutManagerTestUtils { private ShortcutListAsserter haveSequentialRanks() { for (int i = 0; i < mList.size(); i++) { - assertEquals("Rank not sequential", i, mList.get(i).getRank()); + final ShortcutInfo si = mList.get(i); + assertEquals("Rank not sequential: id=" + si.getId(), i, si.getRank()); } return this; } @@ -717,5 +755,87 @@ public class ShortcutManagerTestUtils { assertEquals(0, mList.size()); return this; } + + public ShortcutListAsserter areAllDynamic() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isDynamic())); + return this; + } + + public ShortcutListAsserter areAllNotDynamic() { + forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isDynamic())); + return this; + } + + public ShortcutListAsserter areAllPinned() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isPinned())); + return this; + } + + public ShortcutListAsserter areAllNotPinned() { + forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isPinned())); + return this; + } + + public ShortcutListAsserter areAllManifest() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isManifestShortcut())); + return this; + } + + public ShortcutListAsserter areAllNotManifest() { + forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isManifestShortcut())); + return this; + } + + public ShortcutListAsserter areAllImmutable() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isImmutable())); + return this; + } + + public ShortcutListAsserter areAllMutable() { + forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isImmutable())); + return this; + } + + public ShortcutListAsserter areAllEnabled() { + forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isEnabled())); + return this; + } + + public ShortcutListAsserter areAllDisabled() { + forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isEnabled())); + return this; + } + + public ShortcutListAsserter forAllShortcuts(Consumer sa) { + for (int i = 0; i < mList.size(); i++) { + final ShortcutInfo si = mList.get(i); + sa.accept(si); + } + return this; + } + + public ShortcutListAsserter forShortcut(Predicate p, + Consumer sa) { + boolean found = false; + for (int i = 0; i < mList.size(); i++) { + final ShortcutInfo si = mList.get(i); + if (p.test(si)) { + found = true; + try { + sa.accept(si); + } catch (Throwable e) { + throw new AssertionError("Assertion failed for shortcut " + si.getId(), e); + } + } + } + assertTrue("Shortcut with the given condition not found.", found); + return this; + } + + public ShortcutListAsserter forShortcutWithId(String id, Consumer sa) { + forShortcut(si -> si.getId().equals(id), sa); + + return this; + } } }