diff --git a/api/current.txt b/api/current.txt index 427d9b3176bad..114ef0d3e503e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1106,6 +1106,15 @@ 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 = 16844078; // 0x101052e + field public static final int shortcutDisabledMessage = 16844077; // 0x101052d + field public static final int shortcutIcon = 16844074; // 0x101052a + field public static final int shortcutId = 16844072; // 0x1010528 + field public static final int shortcutIntentAction = 16844079; // 0x101052f + field public static final int shortcutIntentData = 16844080; // 0x1010530 + field public static final int shortcutRank = 16844073; // 0x1010529 + field public static final int shortcutText = 16844076; // 0x101052c + field public static final int shortcutTitle = 16844075; // 0x101052b field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa @@ -9534,13 +9543,14 @@ package android.content.pm { public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); - method public void setActivity(android.content.ComponentName); - method public void setChangedSince(long); - method public void setPackage(java.lang.String); - method public void setQueryFlags(int); - method public void setShortcutIds(java.util.List); + method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); + method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long); + method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String); + method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int); + method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List); field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_MANIFEST = 8; // 0x8 field public static final int FLAG_GET_PINNED = 2; // 0x2 } @@ -10044,29 +10054,30 @@ package android.content.pm { public final class ShortcutInfo implements android.os.Parcelable { method public int describeContents(); - method public android.content.ComponentName getActivityComponent(); + method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); - method public java.lang.String getDisabledMessage(); + method public java.lang.CharSequence getDisabledMessage(); method public int getDisabledMessageResId(); method public android.os.PersistableBundle getExtras(); method public int getIconResourceId(); method public java.lang.String getId(); method public android.content.Intent getIntent(); method public long getLastChangedTimestamp(); - method public java.lang.String getPackageName(); + method public java.lang.String getPackage(); method public int getRank(); - method public java.lang.String getText(); + method public java.lang.CharSequence getText(); method public int getTextResId(); - method public java.lang.String getTitle(); + method public java.lang.CharSequence getTitle(); method public int getTitleResId(); method public android.os.UserHandle getUserHandle(); method public boolean hasIconFile(); method public boolean hasIconResource(); method public boolean hasKeyFieldsOnly(); method public boolean hasStringResourcesResolved(); - method public boolean isDisabled(); method public boolean isDynamic(); - method public boolean isFromManifest(); + method public boolean isEnabled(); + method public boolean isImmutable(); + method public boolean isManifestShortcut(); method public boolean isPinned(); method public void writeToParcel(android.os.Parcel, int); field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 @@ -10075,10 +10086,11 @@ package android.content.pm { field public static final android.os.Parcelable.Creator CREATOR; field public static final int FLAG_DISABLED = 64; // 0x40 field public static final int FLAG_DYNAMIC = 1; // 0x1 - field public static final int FLAG_FROM_MANIFEST = 32; // 0x20 field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_IMMUTABLE = 256; // 0x100 field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10 + field public static final int FLAG_MANIFEST = 32; // 0x20 field public static final int FLAG_PINNED = 2; // 0x2 field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80 field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation"; @@ -10087,19 +10099,16 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context); method public android.content.pm.ShortcutInfo build(); - method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + 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 setDisabledMessageResId(int); 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 setRank(int); method public android.content.pm.ShortcutInfo.Builder setText(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTextResId(int); method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTitleResId(int); } public class ShortcutManager { @@ -10107,9 +10116,12 @@ package android.content.pm { method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, int); method public void disableShortcuts(java.util.List, java.lang.String); + method public void enableShortcuts(java.util.List); method public java.util.List getDynamicShortcuts(); - method public int getIconMaxDimensions(); - method public int getMaxDynamicShortcutCount(); + method public int getIconMaxHeight(); + method public int getIconMaxWidth(); + method public java.util.List getManifestShortcuts(); + method public int getMaxShortcutCountForActivity(); method public java.util.List getPinnedShortcuts(); method public long getRateLimitResetTime(); method public int getRemainingCallCount(); diff --git a/api/system-current.txt b/api/system-current.txt index 3e8d6fa5dabb4..505052da3d252 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1216,6 +1216,15 @@ 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 = 16844078; // 0x101052e + field public static final int shortcutDisabledMessage = 16844077; // 0x101052d + field public static final int shortcutIcon = 16844074; // 0x101052a + field public static final int shortcutId = 16844072; // 0x1010528 + field public static final int shortcutIntentAction = 16844079; // 0x101052f + field public static final int shortcutIntentData = 16844080; // 0x1010530 + field public static final int shortcutRank = 16844073; // 0x1010529 + field public static final int shortcutText = 16844076; // 0x101052c + field public static final int shortcutTitle = 16844075; // 0x101052b field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa @@ -9887,13 +9896,14 @@ package android.content.pm { public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); - method public void setActivity(android.content.ComponentName); - method public void setChangedSince(long); - method public void setPackage(java.lang.String); - method public void setQueryFlags(int); - method public void setShortcutIds(java.util.List); + method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); + method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long); + method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String); + method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int); + method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List); field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_MANIFEST = 8; // 0x8 field public static final int FLAG_GET_PINNED = 2; // 0x2 } @@ -10467,29 +10477,30 @@ package android.content.pm { public final class ShortcutInfo implements android.os.Parcelable { method public int describeContents(); - method public android.content.ComponentName getActivityComponent(); + method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); - method public java.lang.String getDisabledMessage(); + method public java.lang.CharSequence getDisabledMessage(); method public int getDisabledMessageResId(); method public android.os.PersistableBundle getExtras(); method public int getIconResourceId(); method public java.lang.String getId(); method public android.content.Intent getIntent(); method public long getLastChangedTimestamp(); - method public java.lang.String getPackageName(); + method public java.lang.String getPackage(); method public int getRank(); - method public java.lang.String getText(); + method public java.lang.CharSequence getText(); method public int getTextResId(); - method public java.lang.String getTitle(); + method public java.lang.CharSequence getTitle(); method public int getTitleResId(); method public android.os.UserHandle getUserHandle(); method public boolean hasIconFile(); method public boolean hasIconResource(); method public boolean hasKeyFieldsOnly(); method public boolean hasStringResourcesResolved(); - method public boolean isDisabled(); method public boolean isDynamic(); - method public boolean isFromManifest(); + method public boolean isEnabled(); + method public boolean isImmutable(); + method public boolean isManifestShortcut(); method public boolean isPinned(); method public void writeToParcel(android.os.Parcel, int); field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 @@ -10498,10 +10509,11 @@ package android.content.pm { field public static final android.os.Parcelable.Creator CREATOR; field public static final int FLAG_DISABLED = 64; // 0x40 field public static final int FLAG_DYNAMIC = 1; // 0x1 - field public static final int FLAG_FROM_MANIFEST = 32; // 0x20 field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_IMMUTABLE = 256; // 0x100 field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10 + field public static final int FLAG_MANIFEST = 32; // 0x20 field public static final int FLAG_PINNED = 2; // 0x2 field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80 field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation"; @@ -10510,19 +10522,16 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context); method public android.content.pm.ShortcutInfo build(); - method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + 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 setDisabledMessageResId(int); 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 setRank(int); method public android.content.pm.ShortcutInfo.Builder setText(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTextResId(int); method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTitleResId(int); } public class ShortcutManager { @@ -10530,9 +10539,12 @@ package android.content.pm { method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, int); method public void disableShortcuts(java.util.List, java.lang.String); + method public void enableShortcuts(java.util.List); method public java.util.List getDynamicShortcuts(); - method public int getIconMaxDimensions(); - method public int getMaxDynamicShortcutCount(); + method public int getIconMaxHeight(); + method public int getIconMaxWidth(); + method public java.util.List getManifestShortcuts(); + method public int getMaxShortcutCountForActivity(); method public java.util.List getPinnedShortcuts(); method public long getRateLimitResetTime(); method public int getRemainingCallCount(); diff --git a/api/test-current.txt b/api/test-current.txt index 1448c6ca46441..a7d4f9a179d34 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -1106,6 +1106,15 @@ 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 = 16844078; // 0x101052e + field public static final int shortcutDisabledMessage = 16844077; // 0x101052d + field public static final int shortcutIcon = 16844074; // 0x101052a + field public static final int shortcutId = 16844072; // 0x1010528 + field public static final int shortcutIntentAction = 16844079; // 0x101052f + field public static final int shortcutIntentData = 16844080; // 0x1010530 + field public static final int shortcutRank = 16844073; // 0x1010529 + field public static final int shortcutText = 16844076; // 0x101052c + field public static final int shortcutTitle = 16844075; // 0x101052b field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int showAsAction = 16843481; // 0x10102d9 field public static final int showDefault = 16843258; // 0x10101fa @@ -9546,13 +9555,14 @@ package android.content.pm { public static class LauncherApps.ShortcutQuery { ctor public LauncherApps.ShortcutQuery(); - method public void setActivity(android.content.ComponentName); - method public void setChangedSince(long); - method public void setPackage(java.lang.String); - method public void setQueryFlags(int); - method public void setShortcutIds(java.util.List); + method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName); + method public android.content.pm.LauncherApps.ShortcutQuery setChangedSince(long); + method public android.content.pm.LauncherApps.ShortcutQuery setPackage(java.lang.String); + method public android.content.pm.LauncherApps.ShortcutQuery setQueryFlags(int); + method public android.content.pm.LauncherApps.ShortcutQuery setShortcutIds(java.util.List); field public static final int FLAG_GET_DYNAMIC = 1; // 0x1 field public static final int FLAG_GET_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_GET_MANIFEST = 8; // 0x8 field public static final int FLAG_GET_PINNED = 2; // 0x2 } @@ -10057,29 +10067,30 @@ package android.content.pm { public final class ShortcutInfo implements android.os.Parcelable { method public int describeContents(); - method public android.content.ComponentName getActivityComponent(); + method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); - method public java.lang.String getDisabledMessage(); + method public java.lang.CharSequence getDisabledMessage(); method public int getDisabledMessageResId(); method public android.os.PersistableBundle getExtras(); method public int getIconResourceId(); method public java.lang.String getId(); method public android.content.Intent getIntent(); method public long getLastChangedTimestamp(); - method public java.lang.String getPackageName(); + method public java.lang.String getPackage(); method public int getRank(); - method public java.lang.String getText(); + method public java.lang.CharSequence getText(); method public int getTextResId(); - method public java.lang.String getTitle(); + method public java.lang.CharSequence getTitle(); method public int getTitleResId(); method public android.os.UserHandle getUserHandle(); method public boolean hasIconFile(); method public boolean hasIconResource(); method public boolean hasKeyFieldsOnly(); method public boolean hasStringResourcesResolved(); - method public boolean isDisabled(); method public boolean isDynamic(); - method public boolean isFromManifest(); + method public boolean isEnabled(); + method public boolean isImmutable(); + method public boolean isManifestShortcut(); method public boolean isPinned(); method public void writeToParcel(android.os.Parcel, int); field public static final int CLONE_REMOVE_FOR_CREATOR = 1; // 0x1 @@ -10088,10 +10099,11 @@ package android.content.pm { field public static final android.os.Parcelable.Creator CREATOR; field public static final int FLAG_DISABLED = 64; // 0x40 field public static final int FLAG_DYNAMIC = 1; // 0x1 - field public static final int FLAG_FROM_MANIFEST = 32; // 0x20 field public static final int FLAG_HAS_ICON_FILE = 8; // 0x8 field public static final int FLAG_HAS_ICON_RES = 4; // 0x4 + field public static final int FLAG_IMMUTABLE = 256; // 0x100 field public static final int FLAG_KEY_FIELDS_ONLY = 16; // 0x10 + field public static final int FLAG_MANIFEST = 32; // 0x20 field public static final int FLAG_PINNED = 2; // 0x2 field public static final int FLAG_STRINGS_RESOLVED = 128; // 0x80 field public static final java.lang.String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation"; @@ -10100,19 +10112,16 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context); method public android.content.pm.ShortcutInfo build(); - method public android.content.pm.ShortcutInfo.Builder setActivityComponent(android.content.ComponentName); + 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 setDisabledMessageResId(int); 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 setRank(int); method public android.content.pm.ShortcutInfo.Builder setText(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTextResId(int); method public android.content.pm.ShortcutInfo.Builder setTitle(java.lang.String); - method public android.content.pm.ShortcutInfo.Builder setTitleResId(int); } public class ShortcutManager { @@ -10121,9 +10130,12 @@ package android.content.pm { method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, int); method public void disableShortcuts(java.util.List, java.lang.String); + method public void enableShortcuts(java.util.List); method public java.util.List getDynamicShortcuts(); - method public int getIconMaxDimensions(); - method public int getMaxDynamicShortcutCount(); + method public int getIconMaxHeight(); + method public int getIconMaxWidth(); + method public java.util.List getManifestShortcuts(); + method public int getMaxShortcutCountForActivity(); method public java.util.List getPinnedShortcuts(); method public long getRateLimitResetTime(); method public int getRemainingCallCount(); diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index 06200bfdb929c..1c373f91fb530 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -28,6 +28,8 @@ interface IShortcutService { ParceledListSlice getDynamicShortcuts(String packageName, int userId); + ParceledListSlice getManifestShortcuts(String packageName, int userId); + boolean addDynamicShortcuts(String packageName, in ParceledListSlice shortcutInfoList, int userId); @@ -42,6 +44,8 @@ interface IShortcutService { void disableShortcuts(String packageName, in List shortcutIds, String disabledMessage, int disabledMessageResId, int userId); + void enableShortcuts(String packageName, in List shortcutIds, int userId); + int getMaxDynamicShortcutCount(String packageName, int userId); int getRemainingCallCount(String packageName, int userId); diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index acd85cb1ed30d..7ee7048b23dbe 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -187,6 +187,11 @@ public class LauncherApps { */ public static final int FLAG_GET_PINNED = 1 << 1; + /** + * Include manifest shortcuts in the result. + */ + public static final int FLAG_GET_MANIFEST = 1 << 3; + /** * Requests "key" fields only. See {@link ShortcutInfo#hasKeyFieldsOnly()} for which * fields are available. @@ -198,6 +203,7 @@ public class LauncherApps { value = { FLAG_GET_DYNAMIC, FLAG_GET_PINNED, + FLAG_GET_MANIFEST, FLAG_GET_KEY_FIELDS_ONLY, }) @Retention(RetentionPolicy.SOURCE) @@ -224,39 +230,44 @@ public class LauncherApps { * If non-zero, returns only shortcuts that have been added or updated since the timestamp, * which is a milliseconds since the Epoch. */ - public void setChangedSince(long changedSince) { + public ShortcutQuery setChangedSince(long changedSince) { mChangedSince = changedSince; + return this; } /** * If non-null, returns only shortcuts from the package. */ - public void setPackage(@Nullable String packageName) { + public ShortcutQuery setPackage(@Nullable String packageName) { mPackage = packageName; + return this; } /** * If non-null, return only the specified shortcuts by ID. When setting this field, * a packange name must also be set with {@link #setPackage}. */ - public void setShortcutIds(@Nullable List shortcutIds) { + public ShortcutQuery setShortcutIds(@Nullable List shortcutIds) { mShortcutIds = shortcutIds; + return this; } /** - * If non-null, returns only shortcuts associated with the activity, which are - * {@link ShortcutInfo}s that have null {@link ShortcutInfo#getActivityComponent()}, or - * {@link ShortcutInfo#getActivityComponent()} equals to {@code activity}. + * If non-null, returns only shortcuts associated with the activity; i.e. + * {@link ShortcutInfo}s whose {@link ShortcutInfo#getActivity()} are equal + * to {@code activity}. */ - public void setActivity(@Nullable ComponentName activity) { + public ShortcutQuery setActivity(@Nullable ComponentName activity) { mActivity = activity; + return this; } /** * Set query options. */ - public void setQueryFlags(@QueryFlags int queryFlags) { + public ShortcutQuery setQueryFlags(@QueryFlags int queryFlags) { mQueryFlags = queryFlags; + return this; } } @@ -525,7 +536,7 @@ public class LauncherApps { */ public ParcelFileDescriptor getShortcutIconFd( @NonNull ShortcutInfo shortcut) { - return getShortcutIconFd(shortcut.getPackageName(), shortcut.getId(), + return getShortcutIconFd(shortcut.getPackage(), shortcut.getId(), shortcut.getUserId()); } @@ -555,14 +566,14 @@ public class LauncherApps { } } - /** TODO Javadoc */ + /** TODO Javadoc (not implemented yet) */ public Drawable getShortcutIconDrawable(int density) { - throw new RuntimeException("TODO implement it"); + throw new RuntimeException("TODO not implemented yet"); } - /** TODO Javadoc */ + /** TODO Javadoc (not implemented yet) */ public Drawable getShortcutBadgedIconDrawable(int density) { - throw new RuntimeException("TODO implement it"); + throw new RuntimeException("TODO not implemented yet"); } /** @@ -600,7 +611,7 @@ public class LauncherApps { */ public boolean startShortcut(@NonNull ShortcutInfo shortcut, @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) { - return startShortcut(shortcut.getPackageName(), shortcut.getId(), + return startShortcut(shortcut.getPackage(), shortcut.getId(), sourceBounds, startActivityOptions, shortcut.getUserId()); } diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 3648d9e1014d6..c83aa225cf346 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -70,7 +70,7 @@ public final class ShortcutInfo implements Parcelable { public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4; /* @hide */ - public static final int FLAG_FROM_MANIFEST = 1 << 5; + public static final int FLAG_MANIFEST = 1 << 5; /* @hide */ public static final int FLAG_DISABLED = 1 << 6; @@ -78,6 +78,9 @@ public final class ShortcutInfo implements Parcelable { /* @hide */ public static final int FLAG_STRINGS_RESOLVED = 1 << 7; + /* @hide */ + public static final int FLAG_IMMUTABLE = 1 << 8; + /** @hide */ @IntDef(flag = true, value = { @@ -86,9 +89,10 @@ public final class ShortcutInfo implements Parcelable { FLAG_HAS_ICON_RES, FLAG_HAS_ICON_FILE, FLAG_KEY_FIELDS_ONLY, - FLAG_FROM_MANIFEST, + FLAG_MANIFEST, FLAG_DISABLED, FLAG_STRINGS_RESOLVED, + FLAG_IMMUTABLE, }) @Retention(RetentionPolicy.SOURCE) public @interface ShortcutFlags {} @@ -133,7 +137,7 @@ public final class ShortcutInfo implements Parcelable { private final String mPackageName; @Nullable - private ComponentName mActivityComponent; + private ComponentName mActivity; @Nullable private Icon mIcon; @@ -141,17 +145,17 @@ public final class ShortcutInfo implements Parcelable { private int mTitleResId; @Nullable - private String mTitle; + private CharSequence mTitle; private int mTextResId; @Nullable - private String mText; + private CharSequence mText; private int mDisabledMessageResId; @Nullable - private String mDisabledMessage; + private CharSequence mDisabledMessage; @Nullable private ArraySet mCategories; @@ -196,7 +200,7 @@ public final class ShortcutInfo implements Parcelable { // Note we can't do other null checks here because SM.updateShortcuts() takes partial // information. mPackageName = b.mContext.getPackageName(); - mActivityComponent = b.mActivityComponent; + mActivity = b.mActivity; mIcon = b.mIcon; mTitle = b.mTitle; mTitleResId = b.mTitleResId; @@ -229,7 +233,7 @@ public final class ShortcutInfo implements Parcelable { */ public void enforceMandatoryFields() { Preconditions.checkStringNotEmpty(mId, "Shortcut ID must be provided"); - Preconditions.checkNotNull(mActivityComponent, "activityComponent must be provided"); + Preconditions.checkNotNull(mActivity, "activity must be provided"); if (mTitle == null && mTitleResId == 0) { throw new IllegalArgumentException("Shortcut title must be provided"); } @@ -250,7 +254,7 @@ public final class ShortcutInfo implements Parcelable { mIconResourceId = source.mIconResourceId; if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) { - mActivityComponent = source.mActivityComponent; + mActivity = source.mActivity; if ((cloneFlags & CLONE_REMOVE_ICON) == 0) { mIcon = source.mIcon; @@ -308,6 +312,17 @@ public final class ShortcutInfo implements Parcelable { return new ShortcutInfo(this, cloneFlags); } + /** + * @hide + */ + public void ensureUpdatableWith(ShortcutInfo source) { + Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match"); + Preconditions.checkState(mId.equals(source.mId), "ID must match"); + Preconditions.checkState(mPackageName.equals(source.mPackageName), + "Package name must match"); + Preconditions.checkState(!isImmutable(), "Target ShortcutInfo is immutable"); + } + /** * Copy non-null/zero fields from another {@link ShortcutInfo}. Only "public" information * will be overwritten. The timestamp will be updated. @@ -316,16 +331,15 @@ public final class ShortcutInfo implements Parcelable { * - mBitmapPath will not change * - Current time will be set to timestamp * + * @throws IllegalStateException if source is not compatible. + * * @hide */ public void copyNonNullFieldsFrom(ShortcutInfo source) { - Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match"); - Preconditions.checkState(mId.equals(source.mId), "ID must match"); - Preconditions.checkState(mPackageName.equals(source.mPackageName), - "Package name must match"); + ensureUpdatableWith(source); - if (source.mActivityComponent != null) { - mActivityComponent = source.mActivityComponent; + if (source.mActivity != null) { + mActivity = source.mActivity; } if (source.mIcon != null) { @@ -401,21 +415,21 @@ public final class ShortcutInfo implements Parcelable { private String mId; - private ComponentName mActivityComponent; + private ComponentName mActivity; private Icon mIcon; private int mTitleResId; - private String mTitle; + private CharSequence mTitle; private int mTextResId; - private String mText; + private CharSequence mText; private int mDisabledMessageResId; - private String mDisabledMessage; + private CharSequence mDisabledMessage; private Set mCategories; @@ -451,8 +465,8 @@ public final class ShortcutInfo implements Parcelable { * a hint to the launcher app about which launcher icon to associate this shortcut with. */ @NonNull - public Builder setActivityComponent(@NonNull ComponentName activityComponent) { - mActivityComponent = Preconditions.checkNotNull(activityComponent, "activityComponent"); + public Builder setActivity(@NonNull ComponentName activity) { + mActivity = Preconditions.checkNotNull(activity, "activity"); return this; } @@ -476,7 +490,7 @@ public final class ShortcutInfo implements Parcelable { return this; } - /** TODO Javadoc */ + /** @hide */ public Builder setTitleResId(int titleResId) { Preconditions.checkState(mTitle == null, "title already set"); mTitleResId = titleResId; @@ -496,7 +510,7 @@ public final class ShortcutInfo implements Parcelable { return this; } - /** TODO Javadoc */ + /** @hide */ public Builder setTextResId(int textResId) { Preconditions.checkState(mText == null, "text already set"); mTextResId = textResId; @@ -516,7 +530,7 @@ public final class ShortcutInfo implements Parcelable { return this; } - /** TODO Javadoc */ + /** @hide */ public Builder setDisabledMessageResId(int disabledMessageResId) { Preconditions.checkState(mDisabledMessage == null, "disabledMessage already set"); mDisabledMessageResId = disabledMessageResId; @@ -595,7 +609,7 @@ public final class ShortcutInfo implements Parcelable { * Return the package name of the creator application. */ @NonNull - public String getPackageName() { + public String getPackage() { return mPackageName; } @@ -606,11 +620,11 @@ public final class ShortcutInfo implements Parcelable { *

