diff --git a/api/current.txt b/api/current.txt index 8fc9d133451fe..df31f2c468934 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10103,10 +10103,12 @@ package android.content.pm { ctor public LauncherApps.ShortcutQuery(); 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 setIntent(android.content.Intent); 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_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_MATCH_CHOOSER = 16; // 0x10 field public static final int FLAG_MATCH_DYNAMIC = 1; // 0x1 field public static final int FLAG_MATCH_MANIFEST = 8; // 0x8 field public static final int FLAG_MATCH_PINNED = 2; // 0x2 @@ -10645,6 +10647,9 @@ package android.content.pm { method public int describeContents(); method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); + method public android.content.ComponentName[] getChooserComponentNames(); + method public android.os.PersistableBundle getChooserExtras(); + method public android.content.IntentFilter[] getChooserIntentFilters(); method public java.lang.CharSequence getDisabledMessage(); method public android.os.PersistableBundle getExtras(); method public java.lang.String getId(); @@ -10657,6 +10662,7 @@ package android.content.pm { method public java.lang.CharSequence getShortLabel(); method public android.os.UserHandle getUserHandle(); method public boolean hasKeyFieldsOnly(); + method public boolean isChooser(); method public boolean isDeclaredInManifest(); method public boolean isDynamic(); method public boolean isEnabled(); @@ -10669,9 +10675,11 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context, java.lang.String); + method public android.content.pm.ShortcutInfo.Builder addChooserIntentFilter(android.content.IntentFilter, android.content.ComponentName); method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); + method public android.content.pm.ShortcutInfo.Builder setChooserExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); diff --git a/api/system-current.txt b/api/system-current.txt index f8cc875cda9fd..dcf62cdcc1623 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -10640,10 +10640,12 @@ package android.content.pm { ctor public LauncherApps.ShortcutQuery(); 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 setIntent(android.content.Intent); 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_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_MATCH_CHOOSER = 16; // 0x10 field public static final int FLAG_MATCH_DYNAMIC = 1; // 0x1 field public static final int FLAG_MATCH_MANIFEST = 8; // 0x8 field public static final int FLAG_MATCH_PINNED = 2; // 0x2 @@ -11267,6 +11269,9 @@ package android.content.pm { method public int describeContents(); method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); + method public android.content.ComponentName[] getChooserComponentNames(); + method public android.os.PersistableBundle getChooserExtras(); + method public android.content.IntentFilter[] getChooserIntentFilters(); method public java.lang.CharSequence getDisabledMessage(); method public android.os.PersistableBundle getExtras(); method public java.lang.String getId(); @@ -11279,6 +11284,7 @@ package android.content.pm { method public java.lang.CharSequence getShortLabel(); method public android.os.UserHandle getUserHandle(); method public boolean hasKeyFieldsOnly(); + method public boolean isChooser(); method public boolean isDeclaredInManifest(); method public boolean isDynamic(); method public boolean isEnabled(); @@ -11291,9 +11297,11 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context, java.lang.String); + method public android.content.pm.ShortcutInfo.Builder addChooserIntentFilter(android.content.IntentFilter, android.content.ComponentName); method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); + method public android.content.pm.ShortcutInfo.Builder setChooserExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); diff --git a/api/test-current.txt b/api/test-current.txt index 82f972ab016ff..d90b5f0c4b922 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -10135,10 +10135,12 @@ package android.content.pm { ctor public LauncherApps.ShortcutQuery(); 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 setIntent(android.content.Intent); 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_KEY_FIELDS_ONLY = 4; // 0x4 + field public static final int FLAG_MATCH_CHOOSER = 16; // 0x10 field public static final int FLAG_MATCH_DYNAMIC = 1; // 0x1 field public static final int FLAG_MATCH_MANIFEST = 8; // 0x8 field public static final int FLAG_MATCH_PINNED = 2; // 0x2 @@ -10681,6 +10683,9 @@ package android.content.pm { method public int describeContents(); method public android.content.ComponentName getActivity(); method public java.util.Set getCategories(); + method public android.content.ComponentName[] getChooserComponentNames(); + method public android.os.PersistableBundle getChooserExtras(); + method public android.content.IntentFilter[] getChooserIntentFilters(); method public java.lang.CharSequence getDisabledMessage(); method public android.os.PersistableBundle getExtras(); method public java.lang.String getId(); @@ -10693,6 +10698,7 @@ package android.content.pm { method public java.lang.CharSequence getShortLabel(); method public android.os.UserHandle getUserHandle(); method public boolean hasKeyFieldsOnly(); + method public boolean isChooser(); method public boolean isDeclaredInManifest(); method public boolean isDynamic(); method public boolean isEnabled(); @@ -10705,9 +10711,11 @@ package android.content.pm { public static class ShortcutInfo.Builder { ctor public ShortcutInfo.Builder(android.content.Context, java.lang.String); + method public android.content.pm.ShortcutInfo.Builder addChooserIntentFilter(android.content.IntentFilter, android.content.ComponentName); method public android.content.pm.ShortcutInfo build(); method public android.content.pm.ShortcutInfo.Builder setActivity(android.content.ComponentName); method public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set); + method public android.content.pm.ShortcutInfo.Builder setChooserExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setDisabledMessage(java.lang.CharSequence); method public android.content.pm.ShortcutInfo.Builder setExtras(android.os.PersistableBundle); method public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index c08bd1db83029..41311eb048371 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -55,7 +55,8 @@ interface ILauncherApps { String callingPackage, String packageName, int flags, in UserHandle user); ParceledListSlice getShortcuts(String callingPackage, long changedSince, String packageName, - in List shortcutIds, in ComponentName componentName, int flags, in UserHandle user); + in List shortcutIds, in ComponentName componentName, in Intent intent, int flags, + in UserHandle user); void pinShortcuts(String callingPackage, String packageName, in List shortcutIds, in UserHandle user); boolean startShortcut(String callingPackage, String packageName, String id, diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index d76d82492a61d..776492a140c3a 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -275,7 +275,18 @@ public class LauncherApps { @Deprecated public static final int FLAG_GET_MANIFEST = FLAG_MATCH_MANIFEST; - /** @hide */ + /** + * Include chooser shortcuts in the result. + * STOPSHIP TODO: Unless explicitly requesting chooser fields, we should strip out chooser + * relevant fields from the Shortcut. This should also be adequately documented. + */ + public static final int FLAG_MATCH_CHOOSER = 1 << 4; + + /** + * Does not retrieve CHOOSER only shortcuts. + * TODO: Add another flag for MATCH_ALL_PINNED + * @hide + */ public static final int FLAG_MATCH_ALL_KINDS = FLAG_GET_DYNAMIC | FLAG_GET_PINNED | FLAG_GET_MANIFEST; @@ -308,6 +319,7 @@ public class LauncherApps { FLAG_MATCH_DYNAMIC, FLAG_MATCH_PINNED, FLAG_MATCH_MANIFEST, + FLAG_MATCH_CHOOSER, FLAG_GET_KEY_FIELDS_ONLY, }) @Retention(RetentionPolicy.SOURCE) @@ -324,6 +336,9 @@ public class LauncherApps { @Nullable ComponentName mActivity; + @Nullable + Intent mIntent; + @QueryFlags int mQueryFlags; @@ -367,6 +382,14 @@ public class LauncherApps { return this; } + /** + * If non-null, returns only shortcuts with intent filters that match this intent. + */ + public ShortcutQuery setIntent(@Nullable Intent intent) { + mIntent = intent; + return this; + } + /** * Set query options. At least one of the {@code MATCH} flags should be set. Otherwise, * no shortcuts will be returned. @@ -681,7 +704,7 @@ public class LauncherApps { try { return mService.getShortcuts(mContext.getPackageName(), query.mChangedSince, query.mPackage, query.mShortcutIds, query.mActivity, - query.mQueryFlags, user) + query.mIntent, query.mQueryFlags, user) .getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index f1f268306bb24..d3d3c66b20c08 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -21,8 +21,10 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.TaskStackBuilder; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.LauncherApps.ShortcutQuery; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -38,10 +40,12 @@ import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.MemInfoReader; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -95,6 +99,14 @@ public final class ShortcutInfo implements Parcelable { public static final int FLAG_MASKABLE_BITMAP = 1 << 9; /** @hide */ + public static final int FLAG_CHOOSER = 1 << 10; + + /** + * TODO: Add FLAG_CHOOSER_INFO_OMITTED to reflect that chooser info was omitted in the Shortcut + * due to the context in which it was retrieved. + * TODO: Add a FLAG_LAUNCHABLE to reflect whether or not the Shortcut has a launchable intent + * @hide + */ @IntDef(flag = true, value = { FLAG_DYNAMIC, @@ -107,6 +119,7 @@ public final class ShortcutInfo implements Parcelable { FLAG_STRINGS_RESOLVED, FLAG_IMMUTABLE, FLAG_MASKABLE_BITMAP, + FLAG_CHOOSER, }) @Retention(RetentionPolicy.SOURCE) public @interface ShortcutFlags {} @@ -201,6 +214,24 @@ public final class ShortcutInfo implements Parcelable { @Nullable private PersistableBundle[] mIntentPersistableExtrases; + /** + * If used in a chooser, extras that should be added into the intent passed through. + */ + @Nullable + private PersistableBundle mChooserExtras; + + /** + * Intent filters to be used if the shortcut is to be used in a chooser context. + */ + @Nullable + private IntentFilter[] mChooserIntentFilters; + + /** + * Component names corresponding to the above intent filters. + */ + @Nullable + private ComponentName[] mChooserComponentNames; + private int mRank; /** @@ -250,6 +281,13 @@ public final class ShortcutInfo implements Parcelable { mDisabledMessageResId = b.mDisabledMessageResId; mCategories = cloneCategories(b.mCategories); mIntents = cloneIntents(b.mIntents); + if (b.mChooserIntentFilters != null) { + mChooserIntentFilters = b.mChooserIntentFilters.toArray(new IntentFilter[0]); + } + if (b.mChooserComponentNames != null) { + mChooserComponentNames = b.mChooserComponentNames.toArray(new ComponentName[0]); + } + mChooserExtras = b.mChooserExtras; fixUpIntentExtras(); mRank = b.mRank; mExtras = b.mExtras; @@ -330,8 +368,28 @@ public final class ShortcutInfo implements Parcelable { if (mTitle == null && mTitleResId == 0) { throw new IllegalArgumentException("Short label must be provided"); } - Preconditions.checkNotNull(mIntents, "Shortcut Intent must be provided"); - Preconditions.checkArgument(mIntents.length > 0, "Shortcut Intent must be provided"); + + // For a shortcut to be valid, there should either be an Intent, or a non-empty set of + // intent filters. + if (mIntents == null || mIntents.length == 0) { + Preconditions.checkNotNull(mChooserIntentFilters, + "Intent must be provided if not a chooser target"); + Preconditions.checkNotNull(mChooserComponentNames, + "Intent must be provided if not a chooser target"); + } + + // If ChooserIntentFilter are provided, they should match the length of the provided + // component names. + if (mChooserIntentFilters != null) { + if (mChooserComponentNames == null + || mChooserIntentFilters.length != mChooserComponentNames.length) { + throw new IllegalArgumentException("Inconsistent intent filters and " + + "component names given"); + } + if (mChooserIntentFilters.length == 0 || mChooserComponentNames.length == 0) { + throw new IllegalArgumentException("Empty intent filter and component names given"); + } + } } /** @@ -376,6 +434,10 @@ public final class ShortcutInfo implements Parcelable { mDisabledMessageResName = source.mDisabledMessageResName; mIconResName = source.mIconResName; } + // TODO: Omit these by default and add a new clone flag. + mChooserIntentFilters = source.mChooserIntentFilters; + mChooserComponentNames = source.mChooserComponentNames; + mChooserExtras = source.mChooserExtras; } else { // Set this bit. mFlags |= FLAG_KEY_FIELDS_ONLY; @@ -502,6 +564,25 @@ public final class ShortcutInfo implements Parcelable { return fullResourceName.substring(p1 + 1); } + /** + * Whether the shortcut has any intentFilter matching the passed in one. + * @hide + */ + @VisibleForTesting + public boolean hasMatchingFilter(ContentResolver resolver, Intent intent) { + if (mChooserIntentFilters == null) { + return false; + } + for (IntentFilter filter : mChooserIntentFilters) { + int match = filter.match(resolver, intent, false, TAG); + if (match > 0) { + return true; + } + } + return false; + } + + /** * Extract the entry name from a fully-donated resource name. * e.g. "com.android.app1:drawable/icon1" -> "icon1" @@ -685,6 +766,15 @@ public final class ShortcutInfo implements Parcelable { if (source.mExtras != null) { mExtras = source.mExtras; } + if (source.mChooserExtras != null) { + mChooserExtras = source.mChooserExtras; + } + if (source.mChooserIntentFilters != null) { + mChooserIntentFilters = source.mChooserIntentFilters; + } + if (source.mChooserComponentNames != null) { + mChooserComponentNames = source.mChooserComponentNames; + } } /** @@ -746,6 +836,12 @@ public final class ShortcutInfo implements Parcelable { private PersistableBundle mExtras; + private PersistableBundle mChooserExtras; + + private List mChooserIntentFilters; + + private List mChooserComponentNames; + /** * Old style constructor. * @hide @@ -1031,6 +1127,40 @@ public final class ShortcutInfo implements Parcelable { return this; } + /** + * Extras that can be added which will be added to the Intent used to launch the app if + * launched from a chooser context. + */ + @NonNull + public Builder setChooserExtras(@NonNull PersistableBundle extras) { + mChooserExtras = extras; + return this; + } + + /** + * IntentFilters and the components that should resolve a match for a given chooser target. + * If multiple matches are found, the component corresponding to the closest match will be + * used. + * + * @param filter IntendFilter that if matched will have the intent forwarded to the given + * component + * @param name The component that an intent that passes this filter will resolve to. + */ + public Builder addChooserIntentFilter(@NonNull IntentFilter filter, + @NonNull ComponentName name) { + Preconditions.checkNotNull(filter, "intent filter cannot be null"); + Preconditions.checkNotNull(name, "component name cannot be null"); + + if (mChooserIntentFilters == null || mChooserComponentNames == null) { + mChooserIntentFilters = new ArrayList<>(); + mChooserComponentNames = new ArrayList<>(); + } + + mChooserIntentFilters.add(filter); + mChooserComponentNames.add(name); + return this; + } + /** * Creates a {@link ShortcutInfo} instance. */ @@ -1231,6 +1361,30 @@ public final class ShortcutInfo implements Parcelable { return mIntentPersistableExtrases; } + /** + * Retrieve the extras that will be added in to any intent launched through the chooser. + */ + @NonNull + public PersistableBundle getChooserExtras() { + return mChooserExtras; + } + + /** + * Retrieve the list of intent filters for chooser targets. + */ + @NonNull + public IntentFilter[] getChooserIntentFilters() { + return mChooserIntentFilters; + } + + /** + * Retrieve the list of component names corresponding to the above intent filters. + */ + @NonNull + public ComponentName[] getChooserComponentNames() { + return mChooserComponentNames; + } + /** * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each * {@link #getActivity} for each of the two types of shortcuts (static and dynamic). @@ -1352,6 +1506,11 @@ public final class ShortcutInfo implements Parcelable { return hasFlags(FLAG_PINNED); } + /** Return whether a shortcut can be shown in the chooser. */ + public boolean isChooser() { + return hasFlags(FLAG_CHOOSER); + } + /** * Return whether a shortcut is static; that is, whether a shortcut is * published from AndroidManifest.xml. If {@code true}, the shortcut is @@ -1380,6 +1539,14 @@ public final class ShortcutInfo implements Parcelable { return isPinned() && !(isDynamic() || isManifestShortcut()); } + /** + * @return true if pinned but neither static nor dynamic. + * @hide + */ + public boolean isDynamicOrChooser() { + return hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_CHOOSER); + } + /** @hide */ public boolean isOriginallyFromManifest() { return hasFlags(FLAG_IMMUTABLE); @@ -1661,6 +1828,19 @@ public final class ShortcutInfo implements Parcelable { mCategories.add(source.readString().intern()); } } + + // We put a placeholder empty array in to keep the parcelable order, but can do away with + // them at this point if they're empty. + mChooserComponentNames = source.readParcelableArray(cl, ComponentName.class); + if (mChooserComponentNames.length == 0) { + mChooserComponentNames = null; + } + + mChooserIntentFilters = source.readParcelableArray(cl, IntentFilter.class); + if (mChooserIntentFilters.length == 0) { + mChooserIntentFilters = null; + } + mChooserExtras = source.readPersistableBundle(cl); } @Override @@ -1707,6 +1887,17 @@ public final class ShortcutInfo implements Parcelable { } else { dest.writeInt(0); } + if (mChooserComponentNames != null) { + dest.writeParcelableArray(mChooserComponentNames, flags); + } else { + dest.writeParcelableArray(new ComponentName[0], flags); + } + if (mChooserIntentFilters != null) { + dest.writeParcelableArray(mChooserIntentFilters, flags); + } else { + dest.writeParcelableArray(new IntentFilter[0], flags); + } + dest.writePersistableBundle(mChooserExtras); } public static final Creator CREATOR = diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index 87a6d4a5fa096..696fe81ba5a5e 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -44,8 +44,8 @@ public abstract class ShortcutServiceInternal { getShortcuts(int launcherUserId, @NonNull String callingPackage, long changedSince, @Nullable String packageName, @Nullable List shortcutIds, - @Nullable ComponentName componentName, @ShortcutQuery.QueryFlags int flags, - int userId); + @Nullable ComponentName componentName, @Nullable Intent intent, + @ShortcutQuery.QueryFlags int flags, int userId); public abstract boolean isPinnedByCaller(int launcherUserId, @NonNull String callingPackage, diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index df65659dface3..79301aa2997e8 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -23,15 +23,21 @@ import android.app.usage.UsageStatsManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.LabeledIntent; +import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; import android.database.DataSetObserver; import android.graphics.Color; import android.graphics.drawable.Drawable; @@ -356,6 +362,7 @@ public class ChooserActivity extends ResolverActivity { mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets)); } mChooserRowAdapter = new ChooserRowAdapter(mChooserListAdapter); + mChooserRowAdapter.updateRowScales(); mChooserRowAdapter.registerDataSetObserver(new OffsetDataSetObserver(adapterView)); adapterView.setAdapter(mChooserRowAdapter); if (listView != null) { @@ -842,7 +849,9 @@ public class ChooserActivity extends ResolverActivity { return false; } intent.setComponent(mChooserTarget.getComponentName()); - intent.putExtras(mChooserTarget.getIntentExtras()); + if (mChooserTarget.getIntentExtras() != null) { + intent.putExtras(mChooserTarget.getIntentExtras()); + } // Important: we will ignore the target security checks in ActivityManager // if and only if the ChooserTarget's target package is the same package @@ -925,6 +934,8 @@ public class ChooserActivity extends ResolverActivity { private static final int MAX_SERVICE_TARGETS = 8; private static final int MAX_TARGETS_PER_SERVICE = 4; + private boolean mAreChooserShortcutsRetrieved; + private final List mServiceTargets = new ArrayList<>(); private final List mCallerTargets = new ArrayList<>(); private boolean mShowServiceTargets; @@ -1016,6 +1027,20 @@ public class ChooserActivity extends ResolverActivity { if (mServiceTargets != null) { pruneServiceTargets(); } + + if (DEBUG) Log.d(TAG, "Adding pushed chooser targets"); + + if (!mAreChooserShortcutsRetrieved) { + LauncherApps launcherApps = getLauncherApps(); + LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery(); + query.setIntent(getTargetIntent()); + query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_CHOOSER); + List shortcuts = launcherApps.getShortcuts(query, UserHandle.SYSTEM); + if (DEBUG) Log.d(TAG, "Adding " + shortcuts.size() + " chooser shortcuts"); + addShortcuts(shortcuts); + mAreChooserShortcutsRetrieved = true; + } + if (DEBUG) Log.d(TAG, "List built querying services"); queryTargetServices(this); } @@ -1041,6 +1066,7 @@ public class ChooserActivity extends ResolverActivity { public int getServiceTargetCount() { if (!mShowServiceTargets) { + if (DEBUG) Log.d("TAG", "Hiding service targets"); return 0; } return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); @@ -1132,6 +1158,71 @@ public class ChooserActivity extends ResolverActivity { notifyDataSetChanged(); } + // TODO: Pushed targets need to be scored correctly + public void addShortcuts(List infos) { + for (ShortcutInfo info : infos) { + List newTargets = new ArrayList<>(); + final ComponentName cn = info.getActivity(); + ActivityInfo ai; + ResolveInfo ri = new ResolveInfo(); + if (cn != null) { + try { + ai = getPackageManager().getActivityInfo(cn, 0); + ri.activityInfo = ai; + UserManager userManager = + (UserManager) getSystemService(Context.USER_SERVICE); + ri.iconResourceId = ai.icon; + ri.labelRes = ai.labelRes; + ri.resolvePackageName = ai.packageName; + ri.activityInfo.applicationInfo = new ApplicationInfo( + ri.activityInfo.applicationInfo); + ri.activityInfo.applicationInfo = ai.applicationInfo; + ri.activityInfo.applicationInfo.uid = getUserId(); + } catch (PackageManager.NameNotFoundException ignored) { + if (DEBUG) Log.d(TAG, "Package not found, skipping this shortcut"); + continue; + } + } + + DisplayResolveInfo resolveInfo = new DisplayResolveInfo(getTargetIntent(), + ri, + info.getShortLabel(), + info.getLongLabel(), + getTargetIntent()); + + int bestMatch = 0; + ComponentName bestComponent = null; + for (int i = 0; i < info.getChooserIntentFilters().length; i++) { + int newMatch = info.getChooserIntentFilters()[i] + .match(getContentResolver(), getTargetIntent(), false, TAG); + if (DEBUG) Log.d(TAG, "A match was found with value: " + newMatch); + if (newMatch > bestMatch) { + bestMatch = newMatch; + bestComponent = info.getChooserComponentNames()[i]; + } + } + if (bestMatch == 0) { + Log.e(TAG, "Unexpectedly, no match was found for the provided chooser intent"); + return; + } + + Bundle extrasToAdd = + info.getChooserExtras() == null ? null: new Bundle(info.getChooserExtras()); + if (DEBUG) Log.d(TAG, "Adding service target " + info.getShortLabel()); + newTargets.add(new ChooserTarget( + info.getShortLabel(), + info.getIcon(), + 1, + bestComponent, + extrasToAdd)); + addServiceResults(resolveInfo, newTargets); + } + if (mChooserRowAdapter != null) { + mChooserRowAdapter.updateRowScales(); + } + setShowServiceTargets(true); + } + /** * Set to true to reveal all service targets at once. */ @@ -1246,37 +1337,7 @@ public class ChooserActivity extends ResolverActivity { @Override public void onChanged() { super.onChanged(); - final int rcount = getServiceTargetRowCount(); - if (mServiceTargetScale == null - || mServiceTargetScale.length != rcount) { - RowScale[] old = mServiceTargetScale; - int oldRCount = old != null ? old.length : 0; - mServiceTargetScale = new RowScale[rcount]; - if (old != null && rcount > 0) { - System.arraycopy(old, 0, mServiceTargetScale, 0, - Math.min(old.length, rcount)); - } - - for (int i = rcount; i < oldRCount; i++) { - old[i].cancelAnimation(); - } - - for (int i = oldRCount; i < rcount; i++) { - final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f) - .setInterpolator(mInterpolator); - mServiceTargetScale[i] = rs; - } - - // Start the animations in a separate loop. - // The process of starting animations will result in - // binding views to set up initial values, and we must - // have ALL of the new RowScale objects created above before - // we get started. - for (int i = oldRCount; i < rcount; i++) { - mServiceTargetScale[i].startAnimation(); - } - } - + updateRowScales(); notifyDataSetChanged(); } @@ -1293,6 +1354,40 @@ public class ChooserActivity extends ResolverActivity { }); } + void updateRowScales() { + final int rcount = getServiceTargetRowCount(); + if (mServiceTargetScale == null + || mServiceTargetScale.length != rcount) { + if (DEBUG) Log.d(TAG, "Row scales need adjusting to " + rcount + " rows."); + RowScale[] old = mServiceTargetScale; + int oldRCount = old != null ? old.length : 0; + mServiceTargetScale = new RowScale[rcount]; + if (old != null && rcount > 0) { + System.arraycopy(old, 0, mServiceTargetScale, 0, + Math.min(old.length, rcount)); + } + + for (int i = rcount; i < oldRCount; i++) { + old[i].cancelAnimation(); + } + + for (int i = oldRCount; i < rcount; i++) { + final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f) + .setInterpolator(mInterpolator); + mServiceTargetScale[i] = rs; + } + + // Start the animations in a separate loop. + // The process of starting animations will result in + // binding views to set up initial values, and we must + // have ALL of the new RowScale objects created above before + // we get started. + for (int i = oldRCount; i < rcount; i++) { + mServiceTargetScale[i].startAnimation(); + } + } + } + private float getRowScale(int rowPosition) { final int start = getCallerTargetRowCount(); final int end = start + getServiceTargetRowCount(); @@ -1563,6 +1658,10 @@ public class ChooserActivity extends ResolverActivity { } } + public LauncherApps getLauncherApps() { + return (LauncherApps) getSystemService(Context.LAUNCHER_APPS_SERVICE); + } + static class ServiceResultInfo { public final DisplayResolveInfo originalTarget; public final List resultTargets; diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 1080a9fcfe715..3dfecc6461695 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -16,6 +16,17 @@ package com.android.internal.app; +import android.app.Instrumentation; +import android.content.ComponentName; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.SystemClock; import com.android.internal.R; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; @@ -48,25 +59,31 @@ import static com.android.internal.app.ChooserWrapperActivity.sOverrides; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.isA; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Chooser activity instrumentation tests */ @RunWith(AndroidJUnit4.class) public class ChooserActivityTest { + private Instrumentation instrumentation; + + @Before + public void setUp() { + instrumentation = InstrumentationRegistry.getInstrumentation(); + sOverrides.reset(); + } + @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(ChooserWrapperActivity.class, false, false); - @Before - public void cleanOverrideData() { - sOverrides.reset(); - } - @Test public void customTitle() throws InterruptedException { Intent sendIntent = createSendImageIntent(); @@ -235,7 +252,6 @@ public class ChooserActivityTest { chosen[0] = targetInfo.getResolveInfo(); return true; }; - // Make a stable copy of the components as the original list may be modified List stableCopy = createResolvedComponentsForTestWithOtherProfile(2); @@ -324,6 +340,32 @@ public class ChooserActivityTest { assertThat(chosen[0], is(toChoose)); } + public void pushedChooserTarget() { + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + setChooserShortcuts(1); + List resolvedComponentInfos = createResolvedComponentsForTest(2); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + Intent sendIntent = createSendImageIntent(); + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + + waitForIdle(); + + onView(withText("short chooser label 0")) + .perform(click()); + waitForIdle(); + assertThat(chosen[0].resolvePackageName, + is(ResolverDataProvider.createActivityInfo(0).packageName)); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -371,4 +413,48 @@ public class ChooserActivityTest { } return packageStats.mChooserCounts.get(action).getOrDefault(annotation, 0); } + + private void setChooserShortcuts(int numShortcuts) { + ArrayList shortcuts = new ArrayList<>(); + for (int i = 0; i < numShortcuts; i++) { + shortcuts.add(makeShortcut(i)); + } + when(sOverrides.launcherApps.getShortcuts( + Mockito.isA(LauncherApps.ShortcutQuery.class), + Mockito.eq(UserHandle.SYSTEM))) + .thenReturn(shortcuts); + } + + private ShortcutInfo makeShortcut(int i) { + try { + IntentFilter filter = new IntentFilter(Intent.ACTION_SEND, "image/jpeg"); + + ComponentName component = new ComponentName("foo.bar", "foo.bar" + ".MainActivity"); + ShortcutInfo.Builder b = new ShortcutInfo.Builder(instrumentation.getContext(), "" + i) + .setActivity(component) + .setShortLabel("short chooser label " + i) + .setLongLabel("long chooser label" + i) + .setRank(i) + .setIntent(createSendImageIntent()) + .setIcon(Icon.createWithResource(instrumentation.getContext(), + android.R.drawable.ic_menu_add)) + .addChooserIntentFilter( + filter, + component); + + sOverrides.createPackageManager = pm -> { + final PackageManager spied = spy(pm); + try { + doAnswer(invocation -> ResolverDataProvider.createActivityInfo(i)) + .when(spied).getActivityInfo( + Mockito.isA(ComponentName.class), Mockito.anyInt()); + } catch (Exception e) { + // this is ok, just not found + e.printStackTrace(); + } + return spied; + }; + return b.build(); + } catch (Exception e) {return null;} + } } \ No newline at end of file diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index c446f3c79ea87..0dac2602740f0 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -18,6 +18,7 @@ package com.android.internal.app; import android.app.usage.UsageStatsManager; import android.content.Context; +import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import java.util.function.Function; @@ -74,6 +75,11 @@ public class ChooserWrapperActivity extends ChooserActivity { return super.getPackageManager(); } + @Override + public LauncherApps getLauncherApps() { + return sOverrides.launcherApps; + } + /** * We cannot directly mock the activity created since instrumentation creates it. *

@@ -82,6 +88,7 @@ public class ChooserWrapperActivity extends ChooserActivity { static class OverrideData { @SuppressWarnings("Since15") public Function createPackageManager; + public LauncherApps launcherApps; public Function onSafelyStartCallback; public ResolverListController resolverListController; public Boolean isVoiceInteraction; @@ -90,6 +97,7 @@ public class ChooserWrapperActivity extends ChooserActivity { onSafelyStartCallback = null; isVoiceInteraction = null; createPackageManager = null; + launcherApps = mock(LauncherApps.class); resolverListController = mock(ResolverListController.class); } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index c11131a2af7e6..71bfa648bd5f2 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -442,8 +442,8 @@ public class LauncherAppsService extends SystemService { @Override public ParceledListSlice getShortcuts(String callingPackage, long changedSince, - String packageName, List shortcutIds, ComponentName componentName, int flags, - UserHandle targetUser) { + String packageName, List shortcutIds, ComponentName componentName, Intent intent, + int flags, UserHandle targetUser) { ensureShortcutPermission(callingPackage); if (!canAccessProfile(callingPackage, targetUser, "Cannot get shortcuts") || !isUserEnabled(targetUser)) { @@ -454,11 +454,17 @@ public class LauncherAppsService extends SystemService { "To query by shortcut ID, package name must also be set"); } + if ((flags & ShortcutQuery.FLAG_MATCH_CHOOSER) == 0 + && intent != null) { + throw new IllegalArgumentException("Supplied an intent in the query, but did " + + "not request chooser targets"); + } + // TODO(b/29399275): Eclipse compiler requires explicit List cast below. return new ParceledListSlice<>((List) mShortcutServiceInternal.getShortcuts(getCallingUserId(), callingPackage, changedSince, packageName, shortcutIds, - componentName, flags, targetUser.getIdentifier())); + componentName, intent, flags, targetUser.getIdentifier())); } @Override @@ -906,6 +912,7 @@ public class LauncherAppsService extends SystemService { cookie.packageName, /* changedSince= */ 0, packageName, /* shortcutIds=*/ null, /* component= */ null, + /* intent= */ null, ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY | ShortcutQuery.FLAG_GET_ALL_KINDS , userId); diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 570259ba56156..ac98ab96f9cad 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.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; import android.content.res.Resources; @@ -31,6 +32,7 @@ import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import com.android.server.pm.ShortcutService.ShortcutOperation; @@ -68,6 +70,9 @@ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG_EXTRAS = "extras"; private static final String TAG_SHORTCUT = "shortcut"; private static final String TAG_CATEGORIES = "categories"; + private static final String TAG_CHOOSER_EXTRAS = "chooser-extras"; + private static final String TAG_CHOOSER_INTENT_FILTERS = "chooser-intent-filters"; + private static final String TAG_CHOOSER_COMPONENT_NAMES = "chooser-component-names"; private static final String ATTR_NAME = "name"; private static final String ATTR_CALL_COUNT = "call-count"; @@ -91,6 +96,7 @@ class ShortcutPackage extends ShortcutPackageItem { private static final String ATTR_ICON_RES_ID = "icon-res"; private static final String ATTR_ICON_RES_NAME = "icon-resname"; private static final String ATTR_BITMAP_PATH = "bitmap-path"; + private static final String ATTR_COMPONENT_NAMES = "component-names"; private static final String NAME_CATEGORIES = "categories"; @@ -200,7 +206,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (shortcut != null) { mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut); shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED - | ShortcutInfo.FLAG_MANIFEST); + | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CHOOSER); } return shortcut; } @@ -226,7 +232,7 @@ class ShortcutPackage extends ShortcutPackageItem { Preconditions.checkArgument(newShortcut.isEnabled(), "add/setDynamicShortcuts() cannot publish disabled shortcuts"); - newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + addCorrectDynamicFlags(newShortcut); final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId()); @@ -250,6 +256,17 @@ class ShortcutPackage extends ShortcutPackageItem { addShortcutInner(newShortcut); } + // TODO: Sample code & JavaDoc for ShortcutManager needs updating to reflect the fact that + // Chooser shortcuts are not always dynamic. + public void addCorrectDynamicFlags(@NonNull ShortcutInfo shortcut) { + if (shortcut.getIntent() != null) { + shortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC); + } + if (!ArrayUtils.isEmpty(shortcut.getChooserIntentFilters())) { + shortcut.addFlags(ShortcutInfo.FLAG_CHOOSER); + } + } + /** * Remove all shortcuts that aren't pinned nor dynamic. */ @@ -282,11 +299,11 @@ class ShortcutPackage extends ShortcutPackageItem { boolean changed = false; for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); - if (si.isDynamic()) { + if (si.isDynamic() || si.isChooser()) { changed = true; si.setTimestamp(now); - si.clearFlags(ShortcutInfo.FLAG_DYNAMIC); + si.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_CHOOSER); si.setRank(0); // It may still be pinned, so clear the rank. } } @@ -355,7 +372,8 @@ class ShortcutPackage extends ShortcutPackageItem { if (oldShortcut.isPinned()) { oldShortcut.setRank(0); - oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST); + oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_MANIFEST + | ShortcutInfo.FLAG_CHOOSER); if (disable) { oldShortcut.addFlags(ShortcutInfo.FLAG_DISABLED); } @@ -1116,8 +1134,8 @@ class ShortcutPackage extends ShortcutPackageItem { // Don't adjust ranks for manifest shortcuts. continue; } - // At this point, it must be dynamic. - if (!si.isDynamic()) { + // At this point, it must be dynamic or a chooser. + if (!si.isDynamicOrChooser()) { s.wtf("Non-dynamic shortcut found."); continue; } @@ -1294,7 +1312,7 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_FLAGS, si.getFlags() & ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES - | ShortcutInfo.FLAG_DYNAMIC)); + | ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_CHOOSER)); } else { // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored // as dynamic. @@ -1317,15 +1335,36 @@ class ShortcutPackage extends ShortcutPackageItem { } final Intent[] intentsNoExtras = si.getIntentsNoExtras(); final PersistableBundle[] intentsExtras = si.getIntentPersistableExtrases(); - final int numIntents = intentsNoExtras.length; - for (int i = 0; i < numIntents; i++) { - out.startTag(null, TAG_INTENT); - ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]); - ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]); - out.endTag(null, TAG_INTENT); + if (intentsNoExtras != null) { + final int numIntents = intentsNoExtras.length; + for (int i = 0; i < numIntents; i++) { + out.startTag(null, TAG_INTENT); + ShortcutService.writeAttr(out, ATTR_INTENT_NO_EXTRA, intentsNoExtras[i]); + ShortcutService.writeTagExtra(out, TAG_EXTRAS, intentsExtras[i]); + out.endTag(null, TAG_INTENT); + } + } + ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras()); + + ShortcutService.writeTagExtra(out, TAG_CHOOSER_EXTRAS, si.getChooserExtras()); + + final IntentFilter[] intentFilters = si.getChooserIntentFilters(); + if (intentFilters != null) { + for (int i = 0; i < intentFilters.length; i++) { + out.startTag(null, TAG_CHOOSER_INTENT_FILTERS); + intentFilters[i].writeToXml(out); + out.endTag(null, TAG_CHOOSER_INTENT_FILTERS); + } } - ShortcutService.writeTagExtra(out, TAG_EXTRAS, si.getExtras()); + final ComponentName[] componentNames = si.getChooserComponentNames(); + if (componentNames != null) { + for (int i = 0; i < componentNames.length; i++) { + out.startTag(null, TAG_CHOOSER_COMPONENT_NAMES); + ShortcutService.writeAttr(out, ATTR_COMPONENT_NAMES, componentNames[i]); + out.endTag(null, TAG_CHOOSER_COMPONENT_NAMES); + } + } out.endTag(null, TAG_SHORTCUT); } @@ -1398,6 +1437,9 @@ class ShortcutPackage extends ShortcutPackageItem { String iconResName; String bitmapPath; ArraySet categories = null; + PersistableBundle chooserExtras; + List chooserIntentFilters = new ArrayList<>(); + List chooserComponentNames = new ArrayList<>(); id = ShortcutService.parseStringAttribute(parser, ATTR_ID); activityComponent = ShortcutService.parseComponentNameAttribute(parser, @@ -1458,6 +1500,18 @@ class ShortcutPackage extends ShortcutPackageItem { } } continue; + case TAG_CHOOSER_EXTRAS: + chooserExtras = PersistableBundle.restoreFromXml(parser); + continue; + case TAG_CHOOSER_COMPONENT_NAMES: + chooserComponentNames.add(ShortcutService.parseComponentNameAttribute(parser, + ATTR_ACTIVITY)); + continue; + case TAG_CHOOSER_INTENT_FILTERS: + IntentFilter toAdd = new IntentFilter(); + toAdd.readFromXml(parser); + chooserIntentFilters.add(toAdd); + continue; } throw ShortcutService.throwForInvalidTag(depth, tag); } @@ -1551,10 +1605,10 @@ class ShortcutPackage extends ShortcutPackageItem { // Verify each shortcut's status. for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); - if (!(si.isDeclaredInManifest() || si.isDynamic() || si.isPinned())) { + if (!(si.isDeclaredInManifest() || si.isDynamicOrChooser() || si.isPinned())) { failed = true; Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() - + " is not manifest, dynamic or pinned."); + + " is not manifest, dynamic, chooser or pinned."); } if (si.isDeclaredInManifest() && si.isDynamic()) { failed = true; @@ -1596,6 +1650,11 @@ class ShortcutPackage extends ShortcutPackageItem { Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + " has a dummy target activity"); } + if (si.getIntent() == null && !si.isChooser()) { + failed = true; + Log.e(TAG_VERIFY, "Package " + getPackageName() + ": shortcut " + si.getId() + + " has a null intent, but is not a chooser"); + } } if (failed) { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 057e781e76dd4..74eb340b23e3c 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -27,6 +27,7 @@ import android.app.usage.UsageStatsManagerInternal; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -64,6 +65,7 @@ import android.os.FileUtils; import android.os.Handler; import android.os.LocaleList; import android.os.Looper; +import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.Process; @@ -1750,6 +1752,7 @@ public class ShortcutService extends IShortcutService.Stub { ps.clearAllImplicitRanks(); assignImplicitRanks(newShortcuts); + // TODO: Consider removing Chooser fields. If so, the FLAG_CHOOSER should be removed for (int i = 0; i < size; i++) { final ShortcutInfo source = newShortcuts.get(i); fixUpIncomingShortcutInfo(source, /* forUpdate= */ true); @@ -1789,6 +1792,13 @@ public class ShortcutService extends IShortcutService.Stub { if (replacingIcon || source.hasStringResources()) { fixUpShortcutResourceNamesAndValues(target); } + + // While updating, we keep the dynamic flag as it previously was, but refresh the + // chooser flag. + // TODO: If we support clearing Chooser fields, we should also remove the flag. + if (target.getChooserIntentFilters() != null) { + target.addFlags(ShortcutInfo.FLAG_CHOOSER); + } } // Lastly, adjust the ranks. @@ -1852,6 +1862,7 @@ public class ShortcutService extends IShortcutService.Stub { return true; } + // TODO: Ensure non-launchable shortcuts can not be pinned @Override public boolean requestPinShortcut(String packageName, ShortcutInfo shortcut, IntentSender resultIntent, int userId) { @@ -2007,7 +2018,7 @@ public class ShortcutService extends IShortcutService.Stub { return getShortcutsWithQueryLocked( packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR, - ShortcutInfo::isDynamic); + ShortcutInfo::isDynamicOrChooser); } } @@ -2200,6 +2211,14 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mLock) { throwIfUserLockedL(userId); + // For the chooser, we just check is the system is calling. + // STOPSHIP: We need to implement a new permission here rather than this terrible check. + // The packageName check is to try to distinguish between when an actual + // launcher is making the call, and when it's the system. + if (isCallerSystem() && packageName.equals("android")) { + return true; + } + final ShortcutUser user = getUserShortcutsLocked(userId); // Always trust the cached component. @@ -2372,7 +2391,7 @@ public class ShortcutService extends IShortcutService.Stub { public List getShortcuts(int launcherUserId, @NonNull String callingPackage, long changedSince, @Nullable String packageName, @Nullable List shortcutIds, - @Nullable ComponentName componentName, + @Nullable ComponentName componentName, @Nullable Intent intent, int queryFlags, int userId) { final ArrayList ret = new ArrayList<>(); @@ -2394,13 +2413,13 @@ public class ShortcutService extends IShortcutService.Stub { if (packageName != null) { getShortcutsInnerLocked(launcherUserId, callingPackage, packageName, shortcutIds, changedSince, - componentName, queryFlags, userId, ret, cloneFlag); + componentName, intent, queryFlags, userId, ret, cloneFlag); } else { final List shortcutIdsF = shortcutIds; getUserShortcutsLocked(userId).forAllPackages(p -> { getShortcutsInnerLocked(launcherUserId, callingPackage, p.getPackageName(), shortcutIdsF, changedSince, - componentName, queryFlags, userId, ret, cloneFlag); + componentName, intent, queryFlags, userId, ret, cloneFlag); }); } } @@ -2409,7 +2428,7 @@ public class ShortcutService extends IShortcutService.Stub { private void getShortcutsInnerLocked(int launcherUserId, @NonNull String callingPackage, @Nullable String packageName, @Nullable List shortcutIds, long changedSince, - @Nullable ComponentName componentName, int queryFlags, + @Nullable ComponentName componentName, Intent intent, int queryFlags, int userId, ArrayList ret, int cloneFlag) { final ArraySet ids = shortcutIds == null ? null : new ArraySet<>(shortcutIds); @@ -2434,6 +2453,15 @@ public class ShortcutService extends IShortcutService.Stub { return false; } } + if (intent != null + && !si.hasMatchingFilter(mContext.getContentResolver(), intent)) { + return false; + } + + if (((queryFlags & ShortcutQuery.FLAG_MATCH_CHOOSER) != 0) + && si.isChooser()) { + return true; + } if (((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0) && si.isDynamic()) { return true; diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 100338e7bfbf5..1b59d7231f980 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManager; @@ -1324,20 +1325,23 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected ShortcutInfo makeShortcut(String id) { return makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); } @Deprecated // Title was renamed to short label. protected ShortcutInfo makeShortcutWithTitle(String id, String title) { return makeShortcut( id, title, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); } protected ShortcutInfo makeShortcutWithShortLabel(String id, String shortLabel) { return makeShortcut( id, shortLabel, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); } /** @@ -1346,7 +1350,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected ShortcutInfo makeShortcutWithTimestamp(String id, long timestamp) { final ShortcutInfo s = makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); s.setTimestamp(timestamp); return s; } @@ -1358,7 +1363,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { ComponentName activity) { final ShortcutInfo s = makeShortcut( id, "Title-" + id, activity, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); s.setTimestamp(timestamp); return s; } @@ -1369,7 +1375,27 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected ShortcutInfo makeShortcutWithIcon(String id, Icon icon) { return makeShortcut( id, "Title-" + id, /* activity =*/ null, icon, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); + } + + protected ShortcutInfo makeChooserShortcut(String id, int i, boolean includeIntent) { + List filters = new ArrayList<>(); + List componentNames = new ArrayList<>(); + for(int j = 0; j < i; j++) { + final IntentFilter filter = new IntentFilter(); + filter.addAction("view"); + filters.add(filter); + + componentNames.add(new ComponentName("xxxx", "yy" + i)); + } + Intent intent = null; + if (includeIntent) { + intent = makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class); + } + return makeShortcut( + id, "Title-" + id, /* activity =*/ null, /* icon */ null, + intent, /* rank =*/ 0, filters, componentNames); } protected ShortcutInfo makePackageShortcut(String packageName, String id) { @@ -1378,7 +1404,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { setCaller(packageName); ShortcutInfo s = makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); setCaller(origCaller); // restore the caller return s; @@ -1402,39 +1429,52 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected ShortcutInfo makeShortcutWithActivity(String id, ComponentName activity) { return makeShortcut( id, "Title-" + id, activity, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilters =*/ null, /* chooserComponentNames =*/ null); } protected ShortcutInfo makeShortcutWithIntent(String id, Intent intent) { return makeShortcut( id, "Title-" + id, /* activity =*/ null, /* icon =*/ null, - intent, /* rank =*/ 0); + intent, /* rank =*/ 0, /* chooserFilters =*/ null, + /* chooserComponentNames =*/ null); + } protected ShortcutInfo makeShortcutWithActivityAndTitle(String id, ComponentName activity, String title) { return makeShortcut( id, title, activity, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0, + /* chooserFilters =*/ null, /* chooserComponentNames =*/ null); } protected ShortcutInfo makeShortcutWithActivityAndRank(String id, ComponentName activity, int rank) { return makeShortcut( id, "Title-" + id, activity, /* icon =*/ null, - makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), rank); + makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), rank, + /* chooserFilters =*/ null, /* chooserComponentNames =*/ null); } /** * Make a shortcut with details. */ protected ShortcutInfo makeShortcut(String id, String title, ComponentName activity, - Icon icon, Intent intent, int rank) { + Icon icon, Intent intent, int rank, @Nullable List chooserFilters, + @Nullable List chooserComponentNames) { final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext, id) .setActivity(new ComponentName(mClientContext.getPackageName(), "main")) .setShortLabel(title) - .setRank(rank) - .setIntent(intent); + .setRank(rank); + if (intent != null) { + b.setIntent(intent); + } + if (chooserFilters != null) { + for (int i = 0; i < chooserFilters.size(); i++) { + b.addChooserIntentFilter(chooserFilters.get(i), chooserComponentNames.get(i)); + } + } if (icon != null) { b.setIcon(icon); } diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index e4d92ba7a38c8..94ff07fb9d718 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -15,6 +15,7 @@ */ package com.android.server.pm; +import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertAllChooser; 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; @@ -256,7 +257,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { icon1, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo si2 = makeShortcut( "shortcut2", @@ -264,14 +267,18 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* activity */ null, icon2, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 12); + /* weight */ 12, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo si3 = makeShortcut( "shortcut3", "Title 3", /* activity */ null, icon3, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 13); + /* weight */ 13, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(si1, si2, si3))); assertShortcutIds(assertAllNotKeyFieldsOnly( @@ -982,8 +989,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { makeShortcut("s2"), makeShortcut("s3"), makeShortcut("s4"), - makeShortcut("s5") - ))); + makeShortcut("s5"), + makeChooserShortcut("s6", 2, true), + makeChooserShortcut("s7", 2, true), + makeChooserShortcut("s8", 1, true)))); }); runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -991,11 +1000,13 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { makeShortcut("s2"), makeShortcut("s3"), makeShortcut("s4"), - makeShortcut("s5") - ))); + makeShortcut("s5"), + makeChooserShortcut("s6", 2, true), + makeChooserShortcut("s7", 2, true), + makeChooserShortcut("s8", 1, true)))); }); runWithCaller(LAUNCHER_1, UserHandle.USER_SYSTEM, () -> { - mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2", "s3"), + mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s2", "s3", "s6"), getCallingUser()); mLauncherApps.pinShortcuts(CALLING_PACKAGE_2, list("s4", "s5"), getCallingUser()); @@ -1008,19 +1019,20 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mManager.removeDynamicShortcuts(list("s1")); mManager.removeDynamicShortcuts(list("s3")); mManager.removeDynamicShortcuts(list("s5")); + mManager.removeDynamicShortcuts(list("s7")); }); runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { assertShortcutIds(assertAllDynamic( mManager.getDynamicShortcuts()), - "s3", "s4", "s5"); + "s3", "s4", "s5", "s6", "s7", "s8"); assertShortcutIds(assertAllPinned( mManager.getPinnedShortcuts()), - "s2", "s3"); + "s2", "s3", "s6"); }); runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> { assertShortcutIds(assertAllDynamic( mManager.getDynamicShortcuts()), - "s2", "s4"); + "s2", "s4", "s6", "s8"); assertShortcutIds(assertAllPinned( mManager.getPinnedShortcuts()), "s4", "s5"); @@ -1057,10 +1069,10 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { assertShortcutIds(assertAllDynamic( mManager.getDynamicShortcuts()), - "s3", "s4", "s5"); + "s3", "s4", "s5", "s6", "s7", "s8"); assertShortcutIds(assertAllPinned( mManager.getPinnedShortcuts()), - "s2", "s3"); + "s2", "s3", "s6"); ShortcutInfo s = getCallerShortcut("s2"); assertTrue(s.hasIconResource()); @@ -1076,7 +1088,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> { assertShortcutIds(assertAllDynamic( mManager.getDynamicShortcuts()), - "s2", "s4"); + "s2", "s4", "s6", "s8"); assertShortcutIds(assertAllPinned( mManager.getPinnedShortcuts()), "s4", "s5"); @@ -1173,7 +1185,46 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - // === Test for launcher side APIs === + public void testUpdateShortcuts_chooser() { + runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { + assertTrue(mManager.setDynamicShortcuts(list( + makeShortcut("s1"), + makeChooserShortcut("s2", 2, false), + makeChooserShortcut("s3", 2, false) + ))); + + assertFalse(getCallerShortcut("s1").isChooser()); + assertTrue(getCallerShortcut("s2").isChooser()); + assertTrue(getCallerShortcut("s3").isChooser()); + + ShortcutInfo s = getCallerShortcut("s1"); + assertNull(s.getChooserIntentFilters()); + assertNull(s.getChooserComponentNames()); + + assertTrue(getCallerShortcut("s1").isDynamic()); + assertFalse(getCallerShortcut("s2").isDynamic()); + assertFalse(getCallerShortcut("s3").isDynamic()); + + + // Replace 2 with a chooser shortcut + mManager.updateShortcuts(list(makeChooserShortcut("s1", 2, true))); + + s = getCallerShortcut("s1"); + assertEquals(2, s.getChooserIntentFilters().length); + assertEquals(2, s.getChooserComponentNames().length); + + assertShortcutIds(assertAllChooser( + mManager.getDynamicShortcuts()), + "s1", "s2", "s3"); + + assertTrue(getCallerShortcut("s1").isDynamic()); + assertFalse(getCallerShortcut("s2").isDynamic()); + assertFalse(getCallerShortcut("s3").isDynamic()); + }); + } + + + // === Test for launcher side APIs === public void testGetShortcuts() { @@ -1484,15 +1535,17 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* icon =*/ null, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo s1_2 = makeShortcut( - "s2", - "Title 2", + "s2", "Title 2", /* activity */ null, /* icon =*/ null, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 12); + /* weight */ 12, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(s1_1, s1_2))); dumpsysOnLogcat(); @@ -1505,7 +1558,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* icon =*/ null, makeIntent(Intent.ACTION_ANSWER, ShortcutActivity2.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(s2_1))); dumpsysOnLogcat(); @@ -2674,10 +2729,12 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { final ShortcutInfo s1_2 = makeShortcut( "s2", "Title 2", - /* activity */ null, - /* icon =*/ null, + /* activity */ null, + /* icon =*/ null, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* rank */ 12); + /* rank */ 12, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo s1_3 = makeShortcut("s3"); @@ -2692,7 +2749,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* icon =*/ null, makeIntent(Intent.ACTION_ANSWER, ShortcutActivity.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(s2_1))); }); @@ -3110,7 +3169,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { icon1, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo si2 = makeShortcut( "s2", @@ -3118,7 +3179,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* activity */ null, icon2, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 12); + /* weight */ 12, + /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(si1, si2))); @@ -3136,8 +3199,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { makeComponent(ShortcutActivity.class), icon1, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, - "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + "key1", "val1", "nest", makeBundle("key", 123)), /* weight */ 10, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null); final ShortcutInfo si2 = makeShortcut( "s2", @@ -3145,7 +3208,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* activity */ null, icon2, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 12); + /* weight */ 12, /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(si1, si2))); @@ -3167,7 +3231,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { icon1, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class, "key1", "val1", "nest", makeBundle("key", 123)), - /* weight */ 10); + /* weight */ 10, /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); final ShortcutInfo si2 = makeShortcut( "s2", @@ -3175,7 +3240,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /* activity */ null, icon2, makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class), - /* weight */ 12); + /* weight */ 12, /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null); assertTrue(mManager.setDynamicShortcuts(list(si1, si2))); @@ -6800,10 +6866,12 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mManager.setDynamicShortcuts(list( makeShortcut("ms1", "title1", new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), - /* icon */ null, new Intent("action1"), /* rank */ 0), + /* icon */ null, new Intent("action1"), /* rank */ 0, + /* chooserFilter=*/ null, /* chooserComponentNames=*/ null), makeShortcut("ms2", "title2", new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), - /* icon */ null, new Intent("action1"), /* rank */ 0))); + /* icon */ null, new Intent("action1"), /* rank */ 0, /* chooserFilter=*/ null, + /* chooserComponentNames=*/ null))); }); runWithCaller(LAUNCHER_1, USER_0, () -> { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index 28ec4fd27ccc6..c54fa02b68ef1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -34,6 +34,7 @@ import android.Manifest.permission; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.graphics.BitmapFactory; @@ -91,11 +92,6 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { "id cannot be empty", () -> new ShortcutInfo.Builder(getTestContext(), "")); - assertExpectException( - RuntimeException.class, - "intents cannot contain null", - () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(null)); - assertExpectException( RuntimeException.class, "action must be set", @@ -142,6 +138,19 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { "disabledMessage cannot be empty", () -> new ShortcutInfo.Builder(getTestContext(), "id").setDisabledMessage("")); + + assertExpectException( + RuntimeException.class, + "component name cannot be null", + () -> new ShortcutInfo.Builder(getTestContext(), "id") + .addChooserIntentFilter(new IntentFilter(Intent.ACTION_SEND), null)); + + assertExpectException( + RuntimeException.class, + "intent filter cannot be null", + () -> new ShortcutInfo.Builder(getTestContext(), "id") + .addChooserIntentFilter(null, new ComponentName("xxx", "s"))); + assertExpectException(NullPointerException.class, "action must be set", () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent())); @@ -240,6 +249,10 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { PersistableBundle pb = new PersistableBundle(); pb.putInt("k", 1); + IntentFilter chooserFilter = new IntentFilter(); + chooserFilter.addAction(Intent.ACTION_VIEW); + PersistableBundle pb2 = new PersistableBundle(); + pb2.putInt("l", 1); si = new ShortcutInfo.Builder(getTestContext()) .setId("id") @@ -252,6 +265,8 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz")) .setRank(123) .setExtras(pb) + .addChooserIntentFilter(chooserFilter, new ComponentName("a", "b")) + .setChooserExtras(pb2) .build(); si.addFlags(ShortcutInfo.FLAG_PINNED); si.setBitmapPath("abc"); @@ -282,6 +297,12 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(null, si.getTextResName()); assertEquals(0, si.getDisabledMessageResourceId()); assertEquals(null, si.getDisabledMessageResName()); + + assertEquals(1, si.getChooserIntentFilters().length); + assertEquals(Intent.ACTION_VIEW, si.getChooserIntentFilters()[0].getAction(0)); + assertEquals(1, si.getChooserComponentNames().length); + assertEquals(new ComponentName("a", "b"), si.getChooserComponentNames()[0]); + assertEquals(1, si.getChooserExtras().getInt("l")); } public void testShortcutInfoParcel_resId() { @@ -290,6 +311,10 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { PersistableBundle pb = new PersistableBundle(); pb.putInt("k", 1); + IntentFilter chooserFilter = new IntentFilter(); + chooserFilter.addAction(Intent.ACTION_VIEW); + PersistableBundle pb2 = new PersistableBundle(); + pb2.putInt("l", 1); si = new ShortcutInfo.Builder(getTestContext()) .setId("id") @@ -302,6 +327,8 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setCategories(set(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION, "xyz")) .setRank(123) .setExtras(pb) + .addChooserIntentFilter(chooserFilter, new ComponentName("a", "b")) + .setChooserExtras(pb2) .build(); si.addFlags(ShortcutInfo.FLAG_PINNED); si.setBitmapPath("abc"); @@ -338,6 +365,11 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { PersistableBundle pb = new PersistableBundle(); pb.putInt("k", 1); + IntentFilter chooserFilter = new IntentFilter(); + chooserFilter.addAction(Intent.ACTION_VIEW); + PersistableBundle pb2 = new PersistableBundle(); + pb2.putInt("l", 1); + ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") .setActivity(new ComponentName("a", "b")) @@ -349,6 +381,8 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) .setRank(123) .setExtras(pb) + .addChooserIntentFilter(chooserFilter, new ComponentName("a", "b")) + .setChooserExtras(pb2) .build(); sorig.addFlags(ShortcutInfo.FLAG_PINNED); sorig.setBitmapPath("abc"); @@ -378,6 +412,12 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(456, si.getIconResourceId()); assertEquals("string/r456", si.getIconResName()); + assertEquals(1, si.getChooserIntentFilters().length); + assertEquals(Intent.ACTION_VIEW, si.getChooserIntentFilters()[0].getAction(0)); + assertEquals(1, si.getChooserComponentNames().length); + assertEquals(new ComponentName("a", "b"), si.getChooserComponentNames()[0]); + assertEquals(1, si.getChooserExtras().getInt("l")); + si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR); assertEquals(mClientContext.getPackageName(), si.getPackage()); @@ -445,6 +485,10 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { PersistableBundle pb = new PersistableBundle(); pb.putInt("k", 1); + IntentFilter chooserFilter = new IntentFilter(); + chooserFilter.addAction(Intent.ACTION_VIEW); + PersistableBundle pb2 = new PersistableBundle(); + pb2.putInt("l", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(mClientContext) .setId("id") .setActivity(new ComponentName("a", "b")) @@ -456,6 +500,8 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { .setIntent(makeIntent("action", ShortcutActivity.class, "key", "val")) .setRank(123) .setExtras(pb) + .addChooserIntentFilter(chooserFilter, new ComponentName("a", "b")) + .setChooserExtras(pb2) .build(); sorig.addFlags(ShortcutInfo.FLAG_PINNED); sorig.setBitmapPath("abc"); @@ -488,6 +534,12 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(456, si.getIconResourceId()); assertEquals("string/r456", si.getIconResName()); + assertEquals(1, si.getChooserIntentFilters().length); + assertEquals(Intent.ACTION_VIEW, si.getChooserIntentFilters()[0].getAction(0)); + assertEquals(1, si.getChooserComponentNames().length); + assertEquals(new ComponentName("a", "b"), si.getChooserComponentNames()[0]); + assertEquals(1, si.getChooserExtras().getInt("l")); + si = sorig.clone(ShortcutInfo.CLONE_REMOVE_FOR_CREATOR); assertEquals(mClientContext.getPackageName(), si.getPackage()); @@ -603,6 +655,10 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { public void testShortcutInfoCopyNonNullFieldsFrom() throws InterruptedException { PersistableBundle pb = new PersistableBundle(); pb.putInt("k", 1); + IntentFilter chooserFilter = new IntentFilter(); + chooserFilter.addAction(Intent.ACTION_VIEW); + PersistableBundle pb2 = new PersistableBundle(); + pb2.putInt("l", 1); ShortcutInfo sorig = new ShortcutInfo.Builder(getTestContext()) .setId("id") .setActivity(new ComponentName("a", "b")) @@ -714,12 +770,12 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { assertEquals(999, si.getRank()); - PersistableBundle pb2 = new PersistableBundle(); - pb2.putInt("x", 99); + PersistableBundle pb3 = new PersistableBundle(); + pb3.putInt("x", 99); si = sorig.clone(/* flags=*/ 0); si.copyNonNullFieldsFrom(new ShortcutInfo.Builder(getTestContext()).setId("id") - .setExtras(pb2).build()); + .setExtras(pb3).build()); assertEquals("text", si.getText()); assertEquals(99, si.getExtras().getInt("x")); } 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 ea45bd17b2165..fd335c3041fbf 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 @@ -508,6 +508,13 @@ public class ShortcutManagerTestUtils { return actualShortcuts; } + public static List assertAllChooser(List actualShortcuts) { + for (ShortcutInfo s : actualShortcuts) { + assertTrue("ID " + s.getId(), s.isChooser()); + } + return actualShortcuts; + } + public static List assertAllPinned(List actualShortcuts) { for (ShortcutInfo s : actualShortcuts) { assertTrue("ID " + s.getId(), s.isPinned());