From 52c39215db2a0e1d339733e06284a2ba81e4c794 Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Thu, 7 Apr 2016 15:14:18 -0700 Subject: [PATCH] Chooser filtering and caller direct share targets Let apps invoking the system chooser specify components to filter out such as themselves; this will prevent duplicate nonsensical UX where it doesn't make sense for an app to share to itself. Similarly, let apps provide their own Direct Share targets for when they do want to let users share via their own internal services in the same UI. These options can be used together. Also fix a bug where a lingering binder reference from a remote ChooserTargetService that hasn't been GC'd in the remote process could keep an active reference to a ChooserActivity instance. Bug 28073484 Change-Id: Ib613b1153b49dfedf79574b1af7c45379eceec24 --- api/current.txt | 4 +- api/system-current.txt | 4 +- api/test-current.txt | 4 +- core/java/android/content/Intent.java | 25 ++++++ .../android/internal/app/ChooserActivity.java | 79 ++++++++++++++++--- .../internal/app/ResolverActivity.java | 22 +++++- 6 files changed, 122 insertions(+), 16 deletions(-) diff --git a/api/current.txt b/api/current.txt index 1c4f85bc7abe5..8297988a83b21 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8685,6 +8685,7 @@ package android.content { field public static final java.lang.String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list"; field public static final java.lang.String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list"; field public static final java.lang.String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER"; + field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED"; @@ -8696,6 +8697,7 @@ package android.content { field public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; // 0x0 field public static final java.lang.String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP"; field public static final java.lang.String EXTRA_EMAIL = "android.intent.extra.EMAIL"; + field public static final java.lang.String EXTRA_EXCLUDE_COMPONENTS = "android.intent.extra.EXCLUDE_COMPONENTS"; field public static final java.lang.String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; field public static final java.lang.String EXTRA_INDEX = "android.intent.extra.INDEX"; field public static final java.lang.String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; @@ -51977,7 +51979,6 @@ package java.lang.reflect { public final class Method extends java.lang.reflect.AccessibleObject implements java.lang.reflect.GenericDeclaration java.lang.reflect.Member { method public boolean equals(java.lang.Object); method public A getAnnotation(java.lang.Class); - method public java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public java.lang.Class getDeclaringClass(); method public java.lang.Object getDefaultValue(); method public java.lang.Class[] getExceptionTypes(); @@ -51991,7 +51992,6 @@ package java.lang.reflect { method public java.lang.Class getReturnType(); method public java.lang.reflect.TypeVariable[] getTypeParameters(); method public java.lang.Object invoke(java.lang.Object, java.lang.Object...) throws java.lang.IllegalAccessException, java.lang.IllegalArgumentException, java.lang.reflect.InvocationTargetException; - method public boolean isAnnotationPresent(java.lang.Class); method public boolean isBridge(); method public boolean isDefault(); method public boolean isSynthetic(); diff --git a/api/system-current.txt b/api/system-current.txt index 039b9adc24a23..19d4d3217d39e 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9004,6 +9004,7 @@ package android.content { field public static final java.lang.String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list"; field public static final java.lang.String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list"; field public static final java.lang.String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER"; + field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED"; @@ -9017,6 +9018,7 @@ package android.content { field public static final java.lang.String EXTRA_EMAIL = "android.intent.extra.EMAIL"; field public static final java.lang.String EXTRA_EPHEMERAL_FAILURE = "android.intent.extra.EPHEMERAL_FAILURE"; field public static final java.lang.String EXTRA_EPHEMERAL_SUCCESS = "android.intent.extra.EPHEMERAL_SUCCESS"; + field public static final java.lang.String EXTRA_EXCLUDE_COMPONENTS = "android.intent.extra.EXCLUDE_COMPONENTS"; field public static final java.lang.String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; field public static final java.lang.String EXTRA_INDEX = "android.intent.extra.INDEX"; field public static final java.lang.String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; @@ -55074,7 +55076,6 @@ package java.lang.reflect { public final class Method extends java.lang.reflect.AccessibleObject implements java.lang.reflect.GenericDeclaration java.lang.reflect.Member { method public boolean equals(java.lang.Object); method public A getAnnotation(java.lang.Class); - method public java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public java.lang.Class getDeclaringClass(); method public java.lang.Object getDefaultValue(); method public java.lang.Class[] getExceptionTypes(); @@ -55088,7 +55089,6 @@ package java.lang.reflect { method public java.lang.Class getReturnType(); method public java.lang.reflect.TypeVariable[] getTypeParameters(); method public java.lang.Object invoke(java.lang.Object, java.lang.Object...) throws java.lang.IllegalAccessException, java.lang.IllegalArgumentException, java.lang.reflect.InvocationTargetException; - method public boolean isAnnotationPresent(java.lang.Class); method public boolean isBridge(); method public boolean isDefault(); method public boolean isSynthetic(); diff --git a/api/test-current.txt b/api/test-current.txt index 3febda193f3dd..5c975cf73a499 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8692,6 +8692,7 @@ package android.content { field public static final java.lang.String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list"; field public static final java.lang.String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list"; field public static final java.lang.String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER"; + field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT"; field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED"; @@ -8703,6 +8704,7 @@ package android.content { field public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; // 0x0 field public static final java.lang.String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP"; field public static final java.lang.String EXTRA_EMAIL = "android.intent.extra.EMAIL"; + field public static final java.lang.String EXTRA_EXCLUDE_COMPONENTS = "android.intent.extra.EXCLUDE_COMPONENTS"; field public static final java.lang.String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; field public static final java.lang.String EXTRA_INDEX = "android.intent.extra.INDEX"; field public static final java.lang.String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; @@ -52053,7 +52055,6 @@ package java.lang.reflect { public final class Method extends java.lang.reflect.AccessibleObject implements java.lang.reflect.GenericDeclaration java.lang.reflect.Member { method public boolean equals(java.lang.Object); method public A getAnnotation(java.lang.Class); - method public java.lang.annotation.Annotation[] getDeclaredAnnotations(); method public java.lang.Class getDeclaringClass(); method public java.lang.Object getDefaultValue(); method public java.lang.Class[] getExceptionTypes(); @@ -52067,7 +52068,6 @@ package java.lang.reflect { method public java.lang.Class getReturnType(); method public java.lang.reflect.TypeVariable[] getTypeParameters(); method public java.lang.Object invoke(java.lang.Object, java.lang.Object...) throws java.lang.IllegalAccessException, java.lang.IllegalArgumentException, java.lang.reflect.InvocationTargetException; - method public boolean isAnnotationPresent(java.lang.Class); method public boolean isBridge(); method public boolean isDefault(); method public boolean isSynthetic(); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 30f2c948df966..207b70af3f603 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -3729,6 +3729,31 @@ public class Intent implements Parcelable, Cloneable { */ public static final String EXTRA_ALTERNATE_INTENTS = "android.intent.extra.ALTERNATE_INTENTS"; + /** + * A {@link ComponentName ComponentName[]} describing components that should be filtered out + * and omitted from a list of components presented to the user. + * + *