This has nothing to do with the activity that this shortcut will launch. This is * a hint to the launcher app that on which launcher icon this shortcut should be shown. * - * @see Builder#setActivityComponent + * @see Builder#setActivity */ @Nullable - public ComponentName getActivityComponent() { - return mActivityComponent; + public ComponentName getActivity() { + return mActivity; } /** @@ -634,7 +648,7 @@ public final class ShortcutInfo implements Parcelable { * {@link #hasKeyFieldsOnly()} is true. */ @Nullable - public String getTitle() { + public CharSequence getTitle() { return mTitle; } @@ -647,7 +661,7 @@ public final class ShortcutInfo implements Parcelable { * Return the shortcut text. */ @Nullable - public String getText() { + public CharSequence getText() { return mText; } @@ -660,7 +674,7 @@ public final class ShortcutInfo implements Parcelable { * Return the message that should be shown when a shortcut in disabled state is launched. */ @Nullable - public String getDisabledMessage() { + public CharSequence getDisabledMessage() { return mDisabledMessage; } @@ -788,18 +802,67 @@ public final class ShortcutInfo implements Parcelable { } /** - * Return whether a shortcut is published via manifest or not. If true, the shortcut is - * immutable. + * Return whether a shortcut is published via AndroidManifest.xml or not. If {@code true}, + * it's also {@link #isImmutable()}. + * + *