When used with {@link #ACTION_CHOOSER}, the chooser will omit any of the components + * in this array if it otherwise would have shown them. Useful for omitting specific targets + * from your own package or other apps from your organization if the idea of sending to those + * targets would be redundant with other app functionality. Filtered components will not + * be able to present targets from an associated ChooserTargetService.

+ */ + public static final String EXTRA_EXCLUDE_COMPONENTS + = "android.intent.extra.EXCLUDE_COMPONENTS"; + + /** + * A {@link android.service.chooser.ChooserTarget ChooserTarget[]} for {@link #ACTION_CHOOSER} + * describing additional high-priority deep-link targets for the chooser to present to the user. + * + *

Targets provided in this way will be presented inline with all other targets provided + * by services from other apps. They will be prioritized before other service targets, but + * after those targets provided by sources that the user has manually pinned to the front.

+ * + * @see #ACTION_CHOOSER + */ + public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS"; + /** * An {@link IntentSender} for an Activity that will be invoked when the user makes a selection * from the chooser activity presented by {@link #ACTION_CHOOSER}. diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index a4e489c5ccbff..ed6ab5674d719 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -41,12 +41,12 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Parcelable; +import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; -import android.provider.DocumentsContract; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; import android.service.chooser.IChooserTargetResult; @@ -70,6 +70,7 @@ import android.widget.ListView; import com.android.internal.R; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsProto.MetricsEvent; +import com.google.android.collect.Lists; import java.io.File; import java.util.ArrayList; @@ -89,6 +90,7 @@ public class ChooserActivity extends ResolverActivity { private IntentSender mChosenComponentSender; private IntentSender mRefinementIntentSender; private RefinementResultReceiver mRefinementResultReceiver; + private ChooserTarget[] mCallerChooserTargets; private Intent mReferrerFillInIntent; @@ -97,6 +99,7 @@ public class ChooserActivity extends ResolverActivity { private SharedPreferences mPinnedSharedPrefs; private static final float PINNED_TARGET_SCORE_BOOST = 1000.f; + private static final float CALLER_TARGET_SCORE_BOOST = 900.f; private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; @@ -219,6 +222,34 @@ public class ChooserActivity extends ResolverActivity { Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); setSafeForwardingMode(true); + pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS); + if (pa != null) { + ComponentName[] names = new ComponentName[pa.length]; + for (int i = 0; i < pa.length; i++) { + if (!(pa[i] instanceof ComponentName)) { + Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]); + names = null; + break; + } + names[i] = (ComponentName) pa[i]; + } + setFilteredComponents(names); + } + + pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS); + if (pa != null) { + ChooserTarget[] targets = new ChooserTarget[pa.length]; + for (int i = 0; i < pa.length; i++) { + if (!(pa[i] instanceof ChooserTarget)) { + Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]); + targets = null; + break; + } + targets[i] = (ChooserTarget) pa[i]; + } + mCallerChooserTargets = targets; + } + mPinnedSharedPrefs = getPinnedSharedPrefs(this); super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, null, false); @@ -292,6 +323,9 @@ public class ChooserActivity extends ResolverActivity { boolean alwaysUseOption) { final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; mChooserListAdapter = (ChooserListAdapter) adapter; + if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { + mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets)); + } mChooserRowAdapter = new ChooserRowAdapter(mChooserListAdapter); mChooserRowAdapter.registerDataSetObserver(new OffsetDataSetObserver(adapterView)); adapterView.setAdapter(mChooserRowAdapter); @@ -427,13 +461,19 @@ public class ChooserActivity extends ResolverActivity { continue; } } catch (NameNotFoundException e) { - Log.e(TAG, "Could not look up service " + serviceComponent, e); + Log.e(TAG, "Could not look up service " + serviceComponent + + "; component name not found"); continue; } final ChooserTargetServiceConnection conn = new ChooserTargetServiceConnection(this, dri); - if (bindService(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND)) { + + // Explicitly specify Process.myUserHandle instead of calling bindService + // to avoid the warning from calling from the system process without an explicit + // user handle + if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND, + Process.myUserHandle())) { if (DEBUG) { Log.d(TAG, "Binding service connection for target " + dri + " intent " + serviceIntent); @@ -635,7 +675,11 @@ public class ChooserActivity extends ResolverActivity { if (mSourceInfo != null) { return mSourceInfo.getResolvedIntent(); } - return getTargetIntent(); + + final Intent targetIntent = new Intent(getTargetIntent()); + targetIntent.setComponent(mChooserTarget.getComponentName()); + targetIntent.putExtras(mChooserTarget.getIntentExtras()); + return targetIntent; } @Override @@ -650,8 +694,7 @@ public class ChooserActivity extends ResolverActivity { } private Intent getBaseIntentToSend() { - Intent result = mSourceInfo != null - ? mSourceInfo.getResolvedIntent() : getTargetIntent(); + Intent result = getResolvedIntent(); if (result == null) { Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); } else { @@ -677,7 +720,19 @@ public class ChooserActivity extends ResolverActivity { } intent.setComponent(mChooserTarget.getComponentName()); intent.putExtras(mChooserTarget.getIntentExtras()); - activity.startActivityAsCaller(intent, options, true, userId); + + // Important: we will ignore the target security checks in ActivityManager + // if and only if the ChooserTarget's target package is the same package + // where we got the ChooserTargetService that provided it. This lets a + // ChooserTargetService provide a non-exported or permission-guarded target + // to the chooser for the user to pick. + // + // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere + // so we'll obey the caller's normal security checks. + final boolean ignoreTargetSecurity = mSourceInfo != null + && mSourceInfo.getResolvedComponentName().getPackageName() + .equals(mChooserTarget.getComponentName().getPackageName()); + activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId); return true; } @@ -810,6 +865,9 @@ public class ChooserActivity extends ResolverActivity { @Override public float getScore(DisplayResolveInfo target) { + if (target == null) { + return CALLER_TARGET_SCORE_BOOST; + } float score = super.getScore(target); if (target.isPinned()) { score += PINNED_TARGET_SCORE_BOOST; @@ -1281,7 +1339,7 @@ public class ChooserActivity extends ResolverActivity { } static class ChooserTargetServiceConnection implements ServiceConnection { - private final DisplayResolveInfo mOriginalTarget; + private DisplayResolveInfo mOriginalTarget; private ComponentName mConnectedComponent; private ChooserActivity mChooserActivity; private final Object mLock = new Object(); @@ -1359,6 +1417,7 @@ public class ChooserActivity extends ResolverActivity { public void destroy() { synchronized (mLock) { mChooserActivity = null; + mOriginalTarget = null; } } @@ -1366,7 +1425,9 @@ public class ChooserActivity extends ResolverActivity { public String toString() { return "ChooserTargetServiceConnection{service=" + mConnectedComponent + ", activity=" - + mOriginalTarget.getResolveInfo().activityInfo.toString() + "}"; + + (mOriginalTarget != null + ? mOriginalTarget.getResolveInfo().activityInfo.toString() + : "") + "}"; } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index ff680e2002ac4..f2bf9e1d96953 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -105,6 +105,7 @@ public class ResolverActivity extends Activity { private final ArrayList mIntents = new ArrayList<>(); private ResolverComparator mResolverComparator; private PickTargetOptionRequest mPickOptionRequest; + private ComponentName[] mFilteredComponents; protected ResolverDrawerLayout mResolverDrawerLayout; @@ -332,6 +333,24 @@ public class ResolverActivity extends Activity { } } + public final void setFilteredComponents(ComponentName[] components) { + mFilteredComponents = components; + } + + public final boolean isComponentFiltered(ComponentInfo component) { + if (mFilteredComponents == null) { + return false; + } + + final ComponentName checkName = component.getComponentName(); + for (ComponentName name : mFilteredComponents) { + if (name.equals(checkName)) { + return true; + } + } + return false; + } + /** * Perform any initialization needed for voice interaction. */ @@ -1269,7 +1288,8 @@ public class ResolverActivity extends Activity { ai.applicationInfo.uid, ai.exported); boolean suspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; - if (granted != PackageManager.PERMISSION_GRANTED || suspended) { + if (granted != PackageManager.PERMISSION_GRANTED || suspended + || isComponentFiltered(ai)) { // Access not allowed! if (mOrigResolveList == currentResolveList) { mOrigResolveList = new ArrayList<>(mOrigResolveList);