When an app is upgraded and a shortcut is no longer published from AndroidManifest.xml, + * this will be set to {@code false}. If the shortcut is not pinned, then it'll just disappear. + * However, if it's pinned, it will still be alive, and {@link #isEnabled()} will be + * {@code false} and {@link #isImmutable()} will be {@code true}. + * + *

NOTE this is whether a shortcut is published from the current version's + * AndroidManifest.xml. */ - public boolean isFromManifest() { - return hasFlags(FLAG_FROM_MANIFEST); + public boolean isManifestShortcut() { + return hasFlags(FLAG_MANIFEST); } - /** Return whether a shortcut is disabled by publisher or not. */ - public boolean isDisabled() { + /** + * @return true if pinned but neither dynamic nor manifest. + * @hide + */ + public boolean isFloating() { + return isPinned() && !(isDynamic() || isManifestShortcut()); + } + + /** @hide */ + public boolean isOriginallyFromManifest() { + return hasFlags(FLAG_IMMUTABLE); + } + + /** + * Return if a shortcut is immutable, in which case it cannot be modified with any of + * {@link ShortcutManager} APIs. + * + *

All manifest shortcuts are immutable. When a manifest shortcut is pinned and then + * disabled because the app is upgraded and its AndroidManifest.xml no longer publishes it, + * {@link #isManifestShortcut} returns {@code false}, but it is still immutable. + * + *

All shortcuts originally published via the {@link ShortcutManager} APIs + * are all mutable. + */ + public boolean isImmutable() { + return hasFlags(FLAG_IMMUTABLE); + } + + /** + * Returns {@code false} if a shortcut is disabled with + * {@link ShortcutManager#disableShortcuts}. + */ + public boolean isEnabled() { return !hasFlags(FLAG_DISABLED); } + /** @hide */ + public boolean isAlive() { + return hasFlags(FLAG_PINNED) || hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST); + } + + /** @hide */ + public boolean usesQuota() { + return hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST); + } + /** * Return whether a shortcut's icon is a resource in the owning package. * @@ -833,7 +896,7 @@ public final class ShortcutInfo implements Parcelable { * following fields are available. *

    *
  • {@link #getId()} - *
  • {@link #getPackageName()} + *
  • {@link #getPackage()} *
  • {@link #getLastChangedTimestamp()} *
  • {@link #isDynamic()} *
  • {@link #isPinned()} @@ -888,19 +951,31 @@ public final class ShortcutInfo implements Parcelable { mBitmapPath = bitmapPath; } + /** @hide */ + public void setDisabledMessageResId(int disabledMessageResId) { + mDisabledMessageResId = disabledMessageResId; + mDisabledMessage = null; + } + + /** @hide */ + public void setDisabledMessage(String disabledMessage) { + mDisabledMessage = disabledMessage; + mDisabledMessageResId = 0; + } + private ShortcutInfo(Parcel source) { final ClassLoader cl = getClass().getClassLoader(); mUserId = source.readInt(); mId = source.readString(); mPackageName = source.readString(); - mActivityComponent = source.readParcelable(cl); + mActivity = source.readParcelable(cl); mIcon = source.readParcelable(cl); - mTitle = source.readString(); + mTitle = source.readCharSequence(); mTitleResId = source.readInt(); - mText = source.readString(); + mText = source.readCharSequence(); mTextResId = source.readInt(); - mDisabledMessage = source.readString(); + mDisabledMessage = source.readCharSequence(); mDisabledMessageResId = source.readInt(); mIntent = source.readParcelable(cl); mIntentPersistableExtras = source.readParcelable(cl); @@ -927,13 +1002,13 @@ public final class ShortcutInfo implements Parcelable { dest.writeInt(mUserId); dest.writeString(mId); dest.writeString(mPackageName); - dest.writeParcelable(mActivityComponent, flags); + dest.writeParcelable(mActivity, flags); dest.writeParcelable(mIcon, flags); - dest.writeString(mTitle); + dest.writeCharSequence(mTitle); dest.writeInt(mTitleResId); - dest.writeString(mText); + dest.writeCharSequence(mText); dest.writeInt(mTextResId); - dest.writeString(mDisabledMessage); + dest.writeCharSequence(mDisabledMessage); dest.writeInt(mDisabledMessageResId); dest.writeParcelable(mIntent, flags); @@ -991,18 +1066,43 @@ public final class ShortcutInfo implements Parcelable { sb.append("id="); sb.append(secure ? "***" : mId); + sb.append(", flags=0x"); + sb.append(Integer.toHexString(mFlags)); + sb.append(" ["); + if (!isEnabled()) { + sb.append("X"); + } + if (isImmutable()) { + sb.append("Im"); + } + if (isManifestShortcut()) { + sb.append("M"); + } + if (isDynamic()) { + sb.append("D"); + } + if (isPinned()) { + sb.append("P"); + } + if (hasIconFile()) { + sb.append("If"); + } + if (hasIconResource()) { + sb.append("Ir"); + } + if (hasKeyFieldsOnly()) { + sb.append("K"); + } + if (hasStringResourcesResolved()) { + sb.append("Sr"); + } + sb.append("]"); + sb.append(", packageName="); sb.append(mPackageName); - if (isDynamic()) { - sb.append(", dynamic"); - } - if (isPinned()) { - sb.append(", pinned"); - } - sb.append(", activity="); - sb.append(mActivityComponent); + sb.append(mActivity); sb.append(", title="); sb.append(secure ? "***" : mTitle); @@ -1040,35 +1140,6 @@ public final class ShortcutInfo implements Parcelable { sb.append(", extras="); sb.append(mExtras); - sb.append(", flags="); - sb.append(mFlags); - sb.append(" ["); - if (hasFlags(FLAG_DISABLED)) { - sb.append("X"); - } - if (hasFlags(FLAG_FROM_MANIFEST)) { - sb.append("M"); - } - if (hasFlags(FLAG_DYNAMIC)) { - sb.append("D"); - } - if (hasFlags(FLAG_PINNED)) { - sb.append("P"); - } - if (hasFlags(FLAG_HAS_ICON_FILE)) { - sb.append("If"); - } - if (hasFlags(FLAG_HAS_ICON_RES)) { - sb.append("Ir"); - } - if (hasFlags(FLAG_KEY_FIELDS_ONLY)) { - sb.append("K"); - } - if (hasFlags(FLAG_STRINGS_RESOLVED)) { - sb.append("S"); - } - sb.append("]"); - if (includeInternalData) { sb.append(", iconRes="); @@ -1084,16 +1155,16 @@ public final class ShortcutInfo implements Parcelable { /** @hide */ public ShortcutInfo( - @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, - Icon icon, String title, int titleResId, String text, int textResId, - String disabledMessage, int disabledMessageResId, Set categories, Intent intent, - PersistableBundle intentPersistableExtras, + @UserIdInt int userId, String id, String packageName, ComponentName activity, + Icon icon, CharSequence title, int titleResId, CharSequence text, int textResId, + CharSequence disabledMessage, int disabledMessageResId, Set categories, + Intent intent, PersistableBundle intentPersistableExtras, int rank, PersistableBundle extras, long lastChangedTimestamp, int flags, int iconResId, String bitmapPath) { mUserId = userId; mId = id; mPackageName = packageName; - mActivityComponent = activityComponent; + mActivity = activity; mIcon = icon; mTitle = title; mTitleResId = titleResId; diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 44fa98d75a06c..3d214a84b1907 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -28,7 +28,7 @@ import java.util.List; // TODO Enhance javadoc /** - * TODO: Update to reflect DR changes.
    + * TODO: Update to reflect DR changes, such as manifest shortcuts.
    * * {@link ShortcutManager} manages shortcuts created by applications. * @@ -36,8 +36,8 @@ import java.util.List; * * An application can publish shortcuts with {@link #setDynamicShortcuts(List)} and * {@link #addDynamicShortcuts(List)}. There can be at most - * {@link #getMaxDynamicShortcutCount()} number of dynamic shortcuts at a time from the same - * application. + * {@link #getMaxShortcutCountForActivity()} number of dynamic shortcuts at a time from the + * same application. * A dynamic shortcut can be deleted with {@link #removeDynamicShortcuts(List)}, and apps * can also use {@link #removeAllDynamicShortcuts()} to delete all dynamic shortcuts. * @@ -51,7 +51,8 @@ import java.util.List; *

    The number of pinned shortcuts does not affect the number of dynamic shortcuts that can be * published by an application at a time. * No matter how many pinned shortcuts that Launcher has for an application, the - * application can still always publish {@link #getMaxDynamicShortcutCount()} number of dynamic + * application can still always publish {@link #getMaxShortcutCountForActivity()} number of + * dynamic * shortcuts. * *

    Shortcut IDs

    @@ -132,7 +133,7 @@ public class ShortcutManager { * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited. * * @throws IllegalArgumentException if {@code shortcutInfoList} contains more than - * {@link #getMaxDynamicShortcutCount()} shortcuts. + * {@link #getMaxShortcutCountForActivity()} shortcuts. */ public boolean setDynamicShortcuts(@NonNull List shortcutInfoList) { try { @@ -145,7 +146,7 @@ public class ShortcutManager { /** * Return all dynamic shortcuts from the caller application. The number of result items - * will not exceed the value returned by {@link #getMaxDynamicShortcutCount()}. + * will not exceed the value returned by {@link #getMaxShortcutCountForActivity()}. */ @NonNull public List getDynamicShortcuts() { @@ -157,6 +158,19 @@ public class ShortcutManager { } } + /** + * TODO Javadoc + */ + @NonNull + public List getManifestShortcuts() { + try { + return mService.getManifestShortcuts(mContext.getPackageName(), injectMyUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Publish list of dynamic shortcuts. If there's already dynamic or pinned shortcuts with * the same IDs, they will all be updated. @@ -269,9 +283,21 @@ public class ShortcutManager { } /** - * Return the max number of dynamic shortcuts that each application can have at a time. + * TODO Javadoc */ - public int getMaxDynamicShortcutCount() { + public void enableShortcuts(@NonNull List shortcutIds) { + try { + mService.enableShortcuts(mContext.getPackageName(), shortcutIds, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the max number of dynamic shortcuts + manifest shortcuts that each launcehr icon + * can have at a time. + */ + public int getMaxShortcutCountForActivity() { try { return mService.getMaxDynamicShortcutCount(mContext.getPackageName(), injectMyUserId()); } catch (RemoteException e) { @@ -308,10 +334,23 @@ public class ShortcutManager { } /** - * Return the max width and height for icons, in pixels. + * Return the max width for icons, in pixels. */ - public int getIconMaxDimensions() { + public int getIconMaxWidth() { try { + // TODO Implement it properly using xdpi. + return mService.getIconMaxDimensions(mContext.getPackageName(), injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the max height for icons, in pixels. + */ + public int getIconMaxHeight() { + try { + // TODO Implement it properly using ydpi. return mService.getIconMaxDimensions(mContext.getPackageName(), injectMyUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index e0fbca11fca61..3259907edc85e 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8237,4 +8237,17 @@ i color. --> + + + + + + + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 6797799b99a31..2738dc361a791 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2728,4 +2728,18 @@ + + + + + + + + + + + + diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java index db2b9f40239cd..e667838ad5e55 100644 --- a/services/core/java/com/android/server/pm/ShortcutLauncher.java +++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java @@ -36,6 +36,8 @@ import java.util.List; /** * Launcher information used by {@link ShortcutService}. + * + * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. */ class ShortcutLauncher extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; @@ -125,7 +127,8 @@ class ShortcutLauncher extends ShortcutPackageItem { if (si == null) { continue; } - if (si.isDynamic() || (prevSet != null && prevSet.contains(id))) { + if (si.isDynamic() || si.isManifestShortcut() + || (prevSet != null && prevSet.contains(id))) { newSet.add(id); } } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index c298683dc0607..bca37778ee91d 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -20,6 +20,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; import android.os.PersistableBundle; import android.text.format.Formatter; @@ -28,6 +29,7 @@ import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -44,6 +46,11 @@ import java.util.function.Predicate; /** * Package information used by {@link ShortcutService}. + * User information used by {@link ShortcutService}. + * + * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. + * + * TODO Max dynamic shortcuts cap should be per activity. */ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; @@ -55,7 +62,6 @@ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG_CATEGORIES = "categories"; private static final String ATTR_NAME = "name"; - private static final String ATTR_DYNAMIC_COUNT = "dynamic-count"; private static final String ATTR_CALL_COUNT = "call-count"; private static final String ATTR_LAST_RESET = "last-reset"; private static final String ATTR_ID = "id"; @@ -83,11 +89,6 @@ class ShortcutPackage extends ShortcutPackageItem { */ final private ArrayMap mShortcuts = new ArrayMap<>(); - /** - * # of dynamic shortcuts. - */ - private int mDynamicShortcutCount = 0; - /** * # of times the package has called rate-limited APIs. */ @@ -129,19 +130,19 @@ class ShortcutPackage extends ShortcutPackageItem { * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so * we do some initialization for the package. */ - private void onShortcutPublish() { + private void ensurePackageVersionInfo() { // Make sure we have the version code for the app. We need the version code in // handlePackageUpdated(). if (getPackageInfo().getVersionCode() < 0) { final ShortcutService s = mShortcutUser.mService; - final int versionCode = s.getApplicationVersionCode(getPackageName(), getOwnerUserId()); - if (ShortcutService.DEBUG) { - Slog.d(TAG, String.format("Package %s version = %d", getPackageName(), - versionCode)); - } - if (versionCode >= 0) { - getPackageInfo().setVersionCode(versionCode); + final PackageInfo pi = s.getPackageInfo(getPackageName(), getOwnerUserId()); + if (pi != null) { + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format("Package %s version = %d", getPackageName(), + pi.versionCode)); + } + getPackageInfo().updateVersionInfo(pi); s.scheduleSaveUser(getOwnerUserId()); } } @@ -168,17 +169,42 @@ class ShortcutPackage extends ShortcutPackageItem { return mShortcuts.get(id); } - private ShortcutInfo deleteShortcut(@NonNull String id) { + private void ensureNotImmutable(@Nullable ShortcutInfo shortcut) { + if (shortcut != null && shortcut.isImmutable()) { + throw new IllegalArgumentException( + "Manifest shortcut ID=" + shortcut.getId() + + " may not be manipulated via APIs"); + } + } + + private void ensureNotImmutable(@NonNull String id) { + ensureNotImmutable(mShortcuts.get(id)); + } + + public void ensureImmutableShortcutsNotIncludedWithIds(@NonNull List shortcutIds) { + for (int i = shortcutIds.size() - 1; i >= 0; i--) { + ensureNotImmutable(shortcutIds.get(i)); + } + } + + public void ensureImmutableShortcutsNotIncluded(@NonNull List shortcuts) { + for (int i = shortcuts.size() - 1; i >= 0; i--) { + ensureNotImmutable(shortcuts.get(i).getId()); + } + } + + private ShortcutInfo deleteShortcutInner(@NonNull String id) { final ShortcutInfo shortcut = mShortcuts.remove(id); if (shortcut != null) { mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut); - shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED); + shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED + | ShortcutInfo.FLAG_MANIFEST); } return shortcut; } - void addShortcut(@NonNull ShortcutInfo newShortcut) { - deleteShortcut(newShortcut.getId()); + private void addShortcutInner(@NonNull ShortcutInfo newShortcut) { + deleteShortcutInner(newShortcut.getId()); mShortcutUser.mService.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut); mShortcuts.put(newShortcut.getId(), newShortcut); } @@ -188,39 +214,41 @@ class ShortcutPackage extends ShortcutPackageItem { * * It checks the max number of dynamic shortcuts. */ - public void addDynamicShortcut(@NonNull ShortcutInfo newShortcut) { + public void addOrUpdateDynamicShortcut(@NonNull ShortcutInfo newShortcut) { - onShortcutPublish(); + Preconditions.checkArgument(newShortcut.isEnabled(), + "add/setDynamicShortcuts() cannot publish disabled shortcuts"); + + ensurePackageVersionInfo(); newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); final boolean wasPinned; - final int newDynamicCount; if (oldShortcut == null) { wasPinned = false; - newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut. } else { + // It's an update case. + // Make sure the target is updatable. (i.e. should be mutable.) + oldShortcut.ensureUpdatableWith(newShortcut); + wasPinned = oldShortcut.isPinned(); - if (oldShortcut.isDynamic()) { - newDynamicCount = mDynamicShortcutCount; // not adding a dynamic shortcut. - } else { - newDynamicCount = mDynamicShortcutCount + 1; // adding a dynamic shortcut. + if (!oldShortcut.isEnabled()) { + newShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); } } - // Make sure there's still room. - mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount); + // TODO Check max dynamic count. + // mShortcutUser.mService.enforceMaxDynamicShortcuts(newDynamicCount); // Okay, make it dynamic and add. if (wasPinned) { newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); } - addShortcut(newShortcut); - mDynamicShortcutCount = newDynamicCount; + addShortcutInner(newShortcut); } /** @@ -232,7 +260,7 @@ class ShortcutPackage extends ShortcutPackageItem { for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); - if (si.isPinned() || si.isDynamic()) continue; + if (si.isAlive()) continue; if (removeList == null) { removeList = new ArrayList<>(); @@ -241,7 +269,7 @@ class ShortcutPackage extends ShortcutPackageItem { } if (removeList != null) { for (int i = removeList.size() - 1; i >= 0; i--) { - deleteShortcut(removeList.get(i)); + deleteShortcutInner(removeList.get(i)); } } } @@ -250,29 +278,68 @@ class ShortcutPackage extends ShortcutPackageItem { * Remove all dynamic shortcuts. */ public void deleteAllDynamicShortcuts() { + boolean changed = false; for (int i = mShortcuts.size() - 1; i >= 0; i--) { - mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC); + final ShortcutInfo si = mShortcuts.valueAt(i); + if (si.isDynamic()) { + changed = true; + si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + } + } + if (changed) { + removeOrphans(); } - removeOrphans(); - mDynamicShortcutCount = 0; } /** * Remove a dynamic shortcut by ID. */ public void deleteDynamicWithId(@NonNull String shortcutId) { + deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false); + } + + public void disableWithId(@NonNull String shortcutId, String disabledMessage, + int disabledMessageResId, boolean overrideImmutable) { + final ShortcutInfo disabled = deleteOrDisableWithId(shortcutId, /* disable =*/ true, + overrideImmutable); + + if (disabled != null) { + if (disabledMessage != null) { + disabled.setDisabledMessage(disabledMessage); + } else if (disabledMessageResId != 0) { + disabled.setDisabledMessageResId(disabledMessageResId); + } + } + } + + @Nullable + private ShortcutInfo deleteOrDisableWithId(@NonNull String shortcutId, boolean disable, + boolean overrideImmutable) { final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId); - if (oldShortcut == null) { - return; + if (oldShortcut == null || !oldShortcut.isEnabled()) { + return null; // Doesn't exist or already disabled. } - if (oldShortcut.isDynamic()) { - mDynamicShortcutCount--; + if (!overrideImmutable) { + ensureNotImmutable(oldShortcut); } if (oldShortcut.isPinned()) { - oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); + if (disable) { + oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); + } + return oldShortcut; } else { - deleteShortcut(shortcutId); + deleteShortcutInner(shortcutId); + return null; + } + } + + public void enableWithId(@NonNull String shortcutId) { + final ShortcutInfo shortcut = mShortcuts.get(shortcutId); + if (shortcut != null) { + ensureNotImmutable(shortcut); + shortcut.clearFlags(ShortcutInfo.FLAG_DISABLED); } } @@ -430,22 +497,19 @@ class ShortcutPackage extends ShortcutPackageItem { for (int i = 0; i < mShortcuts.size(); i++) { final ShortcutInfo si = mShortcuts.valueAt(i); - // If it's called by non-launcher (i.e. publisher, always include -> true. - // Otherwise, only include non-dynamic pinned one, if the calling launcher has pinned - // it. + // Need to adjust PINNED flag depending on the caller. + // Basically if the caller is a launcher (callingLauncher != null) and the launcher + // isn't pinning it, then we need to clear PINNED for this caller. final boolean isPinnedByCaller = (callingLauncher == null) || ((pinnedByCallerSet != null) && pinnedByCallerSet.contains(si.getId())); - if (!si.isDynamic()) { - if (!si.isPinned()) { - s.wtf("Shortcut not pinned: package " + getPackageName() - + ", user=" + getPackageUserId() + ", id=" + si.getId()); - continue; - } + + if (si.isFloating()) { if (!isPinnedByCaller) { continue; } } final ShortcutInfo clone = si.clone(cloneFlag); + // Fix up isPinned for the caller. Note we need to do it before the "test" callback, // since it may check isPinned. if (!isPinnedByCaller) { @@ -486,32 +550,81 @@ class ShortcutPackage extends ShortcutPackageItem { } /** - * Called when the package is updated. If there are shortcuts with resource icons, update - * their timestamps. + * Called when the package is updated or added. + * + * Add case: + * - Publish manifest shortcuts. + * + * Update case: + * - Re-publish manifest shortcuts. + * - If there are shortcuts with resources (icons or strings), update their timestamps. + * + * @return TRUE if any shortcuts have been changed. */ - public void handlePackageUpdated(int newVersionCode) { - if (getPackageInfo().getVersionCode() >= newVersionCode) { - // Version hasn't changed; nothing to do. - return; - } - if (ShortcutService.DEBUG) { - Slog.d(TAG, String.format("Package %s updated, version %d -> %d", getPackageName(), - getPackageInfo().getVersionCode(), newVersionCode)); + public boolean handlePackageAddedOrUpdated(boolean isNewApp) { + final PackageInfo pi = mShortcutUser.mService.getPackageInfo( + getPackageName(), getPackageUserId()); + if (pi == null) { + return false; // Shouldn't happen. } - getPackageInfo().setVersionCode(newVersionCode); + if (!isNewApp) { + // Make sure the version code or last update time has changed. + // Otherwise, nothing to do. + if (getPackageInfo().getVersionCode() >= pi.versionCode + && getPackageInfo().getLastUpdateTime() >= pi.lastUpdateTime) { + return false; + } + } + + // Now prepare to publish manifest shortcuts. + List newManifestShortcutList = null; + try { + newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService, + getPackageName(), getPackageUserId()); + } catch (IOException|XmlPullParserException e) { + Slog.e(TAG, "Failed to load shortcuts from AndroidManifest.xml.", e); + } + final int manifestShortcutSize = newManifestShortcutList == null ? 0 + : newManifestShortcutList.size(); + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format("Package %s has %d manifest shortcut(s)", + getPackageName(), manifestShortcutSize)); + } + if (isNewApp && (manifestShortcutSize == 0)) { + // If it's a new app, and it doesn't have manifest shortcuts, then nothing to do. + + // If it's an update, then it may already have manifest shortcuts, which need to be + // disabled. + return false; + } + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(), + (isNewApp ? "added" : "updated"), + getPackageInfo().getVersionCode(), pi.versionCode)); + } + + getPackageInfo().updateVersionInfo(pi); final ShortcutService s = mShortcutUser.mService; boolean changed = false; - for (int i = mShortcuts.size() - 1; i >= 0; i--) { - final ShortcutInfo si = mShortcuts.valueAt(i); - if (si.hasAnyResources()) { - changed = true; - si.setTimestamp(s.injectCurrentTimeMillis()); + // For existing shortcuts, update timestamps if they have any resources. + if (!isNewApp) { + for (int i = mShortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + + if (si.hasAnyResources()) { + changed = true; + si.setTimestamp(s.injectCurrentTimeMillis()); + } } } + + // (Re-)publish manifest shortcut. + changed |= publishManifestShortcuts(newManifestShortcutList); + if (changed) { // This will send a notification to the launcher, and also save . s.packageShortcutsChanged(getPackageName(), getPackageUserId()); @@ -519,6 +632,97 @@ class ShortcutPackage extends ShortcutPackageItem { // Still save the version code. s.scheduleSaveUser(getPackageUserId()); } + return changed; + } + + private boolean publishManifestShortcuts(List newManifestShortcutList) { + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format( + "Package %s: publishing manifest shortcuts", getPackageName())); + } + 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--) { + final ShortcutInfo si = mShortcuts.valueAt(i); + + if (si.isManifestShortcut()) { + if (toDisableList == null) { + toDisableList = new ArraySet<>(); + } + toDisableList.add(si.getId()); + } + } + + // Publish new ones. + if (newManifestShortcutList != null) { + final int newListSize = newManifestShortcutList.size(); + + for (int i = 0; i < newListSize; i++) { + changed = true; + + final ShortcutInfo newShortcut = newManifestShortcutList.get(i); + final boolean newDisabled = !newShortcut.isEnabled(); + + final String id = newShortcut.getId(); + final ShortcutInfo oldShortcut = mShortcuts.get(id); + + boolean wasPinned = false; + + if (oldShortcut != null) { + if (!oldShortcut.isOriginallyFromManifest()) { + Slog.e(TAG, "Shortcut with ID=" + newShortcut.getId() + + " exists but is not from AndroidManifest.xml, not updating."); + continue; + } + // Take over the pinned flag. + if (oldShortcut.isPinned()) { + wasPinned = true; + newShortcut.addFlags(ShortcutInfo.FLAG_PINNED); + } + } + if (newDisabled && !wasPinned) { + // If the shortcut is disabled, and it was *not* pinned, then this + // just doesn't have to be published. + // 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. + addShortcutInner(newShortcut); // This will clean up the old one too. + + if (!newDisabled && toDisableList != null) { + // Still alive, don't remove. + toDisableList.remove(id); + } + } + } + + // Disable the previous manifest shortcuts that are no longer in the manifest. + if (toDisableList != null) { + if (ShortcutService.DEBUG) { + Slog.d(TAG, String.format( + "Package %s: disabling %d stale shortcuts", getPackageName(), + toDisableList.size())); + } + for (int i = toDisableList.size() - 1; i >= 0; i--) { + changed = true; + + final String id = toDisableList.valueAt(i); + + disableWithId(id, /* disable message =*/ null, /* disable message resid */ 0, + /* overrideImmutable=*/ true); + } + removeOrphans(); + } + return changed; } public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { @@ -597,7 +801,6 @@ class ShortcutPackage extends ShortcutPackageItem { out.startTag(null, TAG_ROOT); ShortcutService.writeAttr(out, ATTR_NAME, getPackageName()); - ShortcutService.writeAttr(out, ATTR_DYNAMIC_COUNT, mDynamicShortcutCount); ShortcutService.writeAttr(out, ATTR_CALL_COUNT, mApiCallCount); ShortcutService.writeAttr(out, ATTR_LAST_RESET, mLastResetTime); getPackageInfo().saveToXml(out); @@ -619,7 +822,7 @@ class ShortcutPackage extends ShortcutPackageItem { out.startTag(null, TAG_SHORTCUT); ShortcutService.writeAttr(out, ATTR_ID, si.getId()); // writeAttr(out, "package", si.getPackageName()); // not needed - ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent()); + ShortcutService.writeAttr(out, ATTR_ACTIVITY, si.getActivity()); // writeAttr(out, "icon", si.getIcon()); // We don't save it. ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle()); ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId()); @@ -670,8 +873,6 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutPackage ret = new ShortcutPackage(shortcutUser, shortcutUser.getUserId(), packageName); - ret.mDynamicShortcutCount = - ShortcutService.parseIntAttribute(parser, ATTR_DYNAMIC_COUNT); ret.mApiCallCount = ShortcutService.parseIntAttribute(parser, ATTR_CALL_COUNT); ret.mLastResetTime = diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java index ae9709e4b6c83..7f5d931e0b7f6 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java @@ -15,6 +15,7 @@ */ package com.android.server.pm; +import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.pm.PackageInfo; import android.util.Slog; @@ -34,12 +35,15 @@ import java.util.ArrayList; /** * Package information used by {@link android.content.pm.ShortcutManager} for backup / restore. + * + * All methods should be guarded by {@code ShortcutService.mLock}. */ class ShortcutPackageInfo { private static final String TAG = ShortcutService.TAG; static final String TAG_ROOT = "package-info"; private static final String ATTR_VERSION = "version"; + private static final String ATTR_LAST_UPDATE_TIME = "last_udpate_time"; private static final String ATTR_SHADOW = "shadow"; private static final String TAG_SIGNATURE = "signature"; @@ -53,16 +57,20 @@ class ShortcutPackageInfo { */ private boolean mIsShadow; private int mVersionCode = VERSION_UNKNOWN; + private long mLastUpdateTime; private ArrayList mSigHashes; - private ShortcutPackageInfo(int versionCode, ArrayList sigHashes, boolean isShadow) { + private ShortcutPackageInfo(int versionCode, long lastUpdateTime, + ArrayList sigHashes, boolean isShadow) { mVersionCode = versionCode; + mLastUpdateTime = lastUpdateTime; mIsShadow = isShadow; mSigHashes = sigHashes; } public static ShortcutPackageInfo newEmpty() { - return new ShortcutPackageInfo(VERSION_UNKNOWN, new ArrayList<>(0), /* isShadow */ false); + return new ShortcutPackageInfo(VERSION_UNKNOWN, /* last update time =*/ 0, + new ArrayList<>(0), /* isShadow */ false); } public boolean isShadow() { @@ -77,8 +85,15 @@ class ShortcutPackageInfo { return mVersionCode; } - public void setVersionCode(int versionCode) { - mVersionCode = versionCode; + public long getLastUpdateTime() { + return mLastUpdateTime; + } + + public void updateVersionInfo(@NonNull PackageInfo pi) { + if (pi != null) { + mVersionCode = pi.versionCode; + mLastUpdateTime = pi.lastUpdateTime; + } } public boolean hasSignatures() { @@ -111,7 +126,7 @@ class ShortcutPackageInfo { Slog.e(TAG, "Can't get signatures: package=" + packageName); return null; } - final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.versionCode, + final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.versionCode, pi.lastUpdateTime, BackupUtils.hashSignatureArray(pi.signatures), /* shadow=*/ false); return ret; @@ -131,6 +146,7 @@ class ShortcutPackageInfo { return; } mVersionCode = pi.versionCode; + mLastUpdateTime = pi.lastUpdateTime; mSigHashes = BackupUtils.hashSignatureArray(pi.signatures); } @@ -139,6 +155,7 @@ class ShortcutPackageInfo { out.startTag(null, TAG_ROOT); ShortcutService.writeAttr(out, ATTR_VERSION, mVersionCode); + ShortcutService.writeAttr(out, ATTR_LAST_UPDATE_TIME, mLastUpdateTime); ShortcutService.writeAttr(out, ATTR_SHADOW, mIsShadow); for (int i = 0; i < mSigHashes.size(); i++) { @@ -154,6 +171,9 @@ class ShortcutPackageInfo { final int versionCode = ShortcutService.parseIntAttribute(parser, ATTR_VERSION); + final long lastUpdateTime = ShortcutService.parseIntAttribute( + parser, ATTR_LAST_UPDATE_TIME); + // When restoring from backup, it's always shadow. final boolean shadow = fromBackup || ShortcutService.parseBooleanAttribute(parser, ATTR_SHADOW); @@ -185,6 +205,7 @@ class ShortcutPackageInfo { // Successfully loaded; replace the feilds. mVersionCode = versionCode; + mLastUpdateTime = lastUpdateTime; mIsShadow = shadow; mSigHashes = hashes; } @@ -205,6 +226,11 @@ class ShortcutPackageInfo { pw.print(mVersionCode); pw.println(); + pw.print(prefix); + pw.print(" Last package update time: "); + pw.print(mLastUpdateTime); + pw.println(); + for (int i = 0; i < mSigHashes.size(); i++) { pw.print(prefix); pw.print(" "); diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java index 0c2417cffae7c..d08b97405b94c 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java @@ -26,6 +26,9 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; +/** + * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. + */ abstract class ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java new file mode 100644 index 0000000000000..44d0a9f5c7d44 --- /dev/null +++ b/services/core/java/com/android/server/pm/ShortcutParser.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +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.Slog; +import android.util.Xml; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ShortcutParser { + private static final String TAG = ShortcutService.TAG; + + private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE + + @VisibleForTesting + static final String METADATA_KEY = "android.pm.Shortcuts"; + + private static final String TAG_SHORTCUTS = "shortcuts"; + private static final String TAG_SHORTCUT = "shortcut"; + + @Nullable + public static List parseShortcuts(ShortcutService service, + String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException { + final PackageInfo pi = service.injectGetActivitiesWithMetadata(packageName, userId); + + List result = null; + + if (pi != null && pi.activities != null) { + for (ActivityInfo activityInfo : pi.activities) { + result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result); + } + } + return result; + } + + private static List parseShortcutsOneFile( + ShortcutService service, + ActivityInfo activityInfo, String packageName, @UserIdInt int userId, + List result) throws IOException, XmlPullParserException { + XmlResourceParser parser = null; + try { + parser = service.injectXmlMetaData(activityInfo, METADATA_KEY); + if (parser == null) { + return result; + } + + final ComponentName activity = new ComponentName(packageName, activityInfo.name); + + final AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + + outer: + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { + 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. + } + if (depth == 2 && TAG_SHORTCUT.equals(tag)) { + final ShortcutInfo si = parseShortcutAttributes( + service, attrs, packageName, activity, userId); + if (ShortcutService.DEBUG) { + Slog.d(TAG, "Shortcut=" + si); + } + 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."); + continue outer; + } + } + } + + if (si != null) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(si); + } + continue; + } + Slog.w(TAG, "Unknown tag " + tag); + } + } finally { + if (parser != null) { + parser.close(); + } + } + return result; + } + + private static ShortcutInfo parseShortcutAttributes(ShortcutService service, + AttributeSet attrs, String packageName, ComponentName activity, + @UserIdInt int userId) { + final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, + R.styleable.Shortcut); + try { + final String id = sa.getString(R.styleable.Shortcut_shortcutId); + final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); + final int rank = sa.getInt(R.styleable.Shortcut_shortcutRank, 0); + final int iconResId = sa.getResourceId(R.styleable.Shortcut_shortcutIcon, 0); + final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutTitle, 0); + final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutText, 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); + return null; + } + if (titleResId == 0) { + 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, + userId, + id, + packageName, + activity, + titleResId, + textResId, + disabledMessageResId, + categoriesSet, + intent, + rank, + iconResId, + enabled); + } finally { + sa.recycle(); + } + } + + 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) { + + final int flags = + (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) + | ShortcutInfo.FLAG_IMMUTABLE + | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); + + return new ShortcutInfo( + userId, + id, + packageName, + activityComponent, + null, // icon + null, // title string + titleResId, + null, // text string + textResId, + null, // disabled message string + disabledMessageResId, + categories, + intent, + null, // intent extras + rank, + null, // extras + service.injectCurrentTimeMillis(), + flags, + iconResId, + null); // bitmap path + } +} diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 6a10f4198c4da..dc0404c9546cb 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -25,6 +25,7 @@ import android.app.IUidObserver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.IShortcutService; @@ -39,6 +40,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutServiceInternal; import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; +import android.content.res.XmlResourceParser; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Canvas; @@ -103,6 +105,7 @@ import java.io.PrintWriter; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -111,16 +114,21 @@ import java.util.function.Predicate; /** * TODO: - * - Manifest shortcuts. + * - Implement # of dynamic shortcuts cap. * - * - Implement disableShortcuts(). + * - HandleUnlockUser needs to be async. Wait on it in onCleanupUser. * * - Implement reportShortcutUsed(). * + * - validateForXml() should be removed. + * * - Ranks should be recalculated after each update. * * - When the system locale changes, update timestamps for shortcuts with string resources, - * and notify the launcher. + * and notify the launcher. Right now, it resets the throttling, but timestamps are not changed + * and there's no notification either. + * + * - getIconMaxWidth()/getIconMaxHeight() should use xdpi and ydpi. * * - Default launcher check does take a few ms. Worth caching. * @@ -128,11 +136,13 @@ import java.util.function.Predicate; * internal bitmap handling. * * - Add more call stats. + * + * - Rename getMaxDynamicShortcutCount and mMaxDynamicShortcuts */ public class ShortcutService extends IShortcutService.Stub { static final String TAG = "ShortcutService"; - static final boolean DEBUG = false; // STOPSHIP if true + static final boolean DEBUG = true; // STOPSHIP if true static final boolean DEBUG_LOAD = false; // STOPSHIP if true static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true @@ -301,8 +311,11 @@ public class ShortcutService extends IShortcutService.Stub { int GET_APPLICATION_INFO = 3; int LAUNCHER_PERMISSION_CHECK = 4; int CLEANUP_DANGLING_BITMAPS = 5; + int GET_ACTIVITIES_WITH_METADATA = 6; + int GET_INSTALLED_APPLICATIONS = 7; + int CHECK_PACKAGE_CHANGES = 8; - int COUNT = CLEANUP_DANGLING_BITMAPS + 1; + int COUNT = CHECK_PACKAGE_CHANGES + 1; } final Object mStatLock = new Object(); @@ -444,6 +457,9 @@ public class ShortcutService extends IShortcutService.Stub { /** lifecycle event */ void handleUnlockUser(int userId) { + if (DEBUG) { + Slog.d(TAG, "handleUnlockUser: user=" + userId); + } synchronized (mLock) { // Preload getUserShortcutsLocked(userId); @@ -642,10 +658,10 @@ public class ShortcutService extends IShortcutService.Stub { out.endTag(null, tag); } - static void writeAttr(XmlSerializer out, String name, String value) throws IOException { + static void writeAttr(XmlSerializer out, String name, CharSequence value) throws IOException { if (TextUtils.isEmpty(value)) return; - out.attribute(null, name, value); + out.attribute(null, name, value.toString()); } static void writeAttr(XmlSerializer out, String name, long value) throws IOException { @@ -1090,7 +1106,7 @@ public class ShortcutService extends IShortcutService.Stub { FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut) throws IOException { final File packagePath = new File(getUserBitmapFilePath(userId), - shortcut.getPackageName()); + shortcut.getPackage()); if (!packagePath.isDirectory()) { packagePath.mkdirs(); if (!packagePath.isDirectory()) { @@ -1129,7 +1145,6 @@ public class ShortcutService extends IShortcutService.Stub { } Bitmap bitmap; - Bitmap bitmapToRecycle = null; try { switch (icon.getType()) { case Icon.TYPE_RESOURCE: { @@ -1181,9 +1196,6 @@ public class ShortcutService extends IShortcutService.Stub { } } } finally { - if (bitmapToRecycle != null) { - bitmapToRecycle.recycle(); - } // Once saved, we won't use the original icon information, so null it out. shortcut.clearIcon(); } @@ -1196,7 +1208,7 @@ public class ShortcutService extends IShortcutService.Stub { // so override in unit tests. // TODO CTS this case. void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) { - if (!shortcut.getPackageName().equals(icon.getResPackage())) { + if (!shortcut.getPackage().equals(icon.getResPackage())) { throw new IllegalArgumentException( "Icon resource must reside in shortcut owner package"); } @@ -1353,10 +1365,9 @@ public class ShortcutService extends IShortcutService.Stub { */ private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) { Preconditions.checkNotNull(shortcut, "Null shortcut detected"); - if (shortcut.getActivityComponent() != null) { + if (shortcut.getActivity() != null) { Preconditions.checkState( - shortcut.getPackageName().equals( - shortcut.getActivityComponent().getPackageName()), + shortcut.getPackage().equals(shortcut.getActivity().getPackageName()), "Activity package name mismatch"); } @@ -1399,7 +1410,7 @@ public class ShortcutService extends IShortcutService.Stub { } } - private static void validateForXml(String s) { + private static void validateForXml(CharSequence s) { if (TextUtils.isEmpty(s)) { return; } @@ -1427,6 +1438,8 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1444,7 +1457,7 @@ public class ShortcutService extends IShortcutService.Stub { // Then, add/update all. We need to make sure to take over "pinned" flag. for (int i = 0; i < size; i++) { final ShortcutInfo newShortcut = newShortcuts.get(i); - ps.addDynamicShortcut(newShortcut); + ps.addOrUpdateDynamicShortcut(newShortcut); } } packageShortcutsChanged(packageName, userId); @@ -1462,6 +1475,8 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1473,11 +1488,22 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutInfo target = ps.findShortcutById(source.getId()); if (target != null) { + if (target.isEnabled() != source.isEnabled()) { + Slog.w(TAG, + "ShortcutInfo.enabled cannot be changed with updateShortcuts()"); + } + final boolean replacingIcon = (source.getIcon() != null); if (replacingIcon) { removeIcon(userId, target); } + if (source.getActivity() != null && + !source.getActivity().equals(target.getActivity())) { + // TODO When activity is changing, check the dynamic count. + } + + // Note copyNonNullFieldsFrom() does the "udpatable with?" check too. target.copyNonNullFieldsFrom(source); if (replacingIcon) { @@ -1502,6 +1528,8 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + // Throttling. if (!ps.tryApiCall()) { return false; @@ -1513,7 +1541,7 @@ public class ShortcutService extends IShortcutService.Stub { fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false); // Add it. - ps.addDynamicShortcut(newShortcut); + ps.addOrUpdateDynamicShortcut(newShortcut); } } packageShortcutsChanged(packageName, userId); @@ -1528,7 +1556,32 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided"); synchronized (mLock) { - // TODO implement it. + final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + + ps.ensureImmutableShortcutsNotIncludedWithIds((List) shortcutIds); + + for (int i = shortcutIds.size() - 1; i >= 0; i--) { + ps.disableWithId(Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)), + disabledMessage, disabledMessageResId, + /* overrideImmutable=*/ false); + } + } + packageShortcutsChanged(packageName, userId); + } + + @Override + public void enableShortcuts(String packageName, List shortcutIds, @UserIdInt int userId) { + verifyCaller(packageName, userId); + Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided"); + + synchronized (mLock) { + final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + + ps.ensureImmutableShortcutsNotIncludedWithIds((List) shortcutIds); + + for (int i = shortcutIds.size() - 1; i >= 0; i--) { + ps.enableWithId((String) shortcutIds.get(i)); + } } packageShortcutsChanged(packageName, userId); } @@ -1540,8 +1593,12 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkNotNull(shortcutIds, "shortcutIds must be provided"); synchronized (mLock) { + final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId); + + ps.ensureImmutableShortcutsNotIncludedWithIds((List) shortcutIds); + for (int i = shortcutIds.size() - 1; i >= 0; i--) { - getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId( + ps.deleteDynamicWithId( Preconditions.checkStringNotEmpty((String) shortcutIds.get(i))); } } @@ -1569,6 +1626,17 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Override + public ParceledListSlice getManifestShortcuts(String packageName, + @UserIdInt int userId) { + verifyCaller(packageName, userId); + synchronized (mLock) { + return getShortcutsWithQueryLocked( + packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, + ShortcutInfo::isManifestShortcut); + } + } + @Override public ParceledListSlice getPinnedShortcuts(String packageName, @UserIdInt int userId) { @@ -1883,18 +1951,24 @@ public class ShortcutService extends IShortcutService.Stub { return false; } if (componentName != null) { - if (si.getActivityComponent() != null - && !si.getActivityComponent().equals(componentName)) { + if (si.getActivity() != null + && !si.getActivity().equals(componentName)) { return false; } } - final boolean matchDynamic = - ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) - && si.isDynamic(); - final boolean matchPinned = - ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) - && si.isPinned(); - return matchDynamic || matchPinned; + if (((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) + && si.isDynamic()) { + return true; + } + if (((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0) + && si.isPinned()) { + return true; + } + if (((queryFlags & ShortcutQuery.FLAG_GET_MANIFEST) != 0) + && si.isManifestShortcut()) { + return true; + } + return false; }, cloneFlag, callingPackage, launcherUserId); } @@ -1967,7 +2041,7 @@ public class ShortcutService extends IShortcutService.Stub { final ShortcutInfo si = getShortcutInfoLocked( launcherUserId, callingPackage, packageName, shortcutId, userId); // "si == null" should suffice here, but check the flags too just to make sure. - if (si == null || !(si.isDynamic() || si.isPinned())) { + if (si == null || !si.isEnabled() || !si.isAlive()) { return null; } return si.getIntent(); @@ -2114,31 +2188,37 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG) { Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId); } - final ArrayList gonePackages = new ArrayList<>(); - synchronized (mLock) { - final ShortcutUser user = getUserShortcutsLocked(ownerUserId); + final long start = injectElapsedRealtime(); + try { + final ArrayList gonePackages = new ArrayList<>(); - user.forAllPackageItems(spi -> { - if (spi.getPackageInfo().isShadow()) { - return; // Don't delete shadow information. - } - final int versionCode = getApplicationVersionCode( - spi.getPackageName(), spi.getPackageUserId()); - if (versionCode >= 0) { - // Package still installed, see if it's updated. - getUserShortcutsLocked(ownerUserId).handlePackageUpdated( - spi.getPackageName(), versionCode); - } else { - gonePackages.add(PackageWithUser.of(spi)); - } - }); - if (gonePackages.size() > 0) { - for (int i = gonePackages.size() - 1; i >= 0; i--) { - final PackageWithUser pu = gonePackages.get(i); - cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId); + synchronized (mLock) { + final ShortcutUser user = getUserShortcutsLocked(ownerUserId); + + // Find packages that have been uninstalled. + user.forAllPackageItems(spi -> { + if (spi.getPackageInfo().isShadow()) { + return; // Don't delete shadow information. + } + if (!isPackageInstalled(spi.getPackageName(), spi.getPackageUserId())) { + gonePackages.add(PackageWithUser.of(spi)); + } + }); + if (gonePackages.size() > 0) { + for (int i = gonePackages.size() - 1; i >= 0; i--) { + final PackageWithUser pu = gonePackages.get(i); + cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId); + } } + + // Then for each installed app, publish manifest shortcuts when needed. + forInstalledApplications(ownerUserId, ai -> { + user.handlePackageAddedOrUpdated(ai.packageName); + }); } + } finally { + logDurationStat(Stats.CHECK_PACKAGE_CHANGES, start); } } @@ -2147,8 +2227,9 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, String.format("handlePackageAdded: %s user=%d", packageName, userId)); } synchronized (mLock) { - forEachLoadedUserLocked(user -> - user.attemptToRestoreIfNeededAndSave(this, packageName, userId)); + final ShortcutUser user = getUserShortcutsLocked(userId); + user.attemptToRestoreIfNeededAndSave(this, packageName, userId); + user.handlePackageAddedOrUpdated(packageName); } } @@ -2158,14 +2239,12 @@ public class ShortcutService extends IShortcutService.Stub { packageName, userId)); } synchronized (mLock) { - forEachLoadedUserLocked(user -> - user.attemptToRestoreIfNeededAndSave(this, packageName, userId)); + final ShortcutUser user = getUserShortcutsLocked(userId); + user.attemptToRestoreIfNeededAndSave(this, packageName, userId); - final int versionCode = getApplicationVersionCode(packageName, userId); - if (versionCode < 0) { - return; // shouldn't happen + if (isPackageInstalled(packageName, userId)) { + user.handlePackageAddedOrUpdated(packageName); } - getUserShortcutsLocked(userId).handlePackageUpdated(packageName, versionCode); } } @@ -2187,10 +2266,16 @@ public class ShortcutService extends IShortcutService.Stub { // === PackageManager interaction === + @Nullable PackageInfo getPackageInfoWithSignatures(String packageName, @UserIdInt int userId) { return injectPackageInfo(packageName, userId, true); } + @Nullable + PackageInfo getPackageInfo(String packageName, @UserIdInt int userId) { + return injectPackageInfo(packageName, userId, false); + } + int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) { final long token = injectClearCallingIdentity(); try { @@ -2205,6 +2290,7 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Nullable @VisibleForTesting PackageInfo injectPackageInfo(String packageName, @UserIdInt int userId, boolean getSignatures) { @@ -2227,6 +2313,7 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Nullable @VisibleForTesting ApplicationInfo injectApplicationInfo(String packageName, @UserIdInt int userId) { final long start = injectElapsedRealtime(); @@ -2244,6 +2331,61 @@ public class ShortcutService extends IShortcutService.Stub { } } + @Nullable + @VisibleForTesting + PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) { + final long start = injectElapsedRealtime(); + final long token = injectClearCallingIdentity(); + try { + return mIPackageManager.getPackageInfo(packageName, + PACKAGE_MATCH_FLAGS | PackageManager.GET_ACTIVITIES + | PackageManager.GET_META_DATA, userId); + } catch (RemoteException e) { + // Shouldn't happen. + Slog.wtf(TAG, "RemoteException", e); + return null; + } finally { + injectRestoreCallingIdentity(token); + + logDurationStat(Stats.GET_ACTIVITIES_WITH_METADATA, start); + } + } + + @Nullable + @VisibleForTesting + List injectInstalledApplications(@UserIdInt int userId) { + final long start = injectElapsedRealtime(); + final long token = injectClearCallingIdentity(); + try { + final ParceledListSlice parceledList = + mIPackageManager.getInstalledApplications(PACKAGE_MATCH_FLAGS, userId); + if (parceledList == null) { + return Collections.emptyList(); + } + return parceledList.getList(); + } catch (RemoteException e) { + // Shouldn't happen. + Slog.wtf(TAG, "RemoteException", e); + return null; + } finally { + injectRestoreCallingIdentity(token); + + logDurationStat(Stats.GET_INSTALLED_APPLICATIONS, start); + } + } + + private void forInstalledApplications(@UserIdInt int userId, + Consumer callback) { + final List list = injectInstalledApplications(userId); + for (int i = list.size() - 1; i >= 0; i--) { + final ApplicationInfo ai = list.get(i); + + if ((ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { + callback.accept(ai); + } + } + } + private boolean isApplicationFlagSet(String packageName, int userId, int flags) { final ApplicationInfo ai = injectApplicationInfo(packageName, userId); return (ai != null) && ((ai.flags & flags) == flags); @@ -2253,15 +2395,10 @@ public class ShortcutService extends IShortcutService.Stub { return isApplicationFlagSet(packageName, userId, ApplicationInfo.FLAG_INSTALLED); } - /** - * @return the version code of the package, or -1 if the app is not installed. - */ - int getApplicationVersionCode(String packageName, int userId) { - final ApplicationInfo ai = injectApplicationInfo(packageName, userId); - if ((ai == null) || ((ai.flags & ApplicationInfo.FLAG_INSTALLED) == 0)) { - return -1; - } - return ai.versionCode; + @Nullable + XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { +// TODO Doesn't seem like ACROSS_USER is needed, but double check. + return activityInfo.loadXmlMetaData(mContext.getPackageManager(), key); } // === Backup & restore === @@ -2402,8 +2539,10 @@ public class ShortcutService extends IShortcutService.Stub { dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO, "getPackageInfo()"); dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO_WITH_SIG, "getPackageInfo(SIG)"); dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo"); - dumpStatLS(pw, p, Stats.CLEANUP_DANGLING_BITMAPS, "cleanupDanglingBitmaps"); + dumpStatLS(pw, p, Stats.GET_ACTIVITIES_WITH_METADATA, "getActivities+metadata"); + dumpStatLS(pw, p, Stats.GET_INSTALLED_APPLICATIONS, "getInstalledApplications"); + dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges"); } for (int i = 0; i < mUsers.size(); i++) { @@ -2783,12 +2922,19 @@ public class ShortcutService extends IShortcutService.Stub { } @VisibleForTesting - ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) { + ShortcutPackage getPackageShortcutForTest(String packageName, int userId) { synchronized (mLock) { final ShortcutUser user = mUsers.get(userId); if (user == null) return null; - final ShortcutPackage pkg = user.getAllPackagesForTest().get(packageName); + return user.getAllPackagesForTest().get(packageName); + } + } + + @VisibleForTesting + ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) { + synchronized (mLock) { + final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId); if (pkg == null) return null; return pkg.findShortcutById(shortcutId); diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java index d38cfba2b1c07..840c3df97cc34 100644 --- a/services/core/java/com/android/server/pm/ShortcutUser.java +++ b/services/core/java/com/android/server/pm/ShortcutUser.java @@ -24,6 +24,7 @@ import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; @@ -40,6 +41,8 @@ import java.util.function.Consumer; /** * User information used by {@link ShortcutService}. + * + * All methods should be guarded by {@code #mService.mLock}. */ class ShortcutUser { private static final String TAG = ShortcutService.TAG; @@ -230,19 +233,16 @@ class ShortcutUser { } } - /** - * Called when a package is updated. - */ - public void handlePackageUpdated(@NonNull String packageName, - int newVersionCode) { - if (!mPackages.containsKey(packageName)) { - return; + public void handlePackageAddedOrUpdated(@NonNull String packageName) { + final boolean isNewApp = !mPackages.containsKey(packageName); + + final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName); + + if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp)) { + if (isNewApp) { + mPackages.remove(packageName); + } } - final ShortcutPackage p = getPackageShortcutsIfExists(packageName); - if (p == null) { - return; // No need to instantiate ShortcutPackage. - } - p.handlePackageUpdated(newVersionCode); } public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName, diff --git a/services/tests/servicestests/res/values/strings.xml b/services/tests/servicestests/res/values/strings.xml new file mode 100644 index 0000000000000..2f9d06c318c57 --- /dev/null +++ b/services/tests/servicestests/res/values/strings.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_0.xml b/services/tests/servicestests/res/xml/shortcut_0.xml new file mode 100644 index 0000000000000..fda001ec0aa58 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_0.xml @@ -0,0 +1,2 @@ + + diff --git a/services/tests/servicestests/res/xml/shortcut_1.xml b/services/tests/servicestests/res/xml/shortcut_1.xml new file mode 100644 index 0000000000000..c370e74f9a41f --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_1.xml @@ -0,0 +1,14 @@ + + + diff --git a/services/tests/servicestests/res/xml/shortcut_1_disable.xml b/services/tests/servicestests/res/xml/shortcut_1_disable.xml new file mode 100644 index 0000000000000..08ecac31cbce0 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_1_disable.xml @@ -0,0 +1,11 @@ + + + diff --git a/services/tests/servicestests/res/xml/shortcut_2.xml b/services/tests/servicestests/res/xml/shortcut_2.xml new file mode 100644 index 0000000000000..9e923f3eb8d34 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_2.xml @@ -0,0 +1,26 @@ + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml new file mode 100644 index 0000000000000..d90c18d47389b --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_2_duplicate.xml @@ -0,0 +1,12 @@ + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_5.xml b/services/tests/servicestests/res/xml/shortcut_5.xml new file mode 100644 index 0000000000000..f3f71d2f5e29c --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_5.xml @@ -0,0 +1,40 @@ + + + + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_1.xml b/services/tests/servicestests/res/xml/shortcut_error_1.xml new file mode 100644 index 0000000000000..2c5142043ffb4 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_error_1.xml @@ -0,0 +1,11 @@ + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_2.xml b/services/tests/servicestests/res/xml/shortcut_error_2.xml new file mode 100644 index 0000000000000..d075e7d1fb535 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_error_2.xml @@ -0,0 +1,11 @@ + + + + diff --git a/services/tests/servicestests/res/xml/shortcut_error_3.xml b/services/tests/servicestests/res/xml/shortcut_error_3.xml new file mode 100644 index 0000000000000..30bf56e940635 --- /dev/null +++ b/services/tests/servicestests/res/xml/shortcut_error_3.xml @@ -0,0 +1,11 @@ + + + + diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java index 8a2a58cc90194..1a0a00389ceff 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java @@ -15,17 +15,22 @@ */ package com.android.server.pm; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDisabled; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamic; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllDynamicOrPinned; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllEnabled; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIcon; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconFile; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIconResId; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveIntents; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllHaveTitle; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllImmutable; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllKeyFieldsOnly; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllManifest; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveIntents; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotHaveTitle; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotKeyFieldsOnly; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllNotManifest; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllPinned; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllStringsResolved; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllUnique; @@ -33,8 +38,10 @@ import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertBundleEmpty; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackNotReceived; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCallbackReceived; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertCannotUpdateImmutable; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicAndPinned; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicOnly; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut; @@ -70,6 +77,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ILauncherApps; import android.content.pm.LauncherApps; @@ -83,6 +91,7 @@ import android.content.pm.ShortcutServiceInternal; import android.content.pm.Signature; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; @@ -143,6 +152,11 @@ import java.util.function.Consumer; adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest \ -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner + + * TODO More tests for pinning + manifest shortcuts + * TODO Manifest shortcuts + app upgrade -> launcher callback. + * Also locale change should trigger launcehr 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. @@ -155,9 +169,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 = false; // DO NOT SUBMIT WITH true + private static final boolean ENABLE_DUMP = true; // DO NOT SUBMIT WITH true - private static final boolean DUMP_IN_TEARDOWN = false; // DO NOT SUBMIT WITH true + private static final boolean DUMP_IN_TEARDOWN = true; // DO NOT SUBMIT WITH true private static final String[] EMPTY_STRINGS = new String[0]; // Just for readability. @@ -229,6 +243,15 @@ public class ShortcutManagerTest extends InstrumentationTestCase { public int getUserId() { return UserHandle.USER_SYSTEM; } + + public PackageInfo injectGetActivitiesWithMetadata( + String packageName, @UserIdInt int userId) { + return ShortcutManagerTest.this.injectGetActivitiesWithMetadata(packageName, userId); + } + + public XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { + return ShortcutManagerTest.this.injectXmlMetaData(activityInfo, key); + } } /** ShortcutService with injection override methods. */ @@ -338,6 +361,21 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return pi != null ? pi.applicationInfo : null; } + @Override + List injectInstalledApplications(@UserIdInt int userId) { + return getInstalledApplications(userId); + } + + @Override + PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) { + return mContext.injectGetActivitiesWithMetadata(packageName, userId); + } + + @Override + XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { + return mContext.injectXmlMetaData(activityInfo, key); + } + @Override void postToHandler(Runnable r) { final long token = mContext.injectClearCallingIdentity(); @@ -524,6 +562,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { private final ArrayList mCallerPermissions = new ArrayList<>(); + private final HashMap> mActivityMetadataResId + = new HashMap<>(); + static { QUERY_ALL.setQueryFlags( ShortcutQuery.FLAG_GET_DYNAMIC | ShortcutQuery.FLAG_GET_PINNED); @@ -720,6 +761,12 @@ public class ShortcutManagerTest extends InstrumentationTestCase { }); } + private void updatePackageLastUpdateTime(String packageName, long increment) { + updatePackageInfo(packageName, pi -> { + pi.lastUpdateTime += increment; + }); + } + private void uninstallPackage(int userId, String packageName) { if (ENABLE_DUMP) { Log.i(TAG, "Unnstall package " + packageName + " / " + userId); @@ -742,8 +789,12 @@ public class ShortcutManagerTest extends InstrumentationTestCase { final PackageInfo ret = new PackageInfo(); ret.packageName = pi.packageName; ret.versionCode = pi.versionCode; + ret.lastUpdateTime = pi.lastUpdateTime; + ret.applicationInfo = new ApplicationInfo(pi.applicationInfo); ret.applicationInfo.uid = UserHandle.getUid(userId, pi.applicationInfo.uid); + ret.applicationInfo.packageName = pi.packageName; + if (mUninstalledPackages.contains(PackageWithUser.of(userId, packageName))) { ret.applicationInfo.flags &= ~ApplicationInfo.FLAG_INSTALLED; } @@ -755,6 +806,66 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return ret; } + private void addApplicationInfo(PackageInfo pi, List list) { + if (pi != null && pi.applicationInfo != null) { + list.add(pi.applicationInfo); + } + } + + private List getInstalledApplications(int userId) { + final ArrayList ret = new ArrayList<>(); + + addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_1, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_2, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_3, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(CALLING_PACKAGE_4, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(LAUNCHER_1, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(LAUNCHER_2, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(LAUNCHER_3, userId, false), ret); + addApplicationInfo(getInjectedPackageInfo(LAUNCHER_4, userId, false), ret); + + return ret; + } + + private void addManifestShortcutResource(ComponentName activity, int resId) { + final String packageName = activity.getPackageName(); + HashMap map = mActivityMetadataResId.get(packageName); + if (map == null) { + map = new HashMap<>(); + mActivityMetadataResId.put(packageName, map); + } + map.put(activity, resId); + } + + private PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) { + final PackageInfo ret = getInjectedPackageInfo(packageName, userId, + /* getSignatures=*/ false); + + final HashMap activities = mActivityMetadataResId.get(packageName); + if (activities != null) { + final ArrayList list = new ArrayList<>(); + + for (ComponentName cn : activities.keySet()) { + ActivityInfo ai = new ActivityInfo(); + ai.packageName = cn.getPackageName(); + ai.name = cn.getClassName(); + ai.metaData = new Bundle(); + ai.metaData.putInt(ShortcutParser.METADATA_KEY, activities.get(cn)); + list.add(ai); + } + ret.activities = list.toArray(new ActivityInfo[list.size()]); + } + return ret; + } + + private XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) { + if (!ShortcutParser.METADATA_KEY.equals(key) || activityInfo.metaData == null) { + return null; + } + final int resId = activityInfo.metaData.getInt(key); + return getTestContext().getResources().getXml(resId); + } + /** Replace the current calling package */ private void setCaller(String packageName, int userId) { mInjectedClientPackage = packageName; @@ -883,6 +994,12 @@ public class ShortcutManagerTest extends InstrumentationTestCase { makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); } + private ShortcutInfo makeShortcutWithTitle(String id, String title) { + return makeShortcut( + id, title, /* activity =*/ null, /* icon =*/ null, + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* weight =*/ 0); + } + /** * Make a shortcut with an ID and timestamp. */ @@ -949,7 +1066,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { Icon icon, Intent intent, int rank) { final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext) .setId(id) - .setActivityComponent(new ComponentName(mClientContext.getPackageName(), "dummy")) + .setActivity(new ComponentName(mClientContext.getPackageName(), "dummy")) .setTitle(title) .setRank(rank) .setIntent(intent); @@ -957,7 +1074,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { b.setIcon(icon); } if (activity != null) { - b.setActivityComponent(activity); + b.setActivity(activity); } final ShortcutInfo s = b.build(); @@ -1126,8 +1243,10 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return new File(si.getBitmapPath()).getName(); } - private ShortcutInfo getPackageShortcut(String packageName, String shortcutId) { - return getPackageShortcut(packageName, shortcutId, getCallingUserId()); + private List getCallerShortcuts() { + final ShortcutPackage p = mService.getPackageShortcutForTest( + getCallingPackage(), getCallingUserId()); + return p == null ? null : p.getAllShortcutsForTest(); } private ShortcutInfo getCallerShortcut(String shortcutId) { @@ -1157,6 +1276,13 @@ public class ShortcutManagerTest extends InstrumentationTestCase { return infoList.get(0); } + private Intent genPackageAddIntent(String pakcageName, int userId) { + Intent i = new Intent(Intent.ACTION_PACKAGE_ADDED); + i.setData(Uri.parse("package:" + pakcageName)); + i.putExtra(Intent.EXTRA_USER_HANDLE, userId); + return i; + } + private Intent genPackageDeleteIntent(String pakcageName, int userId) { Intent i = new Intent(Intent.ACTION_PACKAGE_REMOVED); i.setData(Uri.parse("package:" + pakcageName)); @@ -1291,9 +1417,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { // === Test for app side APIs === - /** Test for {@link android.content.pm.ShortcutManager#getMaxDynamicShortcutCount()} */ + /** Test for {@link android.content.pm.ShortcutManager#getMaxShortcutCountForActivity()} */ public void testGetMaxDynamicShortcutCount() { - assertEquals(MAX_SHORTCUTS, mManager.getMaxDynamicShortcutCount()); + assertEquals(MAX_SHORTCUTS, mManager.getMaxShortcutCountForActivity()); } /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */ @@ -1301,6 +1427,11 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertEquals(MAX_UPDATES_PER_INTERVAL, mManager.getRemainingCallCount()); } + public void getIconMaxDimensions() { + assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxWidth()); + assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxHeight()); + } + /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */ public void testGetRateLimitResetTime() { assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); @@ -2393,7 +2524,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { ShortcutInfo si = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, "dummy")) + .setActivity(new ComponentName(mClientContext, "dummy")) .setTitleResId(10) .setTextResId(11) .setDisabledMessageResId(12) @@ -2404,7 +2535,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { ShortcutInfo si = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, "dummy")) + .setActivity(new ComponentName(mClientContext, "dummy")) .setTitleResId(10) .setTextResId(11) .setDisabledMessageResId(12) @@ -2514,7 +2645,6 @@ public class ShortcutManagerTest extends InstrumentationTestCase { } public void testPinShortcutAndGetPinnedShortcuts() { - // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); @@ -2551,38 +2681,155 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); mManager.removeDynamicShortcuts(list("s2")); assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + assertShortcutIds(mManager.getDynamicShortcuts(), "s1"); }); runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); mManager.removeDynamicShortcuts(list("s3")); assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + + assertShortcutIds(mManager.getDynamicShortcuts(), "s2", "s4"); }); runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { assertShortcutIds(mManager.getPinnedShortcuts() /* none */); mManager.removeDynamicShortcuts(list("s2")); assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + + assertEmpty(mManager.getDynamicShortcuts()); }); // Get pinned shortcuts from launcher runWithCaller(LAUNCHER_1, USER_0, () -> { // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists. - assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly( + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled( mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1, - /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))), + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))), "s2"); + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled( + mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))), + "s3", "s4"); + + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled( + mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_3, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))) + /* none */); + }); + } + + /** + * This is similar to the above test, except it used "disable" instead of "remove". It also + * does "enable". + */ + public void testDisableAndEnableShortcuts() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); + final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); + + assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2))); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + final ShortcutInfo s2_2 = makeShortcutWithTimestamp("s2", 1500); + final ShortcutInfo s2_3 = makeShortcutWithTimestamp("s3", 3000); + final ShortcutInfo s2_4 = makeShortcutWithTimestamp("s4", 500); + assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4))); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + final ShortcutInfo s3_2 = makeShortcutWithTimestamp("s2", 1000); + assertTrue(mManager.setDynamicShortcuts(list(s3_2))); + }); + + // Pin some. + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, + list("s2", "s3"), getCallingUser()); + + mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, + list("s3", "s4", "s5"), getCallingUser()); + + mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, + list("s3"), getCallingUser()); // Note ID doesn't exist + }); + + // Disable some. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + + mManager.disableShortcuts(list("s2")); + + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + assertShortcutIds(mManager.getDynamicShortcuts(), "s1"); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + + // disable should work even if a shortcut is not dynamic, so try calling "remove" first + // here. + mManager.removeDynamicShortcuts(list("s3")); + mManager.disableShortcuts(list("s3")); + + assertShortcutIds(mManager.getPinnedShortcuts(), "s3", "s4"); + assertShortcutIds(mManager.getDynamicShortcuts(), "s2", "s4"); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + + mManager.disableShortcuts(list("s2")); + + assertShortcutIds(mManager.getPinnedShortcuts() /* none */); + + assertEmpty(mManager.getDynamicShortcuts()); + assertEmpty(getCallerShortcuts()); + }); + + // Get pinned shortcuts from launcher + runWithCaller(LAUNCHER_1, USER_0, () -> { + // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists. + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllDisabled( + mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))), + "s2"); + assertFalse(mLauncherApps.startShortcut( + CALLING_PACKAGE_1, "s2", null, null, HANDLE_USER_0)); + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly( mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_2, /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))), "s3", "s4"); + assertFalse(mLauncherApps.startShortcut( + CALLING_PACKAGE_2, "s3", null, null, HANDLE_USER_0)); + assertTrue(mLauncherApps.startShortcut( + CALLING_PACKAGE_2, "s4", null, null, HANDLE_USER_0)); - assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly( + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled( mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_3, - /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser()))) + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))) /* none */); }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.enableShortcuts(list("s2")); + + assertShortcutIds(mManager.getPinnedShortcuts(), "s2"); + assertShortcutIds(mManager.getDynamicShortcuts(), "s1"); + }); + runWithCaller(LAUNCHER_1, USER_0, () -> { + // CALLING_PACKAGE_1 deleted s2, but it's pinned, so it still exists. + assertShortcutIds(assertAllPinned(assertAllNotKeyFieldsOnly(assertAllEnabled( + mLauncherApps.getShortcuts(buildQuery(/* time =*/ 0, CALLING_PACKAGE_1, + /* activity =*/ null, ShortcutQuery.FLAG_GET_PINNED), getCallingUser())))), + "s2"); + assertTrue(mLauncherApps.startShortcut( + CALLING_PACKAGE_1, "s2", null, null, HANDLE_USER_0)); + }); } public void testPinShortcutAndGetPinnedShortcuts_multi() { @@ -6007,14 +6254,14 @@ public class ShortcutManagerTest extends InstrumentationTestCase { () -> new ShortcutInfo.Builder(getTestContext()).setIntent(new Intent())); assertExpectException( NullPointerException.class, - "activityComponent must be provided", + "activity must be provided", () -> new ShortcutInfo.Builder(getTestContext()).setId("id").build() .enforceMandatoryFields()); assertExpectException( IllegalArgumentException.class, "title must be provided", () -> new ShortcutInfo.Builder(getTestContext()).setId("id") - .setActivityComponent( + .setActivity( new ComponentName(getTestContext().getPackageName(), "s")) .build() .enforceMandatoryFields()); @@ -6022,7 +6269,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { NullPointerException.class, "Intent must be provided", () -> new ShortcutInfo.Builder(getTestContext()).setId("id") - .setActivityComponent( + .setActivity( new ComponentName(getTestContext().getPackageName(), "s")) .setTitle("x").build() .enforceMandatoryFields()); @@ -6035,7 +6282,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { .setTitle("title") .setIntent(makeIntent("action", ShortcutActivity.class)) .build()); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals(USER_10, si.getUserId()); assertEquals(HANDLE_USER_10, si.getUserHandle()); assertEquals("id", si.getId()); @@ -6047,7 +6294,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = new ShortcutInfo.Builder(getTestContext()) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitle("title") .setText("text") @@ -6063,9 +6310,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = parceled(si); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(123, si.getIcon().getResId()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6090,7 +6337,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = new ShortcutInfo.Builder(getTestContext()) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitleResId(10) .setTextResId(11) @@ -6106,9 +6353,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = parceled(si); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(123, si.getIcon().getResId()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6131,7 +6378,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitle("title") .setText("text") @@ -6149,9 +6396,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertEquals(USER_11, si.getUserId()); assertEquals(HANDLE_USER_11, si.getUserHandle()); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(123, si.getIcon().getResId()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6168,9 +6415,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(null, si.getIcon()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6188,9 +6435,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(null, si.getIcon()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6207,9 +6454,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(null, si.getActivityComponent()); + assertEquals(null, si.getActivity()); assertEquals(null, si.getIcon()); assertEquals(null, si.getTitle()); assertEquals(null, si.getText()); @@ -6232,7 +6479,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitleResId(10) .setTextResId(11) @@ -6250,9 +6497,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertEquals(USER_11, si.getUserId()); assertEquals(HANDLE_USER_11, si.getUserHandle()); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(123, si.getIcon().getResId()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6269,9 +6516,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(null, si.getIcon()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6289,9 +6536,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(new ComponentName("a", "b"), si.getActivityComponent()); + assertEquals(new ComponentName("a", "b"), si.getActivity()); assertEquals(null, si.getIcon()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6308,9 +6555,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); - assertEquals(mClientContext.getPackageName(), si.getPackageName()); + assertEquals(mClientContext.getPackageName(), si.getPackage()); assertEquals("id", si.getId()); - assertEquals(null, si.getActivityComponent()); + assertEquals(null, si.getActivity()); assertEquals(null, si.getIcon()); assertEquals(0, si.getTitleResId()); assertEquals(0, si.getTextResId()); @@ -6336,7 +6583,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { .build(); ShortcutInfo si = sorig.clone(/* clone flags*/ 0); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); assertEquals("title", si.getTitle()); assertEquals("action", si.getIntent().getAction()); @@ -6344,7 +6591,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); assertEquals("title", si.getTitle()); assertEquals("action", si.getIntent().getAction()); @@ -6352,7 +6599,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); assertEquals("title", si.getTitle()); assertEquals(null, si.getIntent()); @@ -6360,7 +6607,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO); - assertEquals(getTestContext().getPackageName(), si.getPackageName()); + assertEquals(getTestContext().getPackageName(), si.getPackage()); assertEquals("id", si.getId()); assertEquals(null, si.getTitle()); assertEquals(null, si.getIntent()); @@ -6372,7 +6619,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext()) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitle("title") .setText("text") @@ -6390,9 +6637,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(/* flags=*/ 0); si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") - .setActivityComponent(new ComponentName("x", "y")).build()); + .setActivity(new ComponentName("x", "y")).build()); assertEquals("text", si.getText()); - assertEquals(new ComponentName("x", "y"), si.getActivityComponent()); + assertEquals(new ComponentName("x", "y"), si.getActivity()); si = sorig.clone(/* flags=*/ 0); si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") @@ -6500,7 +6747,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext()) .setId("id") - .setActivityComponent(new ComponentName("a", "b")) + .setActivity(new ComponentName("a", "b")) .setIcon(Icon.createWithResource(mClientContext, 123)) .setTitleResId(10) .setTextResId(11) @@ -6518,9 +6765,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { si = sorig.clone(/* flags=*/ 0); si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") - .setActivityComponent(new ComponentName("x", "y")).build()); + .setActivity(new ComponentName("x", "y")).build()); assertEquals(11, si.getTextResId()); - assertEquals(new ComponentName("x", "y"), si.getActivityComponent()); + assertEquals(new ComponentName("x", "y"), si.getActivity()); si = sorig.clone(/* flags=*/ 0); si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") @@ -6633,7 +6880,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) .setIcon(bmp32x32) .setTitle("title") .setText("text") @@ -6659,9 +6906,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertEquals(USER_10, si.getUserId()); assertEquals(HANDLE_USER_10, si.getUserHandle()); - assertEquals(CALLING_PACKAGE_1, si.getPackageName()); + assertEquals(CALLING_PACKAGE_1, si.getPackage()); assertEquals("id", si.getId()); - assertEquals(ShortcutActivity2.class.getName(), si.getActivityComponent().getClassName()); + assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName()); assertEquals(null, si.getIcon()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6688,7 +6935,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) .setIcon(bmp32x32) .setTitleResId(10) .setTextResId(11) @@ -6714,9 +6961,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { assertEquals(USER_10, si.getUserId()); assertEquals(HANDLE_USER_10, si.getUserHandle()); - assertEquals(CALLING_PACKAGE_1, si.getPackageName()); + assertEquals(CALLING_PACKAGE_1, si.getPackage()); assertEquals("id", si.getId()); - assertEquals(ShortcutActivity2.class.getName(), si.getActivityComponent().getClassName()); + assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName()); assertEquals(null, si.getIcon()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6743,7 +6990,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) .setIcon(bmp32x32) .setTitle("title") .setText("text") @@ -6768,9 +7015,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { ShortcutInfo si; si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_0); - assertEquals(CALLING_PACKAGE_1, si.getPackageName()); + assertEquals(CALLING_PACKAGE_1, si.getPackage()); assertEquals("id", si.getId()); - assertEquals(ShortcutActivity2.class.getName(), si.getActivityComponent().getClassName()); + assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName()); assertEquals(null, si.getIcon()); assertEquals("title", si.getTitle()); assertEquals("text", si.getText()); @@ -6796,7 +7043,7 @@ public class ShortcutManagerTest extends InstrumentationTestCase { pb.putInt("k", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") - .setActivityComponent(new ComponentName(mClientContext, ShortcutActivity2.class)) + .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) .setIcon(bmp32x32) .setTitleResId(10) .setTextResId(11) @@ -6821,9 +7068,9 @@ public class ShortcutManagerTest extends InstrumentationTestCase { ShortcutInfo si; si = mService.getPackageShortcutForTest(CALLING_PACKAGE_1, "id", USER_0); - assertEquals(CALLING_PACKAGE_1, si.getPackageName()); + assertEquals(CALLING_PACKAGE_1, si.getPackage()); assertEquals("id", si.getId()); - assertEquals(ShortcutActivity2.class.getName(), si.getActivityComponent().getClassName()); + assertEquals(ShortcutActivity2.class.getName(), si.getActivity().getClassName()); assertEquals(null, si.getIcon()); assertEquals(10, si.getTitleResId()); assertEquals(11, si.getTextResId()); @@ -6849,4 +7096,810 @@ public class ShortcutManagerTest extends InstrumentationTestCase { // Dump after having some icons. dumpsysOnLogcat("test1", /* force= */ true); } + + public void testManifestShortcut_publishOnUnlockUser() { + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_1); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()), + R.xml.shortcut_5); + + // Unlock user-0. + mService.handleUnlockUser(USER_0); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Try on another user, with some packages uninstalled. + uninstallPackage(USER_10, CALLING_PACKAGE_1); + uninstallPackage(USER_10, CALLING_PACKAGE_3); + + mService.handleUnlockUser(USER_10); + + runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_10, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_10, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Now change the resources for package 1, and unlock again. + // But we still see *old* shortcuts, because the package version and install time + // hasn't changed. + shutdownServices(); + + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_5); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()), + R.xml.shortcut_1); + + initService(); + mService.handleUnlockUser(USER_0); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Do it again, but this time we change the app version, so we do detect the changes. + shutdownServices(); + + updatePackageVersion(CALLING_PACKAGE_1, 1); + updatePackageLastUpdateTime(CALLING_PACKAGE_3, 1); + + initService(); + mService.handleUnlockUser(USER_0); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Next, try removing all shortcuts, with some of them pinned. + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms3"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("ms2"), HANDLE_USER_0); + mLauncherApps.pinShortcuts(CALLING_PACKAGE_3, list("ms1"), HANDLE_USER_0); + }); + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest( + assertAllEnabled(mManager.getPinnedShortcuts())))), + "ms3"); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest( + assertAllEnabled(mManager.getPinnedShortcuts())))), + "ms2"); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllManifest( + assertAllEnabled(mManager.getPinnedShortcuts())))), + "ms1"); + }); + + shutdownServices(); + + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_0); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_1); + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_3, ShortcutActivity.class.getName()), + R.xml.shortcut_0); + + updatePackageVersion(CALLING_PACKAGE_1, 1); + updatePackageVersion(CALLING_PACKAGE_2, 1); + updatePackageVersion(CALLING_PACKAGE_3, 1); + + initService(); + mService.handleUnlockUser(USER_0); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest( + assertAllDisabled(mManager.getPinnedShortcuts())))), + "ms3"); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest( + assertAllDisabled(mManager.getPinnedShortcuts())))), + "ms2"); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllNotManifest( + assertAllDisabled(mManager.getPinnedShortcuts())))), + "ms1"); + }); + + // Make sure we don't have ShortcutPackage for packages that don't have shortcuts. + assertNull(mService.getPackageShortcutForTest(CALLING_PACKAGE_4, USER_0)); + assertNull(mService.getPackageShortcutForTest(LAUNCHER_1, USER_0)); + } + + + public void testManifestShortcut_publishOnBroadcast() { + // First, no packages are installed. + uninstallPackage(USER_0, CALLING_PACKAGE_1); + uninstallPackage(USER_0, CALLING_PACKAGE_2); + uninstallPackage(USER_0, CALLING_PACKAGE_3); + uninstallPackage(USER_0, CALLING_PACKAGE_4); + uninstallPackage(USER_10, CALLING_PACKAGE_1); + uninstallPackage(USER_10, CALLING_PACKAGE_2); + uninstallPackage(USER_10, CALLING_PACKAGE_3); + uninstallPackage(USER_10, CALLING_PACKAGE_4); + + mService.handleUnlockUser(USER_0); + mService.handleUnlockUser(USER_10); + + // Originally no manifest shortcuts. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_3, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Package 1 updated, with manifest shortcuts. + 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)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Package 2 updated, with manifest shortcuts. + + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_5); + updatePackageVersion(CALLING_PACKAGE_2, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_2, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // Package 2 updated, with less manifest shortcuts. + // This time we use updatePackageLastUpdateTime() instead of updatePackageVersion(). + + dumpsysOnLogcat("Before pinning"); + + // Also pin some. + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("ms2", "ms3"), HANDLE_USER_0); + }); + + dumpsysOnLogcat("After pinning"); + + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_2); + updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_2, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertShortcutIds(assertAllImmutable(assertAllPinned( + mManager.getPinnedShortcuts())), + "ms2", "ms3"); + // ms3 is no longer in manifest, so should be disabled. + // but ms1 and ms2 should be enabled. + assertAllEnabled(list(getCallerShortcut("ms1"))); + assertAllEnabled(list(getCallerShortcut("ms2"))); + assertAllDisabled(list(getCallerShortcut("ms3"))); + }); + + // Package 2 on user 10 has no shortcuts yet. + runWithCaller(CALLING_PACKAGE_2, USER_10, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + }); + // Send PACKAGE_ADD broadcast to have Package 2 on user-10 publish manifest shortcuts. + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_2, USER_10)); + + runWithCaller(CALLING_PACKAGE_2, USER_10, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertEmpty(mManager.getPinnedShortcuts()); + }); + + // But it shouldn't affect user-0. + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + assertShortcutIds(assertAllImmutable(assertAllPinned( + mManager.getPinnedShortcuts())), + "ms2", "ms3"); + assertAllEnabled(list(getCallerShortcut("ms1"))); + assertAllEnabled(list(getCallerShortcut("ms2"))); + assertAllDisabled(list(getCallerShortcut("ms3"))); + }); + + // Package 2 now has no manifest shortcuts. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_2, ShortcutActivity.class.getName()), + R.xml.shortcut_0); + updatePackageLastUpdateTime(CALLING_PACKAGE_2, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_2, USER_0)); + + // No manifest shortcuts, and pinned ones are disabled. + runWithCaller(CALLING_PACKAGE_2, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertShortcutIds(assertAllImmutable(assertAllPinned(assertAllDisabled( + mManager.getPinnedShortcuts()))), + "ms2", "ms3"); + }); + } + + public void testManifestShortcuts_missingMandatoryFields() { + // Start with no apps installed. + uninstallPackage(USER_0, CALLING_PACKAGE_1); + uninstallPackage(USER_0, CALLING_PACKAGE_2); + uninstallPackage(USER_0, CALLING_PACKAGE_3); + uninstallPackage(USER_0, CALLING_PACKAGE_4); + + mService.handleUnlockUser(USER_0); + + // Make sure no manifest shortcuts. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + }); + + // Package 1 updated, which has one valid manifest shortcut and one invalid. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_error_1); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "x1"); + }); + + // Package 1 updated, which has one valid manifest shortcut and one invalid. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_error_2); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "x2"); + }); + + // Package 1 updated, which has one valid manifest shortcut and one invalid. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_error_3); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "x3"); + }); + } + + public void testManifestShortcuts_checkAllFields() { + mService.handleUnlockUser(USER_0); + + // Package 1 updated, which has one valid manifest shortcut and one invalid. + 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)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2", "ms3", "ms4", "ms5"); + + // check first shortcut. + ShortcutInfo si = getCallerShortcut("ms1"); + + 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.string.shortcut_text1, si.getTextResId()); + assertEquals(R.string.shortcut_disabled_message1, si.getDisabledMessageResId()); + 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()); + + // check another + si = getCallerShortcut("ms2"); + + assertEquals("ms2", si.getId()); + assertEquals(R.drawable.icon2, si.getIconResourceId()); + assertEquals(R.string.shortcut_title2, si.getTitleResId()); + assertEquals(R.string.shortcut_text2, si.getTextResId()); + assertEquals(R.string.shortcut_disabled_message2, si.getDisabledMessageResId()); + assertEquals(set("android.shortcut.conversation"), si.getCategories()); + assertEquals("action2", si.getIntent().getAction()); + assertEquals(null, si.getIntent().getData()); + + // check another + si = getCallerShortcut("ms3"); + + assertEquals("ms3", si.getId()); + assertEquals(0, si.getIconResourceId()); + assertEquals(R.string.shortcut_title1, si.getTitleResId()); + assertEquals(0, si.getTextResId()); + assertEquals(0, si.getDisabledMessageResId()); + assertEquals(null, si.getCategories()); + assertEquals("android.intent.action.VIEW", si.getIntent().getAction()); + assertEquals(null, si.getIntent().getData()); + }); + } + + public void testManifestShortcuts_updateAndDisabled_notPinned() { + mService.handleUnlockUser(USER_0); + + // First, just publish a manifest shortcut. + 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)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + + // Make sure there's no other dangling shortcuts. + assertShortcutIds(getCallerShortcuts(), "ms1"); + }); + + // Now version up, the manifest shortcut is disabled now. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_1_disable); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Because shortcut 1 wasn't pinned, it'll just go away. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertEmpty(mManager.getPinnedShortcuts()); + + // Make sure there's no other dangling shortcuts. + assertEmpty(getCallerShortcuts()); + }); + } + + public void testManifestShortcuts_updateAndDisabled_pinned() { + mService.handleUnlockUser(USER_0); + + // First, just publish a manifest shortcut. + 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)); + + // Only the valid one is published. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + assertEmpty(mManager.getPinnedShortcuts()); + + // Make sure there's no other dangling shortcuts. + assertShortcutIds(getCallerShortcuts(), "ms1"); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms1"), HANDLE_USER_0); + }); + + // Now upgrade, the manifest shortcut is disabled now. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_1_disable); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + // Because shortcut 1 was pinned, it'll still exist as pinned, but disabled. + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertEmpty(mManager.getManifestShortcuts()); + assertShortcutIds(assertAllNotManifest(assertAllImmutable(assertAllDisabled( + mManager.getPinnedShortcuts()))), + "ms1"); + + // Make sure the fields are updated. + ShortcutInfo si = getCallerShortcut("ms1"); + + assertEquals("ms1", si.getId()); + assertEquals(R.drawable.icon2, si.getIconResourceId()); + assertEquals(R.string.shortcut_title2, si.getTitleResId()); + assertEquals(R.string.shortcut_text2, si.getTextResId()); + assertEquals(R.string.shortcut_disabled_message2, si.getDisabledMessageResId()); + assertEquals(Intent.ACTION_VIEW, si.getIntent().getAction()); + + // Make sure there's no other dangling shortcuts. + assertShortcutIds(getCallerShortcuts(), "ms1"); + }); + } + + public void testManifestShortcuts_duplicateInSingleActivity() { + mService.handleUnlockUser(USER_0); + + // The XML has two shortcuts with the same ID. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_2_duplicate); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1"); + + // Make sure the first one has survived. (the second one has a different title.) + ShortcutInfo si = getCallerShortcut("ms1"); + assertEquals(R.string.shortcut_title1, si.getTitleResId()); + + // Make sure there's no other dangling shortcuts. + assertShortcutIds(getCallerShortcuts(), "ms1"); + }); + } + + public void testManifestShortcuts_duplicateInTwoActivities() { + mService.handleUnlockUser(USER_0); + + // ShortcutActivity has shortcut ms1 + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + R.xml.shortcut_1); + + // ShortcutActivity2 has two shortcuts, ms1 and ms2. + addManifestShortcutResource( + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), + R.xml.shortcut_2); + updatePackageVersion(CALLING_PACKAGE_1, 1); + mService.mPackageMonitor.onReceive(getTestContext(), + genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllManifest(assertAllImmutable(assertAllEnabled( + mManager.getManifestShortcuts()))), + "ms1", "ms2"); + + // ms1 should belong to ShortcutActivity. + ShortcutInfo si = getCallerShortcut("ms1"); + assertEquals(R.string.shortcut_title1, si.getTitleResId()); + assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + si.getActivity()); + + // ms2 should belong to ShortcutActivity*2*. + si = getCallerShortcut("ms2"); + assertEquals(R.string.shortcut_title2, si.getTitleResId()); + assertEquals(new ComponentName(CALLING_PACKAGE_1, ShortcutActivity2.class.getName()), + si.getActivity()); + + // Make sure there's no other dangling shortcuts. + assertShortcutIds(getCallerShortcuts(), "ms1", "ms2"); + }); + } + + /** + * Manifest shortcuts cannot override shortcuts that were published via the APIs. + */ + public void testManifestShortcuts_cannotOverrideNonManifest() { + mService.handleUnlockUser(USER_0); + + // Create a non-pinned dynamic shortcut and a non-dynamic pinned shortcut. + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.setDynamicShortcuts(list( + makeShortcut("ms1", "title1", + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + /* icon */ null, new Intent("action1"), /* rank */ 0), + makeShortcut("ms2", "title2", + new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), + /* icon */ null, new Intent("action1"), /* rank */ 0))); + }); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2"), HANDLE_USER_0); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("ms2")); + + assertShortcutIds(mManager.getDynamicShortcuts(), "ms1"); + assertShortcutIds(mManager.getPinnedShortcuts(), "ms2"); + assertEmpty(mManager.getManifestShortcuts()); + }); + + // Then update the app with 5 manifest shortcuts. + // Make sure "ms1" and "ms2" won't be replaced. + 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, () -> { + assertShortcutIds(assertAllNotManifest(mManager.getDynamicShortcuts()), "ms1"); + assertShortcutIds(assertAllNotManifest(mManager.getPinnedShortcuts()), "ms2"); + assertShortcutIds(assertAllManifest(mManager.getManifestShortcuts()), + "ms3", "ms4", "ms5"); + + // ms1 and ms2 shouold keep the original title. + ShortcutInfo si = getCallerShortcut("ms1"); + assertEquals("title1", si.getTitle()); + + si = getCallerShortcut("ms2"); + assertEquals("title2", si.getTitle()); + }); + } + + private void checkManifestShortcuts_immutable_verify() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertShortcutIds(assertAllNotManifest(assertAllEnabled( + mManager.getDynamicShortcuts())), + "s1"); + assertShortcutIds(assertAllManifest(assertAllEnabled( + mManager.getManifestShortcuts())), + "ms1"); + assertShortcutIds(assertAllNotManifest(assertAllDisabled( + mManager.getPinnedShortcuts())), + "ms2"); + + assertEquals("t1", getCallerShortcut("s1").getTitle()); + + // Make sure there are no other shortcuts. + assertShortcutIds(getCallerShortcuts(), "s1", "ms1", "ms2"); + }); + } + + /** + * Make sure the APIs won't work on manifest shortcuts. + */ + public void testManifestShortcuts_immutable() { + mService.handleUnlockUser(USER_0); + + // Create a non-pinned manifest shortcut, a pinned shortcut that was originally + // a manifest shortcut, as well as a dynamic shortcut. + + 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)); + + runWithCaller(LAUNCHER_1, USER_0, () -> { + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2"), HANDLE_USER_0); + }); + + 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)); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.addDynamicShortcuts(list(makeShortcutWithTitle("s1", "t1"))); + }); + + checkManifestShortcuts_immutable_verify(); + + // Note that even though the first argument is not immutable and only the second one + // is immutable, the first argument should not be executed either. + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.setDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms1"))); + }); + assertCannotUpdateImmutable(() -> { + mManager.setDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms2"))); + }); + }); + checkManifestShortcuts_immutable_verify(); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.addDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms1"))); + }); + assertCannotUpdateImmutable(() -> { + mManager.addDynamicShortcuts(list(makeShortcut("xx"), makeShortcut("ms2"))); + }); + }); + checkManifestShortcuts_immutable_verify(); + + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.updateShortcuts(list(makeShortcut("s1"), makeShortcut("ms1"))); + }); + assertCannotUpdateImmutable(() -> { + mManager.updateShortcuts(list(makeShortcut("s1"), makeShortcut("ms2"))); + }); + }); + checkManifestShortcuts_immutable_verify(); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.removeDynamicShortcuts(list("s1", "ms1")); + }); + assertCannotUpdateImmutable(() -> { + mManager.removeDynamicShortcuts(list("s2", "ms2")); + }); + }); + checkManifestShortcuts_immutable_verify(); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.disableShortcuts(list("s1", "ms1")); + }); + }); + checkManifestShortcuts_immutable_verify(); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertCannotUpdateImmutable(() -> { + mManager.enableShortcuts(list("s1", "ms2")); + }); + }); + checkManifestShortcuts_immutable_verify(); + } } 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 c23f9e65a1fa4..7fa8a800f9574 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 @@ -246,11 +246,22 @@ public class ShortcutManagerTestUtils { reset(o); } } + + public static List assertEmpty(List list) { + assertEquals(0, list.size()); + return list; + } + public static void assertExpectException(Class expectedExceptionType, String expectedExceptionMessageRegex, Runnable r) { assertExpectException("", expectedExceptionType, expectedExceptionMessageRegex, r); } + public static void assertCannotUpdateImmutable(Runnable r) { + assertExpectException( + IllegalArgumentException.class, "may not be manipulated via APIs", r); + } + public static void assertDynamicShortcutCountExceeded(Runnable r) { assertExpectException(IllegalArgumentException.class, "Max number of dynamic shortcuts exceeded", r); @@ -383,6 +394,46 @@ public class ShortcutManagerTestUtils { return actualShortcuts; } + public static List assertAllManifest( + List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isManifestShortcut()); + } + return actualShortcuts; + } + + public static List assertAllNotManifest( + List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertFalse("ID " + s.getId(), s.isManifestShortcut()); + } + return actualShortcuts; + } + + public static List assertAllDisabled( + List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), !s.isEnabled()); + } + return actualShortcuts; + } + + public static List assertAllEnabled( + List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isEnabled()); + } + return actualShortcuts; + } + + public static List assertAllImmutable( + List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isImmutable()); + } + return actualShortcuts; + } + public static List assertAllStringsResolved( List actualShortcuts) { for (ShortcutInfo s : actualShortcuts) { @@ -398,6 +449,7 @@ public class ShortcutManagerTestUtils { public static void assertPinnedOnly(ShortcutInfo si) { assertFalse(si.isDynamic()); + assertFalse(si.isManifestShortcut()); assertTrue(si.isPinned()); }