From 85257d68d79fe1d1188b7d4365f35f7d72801355 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Tue, 15 Nov 2022 10:46:47 -0500 Subject: SelectableTargetInfo "immutability"/other cleanup This CL makes many small "pure refactoring" changes as part of my ongoing go/chooser-targetinfo-cleanup. 1. Most important: introduce APIs to manage "functional"/"mutable" aspects of `SelectableTargetInfo`: `IconHolder` (to manage icon getting/setting), `TargetActivityStarter` (for the various `start` methods in `TargetInfo`), and `TargetHashProvider` (to compute a hash we use for metrics logging). I pulled `IconHolder` up into the base API and exposed it directly to clients (since they're really the ones that manipulate the "set" icon, and eventually they shold take over more of the icon-management responsibilities, as I've noted in a new TODO comment). I've left the others in `SelectableTargetInfo` for now since their responsibilities aren't as concerning in an "immutable" `TargetInfo` -- they defer some computation that's parameterized on the caller arguments, but they don't directly mutate any state in the target object. Eventually, the other interfaces can move to the upcoming new `ImmutableTargetInfo` type (where it will be convenient having the ability to copy these sub-components as whole objects). 2. Precompute any other fields in `SelectableTargetInfo` to show that we're now basically ready to implement immutability. 3. Extract `ChooserTarget` fields in the `SelectableTargetInfo` "factory method" -- that deprecated type is no longer part of the API of this `TargetInfo` implementation. Also add a new factory overload to skip the `ChooserTarget` representation for synthetic targets where it was only used as an intermediary in shuttling around these particular fields. 4. Implement `SelectableTargetInfo` "copy constructor" in terms of its primary (internal) constructor to make sure we perform any necessary initialization steps consistently. Test: atest IntentResolverUnitTests Bug: 202167050 Change-Id: If6f0cceebc4627fcd9a796948738c34195269e6d --- .../android/intentresolver/ChooserActivity.java | 9 +- .../android/intentresolver/ChooserListAdapter.java | 2 +- .../android/intentresolver/ResolverActivity.java | 2 +- .../intentresolver/ResolverListAdapter.java | 4 +- .../intentresolver/chooser/DisplayResolveInfo.java | 18 +- .../chooser/NotSelectableTargetInfo.java | 37 ++- .../chooser/SelectableTargetInfo.java | 331 ++++++++++++++------- .../android/intentresolver/chooser/TargetInfo.java | 49 ++- 8 files changed, 306 insertions(+), 146 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index ad106bba..f6feaf77 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -902,7 +902,8 @@ public class ChooserActivity extends ResolverActivity implements "", resolveIntent, null); - dri.setDisplayIcon(getDrawable(com.android.internal.R.drawable.ic_screenshot_edit)); + dri.getDisplayIconHolder().setDisplayIcon( + getDrawable(com.android.internal.R.drawable.ic_screenshot_edit)); return dri; } @@ -946,7 +947,7 @@ public class ChooserActivity extends ResolverActivity implements final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( originalIntent, ri, name, "", resolveIntent, null); - dri.setDisplayIcon(icon); + dri.getDisplayIconHolder().setDisplayIcon(icon); return dri; } @@ -976,7 +977,7 @@ public class ChooserActivity extends ResolverActivity implements if (ti == null) return null; final Button b = createActionButton( - ti.getDisplayIcon(), + ti.getDisplayIconHolder().getDisplayIcon(), ti.getDisplayLabel(), (View unused) -> { // Log share completion via nearby @@ -999,7 +1000,7 @@ public class ChooserActivity extends ResolverActivity implements if (ti == null) return null; final Button b = createActionButton( - ti.getDisplayIcon(), + ti.getDisplayIconHolder().getDisplayIcon(), ti.getDisplayLabel(), (View unused) -> { // Log share completion via edit diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index b18d2718..c3926889 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -660,7 +660,7 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override protected void onPostExecute(@Nullable Drawable icon) { if (icon != null && !mTargetInfo.hasDisplayIcon()) { - mTargetInfo.setDisplayIcon(icon); + mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon); notifyDataSetChanged(); } } diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index fece8d3d..64620a9d 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -1610,7 +1610,7 @@ public class ResolverActivity extends FragmentActivity implements @Override protected void onPostExecute(Drawable drawable) { if (!isDestroyed()) { - otherProfileResolveInfo.setDisplayIcon(drawable); + otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); } } diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index d97191c6..89bee17a 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -920,7 +920,7 @@ public class ResolverListAdapter extends BaseAdapter { } public void bindIcon(TargetInfo info) { - icon.setImageDrawable(info.getDisplayIcon()); + icon.setImageDrawable(info.getDisplayIconHolder().getDisplayIcon()); if (info.isSuspended()) { icon.setColorFilter(getSuspendedColorMatrix()); } else { @@ -999,7 +999,7 @@ public class ResolverListAdapter extends BaseAdapter { if (getOtherProfile() == mDisplayResolveInfo) { mResolverListCommunicator.updateProfileViewButton(); } else if (!mDisplayResolveInfo.hasDisplayIcon()) { - mDisplayResolveInfo.setDisplayIcon(d); + mDisplayResolveInfo.getDisplayIconHolder().setDisplayIcon(d); notifyDataSetChanged(); } } diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index 16dd28bc..c1b007af 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; @@ -42,13 +41,13 @@ import java.util.List; public class DisplayResolveInfo implements TargetInfo { private final ResolveInfo mResolveInfo; private CharSequence mDisplayLabel; - private Drawable mDisplayIcon; private CharSequence mExtendedInfo; private final Intent mResolvedIntent; private final List mSourceIntents = new ArrayList<>(); private final boolean mIsSuspended; private ResolveInfoPresentationGetter mResolveInfoPresentationGetter; private boolean mPinned = false; + private final IconHolder mDisplayIconHolder = new SettableIconHolder(); /** Create a new {@code DisplayResolveInfo} instance. */ public static DisplayResolveInfo newDisplayResolveInfo( @@ -103,7 +102,6 @@ public class DisplayResolveInfo implements TargetInfo { | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); mResolvedIntent = intent; - } private DisplayResolveInfo( @@ -115,11 +113,12 @@ public class DisplayResolveInfo implements TargetInfo { mResolveInfo = other.mResolveInfo; mIsSuspended = other.mIsSuspended; mDisplayLabel = other.mDisplayLabel; - mDisplayIcon = other.mDisplayIcon; mExtendedInfo = other.mExtendedInfo; mResolvedIntent = new Intent(other.mResolvedIntent); mResolvedIntent.fillIn(fillInIntent, flags); mResolveInfoPresentationGetter = resolveInfoPresentationGetter; + + mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon()); } protected DisplayResolveInfo(DisplayResolveInfo other) { @@ -127,10 +126,11 @@ public class DisplayResolveInfo implements TargetInfo { mResolveInfo = other.mResolveInfo; mIsSuspended = other.mIsSuspended; mDisplayLabel = other.mDisplayLabel; - mDisplayIcon = other.mDisplayIcon; mExtendedInfo = other.mExtendedInfo; mResolvedIntent = other.mResolvedIntent; mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter; + + mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon()); } @Override @@ -163,8 +163,8 @@ public class DisplayResolveInfo implements TargetInfo { } @Override - public Drawable getDisplayIcon() { - return mDisplayIcon; + public IconHolder getDisplayIconHolder() { + return mDisplayIconHolder; } @Override @@ -186,10 +186,6 @@ public class DisplayResolveInfo implements TargetInfo { mSourceIntents.add(alt); } - public void setDisplayIcon(Drawable icon) { - mDisplayIcon = icon; - } - public CharSequence getExtendedInfo() { return mExtendedInfo; } diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java index 3b4b89b1..d6333374 100644 --- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java @@ -43,11 +43,6 @@ public abstract class NotSelectableTargetInfo extends ChooserTargetInfo { public boolean isEmptyTargetInfo() { return true; } - - @Override - public Drawable getDisplayIcon() { - return null; - } }; } @@ -63,11 +58,20 @@ public abstract class NotSelectableTargetInfo extends ChooserTargetInfo { } @Override - public Drawable getDisplayIcon() { - AnimatedVectorDrawable avd = (AnimatedVectorDrawable) - context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder); - avd.start(); // Start animation after generation. - return avd; + public IconHolder getDisplayIconHolder() { + return new IconHolder() { + @Override + public Drawable getDisplayIcon() { + AnimatedVectorDrawable avd = (AnimatedVectorDrawable) + context.getDrawable( + R.drawable.chooser_direct_share_icon_placeholder); + avd.start(); // Start animation after generation. + return avd; + } + + @Override + public void setDisplayIcon(Drawable icon) {} + }; } @Override @@ -132,4 +136,17 @@ public abstract class NotSelectableTargetInfo extends ChooserTargetInfo { public boolean isPinned() { return false; } + + @Override + public IconHolder getDisplayIconHolder() { + return new IconHolder() { + @Override + public Drawable getDisplayIcon() { + return null; + } + + @Override + public void setDisplayIcon(Drawable icon) {} + }; + } } diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java index 51a776db..3ab50175 100644 --- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java @@ -24,7 +24,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; -import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; @@ -47,6 +46,16 @@ import java.util.List; public final class SelectableTargetInfo extends ChooserTargetInfo { private static final String TAG = "SelectableTargetInfo"; + private interface TargetHashProvider { + HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context); + } + + private interface TargetActivityStarter { + boolean start(Activity activity, Bundle options); + boolean startAsCaller(Activity activity, Bundle options, int userId); + boolean startAsUser(Activity activity, Bundle options, UserHandle user); + } + private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity"; // For legacy reasons. private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7; @@ -67,9 +76,20 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { private final ShortcutInfo mShortcutInfo; private final ComponentName mChooserTargetComponentName; - private final String mChooserTargetUnsanitizedTitle; + private final CharSequence mChooserTargetUnsanitizedTitle; private final Icon mChooserTargetIcon; private final Bundle mChooserTargetIntentExtras; + private final int mFillInFlags; + private final boolean mIsPinned; + private final float mModifiedScore; + private final boolean mIsSuspended; + private final ComponentName mResolvedComponentName; + private final Intent mBaseIntentToSend; + private final ResolveInfo mResolveInfo; + private final List mAllSourceIntents; + private final IconHolder mDisplayIconHolder = new SettableIconHolder(); + private final TargetHashProvider mHashProvider; + private final TargetActivityStarter mActivityStarter; /** * A refinement intent from the caller, if any (see @@ -82,13 +102,14 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { * in its extended data under the key {@link Intent#EXTRA_REFERRER}. */ private final Intent mReferrerFillInIntent; - private final int mFillInFlags; - private final boolean mIsPinned; - private final float mModifiedScore; - private Drawable mDisplayIcon; - - /** Create a new {@link TargetInfo} instance representing a selectable target. */ + /** + * Create a new {@link TargetInfo} instance representing a selectable target. Some target + * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure. + * + * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}. + */ + @Deprecated public static TargetInfo newSelectableTargetInfo( @Nullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, @@ -98,65 +119,175 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent) { - return new SelectableTargetInfo( + return newSelectableTargetInfo( sourceInfo, backupResolveInfo, resolvedIntent, - chooserTarget, + chooserTarget.getComponentName(), + chooserTarget.getTitle(), + chooserTarget.getIcon(), + chooserTarget.getIntentExtras(), modifiedScore, shortcutInfo, appTarget, referrerFillInIntent); } - private SelectableTargetInfo( + /** + * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*` + * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures + * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed + * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly + * as if they had been provided in the legacy representation. + * + * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename + * to avoid making reference to the legacy type; and reflect the improved semantics in the + * signature (and documentation) of this method. + */ + public static TargetInfo newSelectableTargetInfo( @Nullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, - ChooserTarget chooserTarget, + ComponentName chooserTargetComponentName, + CharSequence chooserTargetUnsanitizedTitle, + Icon chooserTargetIcon, + @Nullable Bundle chooserTargetIntentExtras, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent) { + return new SelectableTargetInfo( + sourceInfo, + backupResolveInfo, + resolvedIntent, + chooserTargetComponentName, + chooserTargetUnsanitizedTitle, + chooserTargetIcon, + chooserTargetIntentExtras, + modifiedScore, + shortcutInfo, + appTarget, + referrerFillInIntent, + /* fillInIntent = */ null, + /* fillInFlags = */ 0); + } + + private SelectableTargetInfo( + @Nullable DisplayResolveInfo sourceInfo, + @Nullable ResolveInfo backupResolveInfo, + Intent resolvedIntent, + ComponentName chooserTargetComponentName, + CharSequence chooserTargetUnsanitizedTitle, + Icon chooserTargetIcon, + Bundle chooserTargetIntentExtras, + float modifiedScore, + @Nullable ShortcutInfo shortcutInfo, + @Nullable AppTarget appTarget, + Intent referrerFillInIntent, + @Nullable Intent fillInIntent, + int fillInFlags) { mSourceInfo = sourceInfo; + mBackupResolveInfo = backupResolveInfo; + mResolvedIntent = resolvedIntent; mModifiedScore = modifiedScore; mShortcutInfo = shortcutInfo; mAppTarget = appTarget; - mIsPinned = shortcutInfo != null && shortcutInfo.isPinned(); - mBackupResolveInfo = backupResolveInfo; - mResolvedIntent = resolvedIntent; mReferrerFillInIntent = referrerFillInIntent; + mFillInIntent = fillInIntent; + mFillInFlags = fillInFlags; + mChooserTargetComponentName = chooserTargetComponentName; + mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle; + mChooserTargetIcon = chooserTargetIcon; + mChooserTargetIntentExtras = chooserTargetIntentExtras; - mFillInIntent = null; - mFillInFlags = 0; + mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned(); + mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle); + mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended(); + mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; + + mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo); + + mAllSourceIntents = getAllSourceIntents(sourceInfo); + + mBaseIntentToSend = getBaseIntentToSend( + mResolvedIntent, + mFillInIntent, + mFillInFlags, + mReferrerFillInIntent); + + mHashProvider = context -> { + final String plaintext = + getChooserTargetComponentName().getPackageName() + + mChooserTargetUnsanitizedTitle; + return HashedStringCache.getInstance().hashString( + context, + HASHED_STRING_CACHE_TAG, + plaintext, + mMaxHashSaltDays); + }; + + mActivityStarter = new TargetActivityStarter() { + @Override + public boolean start(Activity activity, Bundle options) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } - mChooserTargetComponentName = chooserTarget.getComponentName(); - mChooserTargetUnsanitizedTitle = chooserTarget.getTitle().toString(); - mChooserTargetIcon = chooserTarget.getIcon(); - mChooserTargetIntentExtras = chooserTarget.getIntentExtras(); + @Override + public boolean startAsCaller(Activity activity, Bundle options, int userId) { + final Intent intent = mBaseIntentToSend; + if (intent == null) { + return false; + } + intent.setComponent(getChooserTargetComponentName()); + intent.putExtras(mChooserTargetIntentExtras); + TargetInfo.prepareIntentForCrossProfileLaunch(intent, 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 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(getChooserTargetComponentName().getPackageName()); + activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId); + return true; + } - mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle); + @Override + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } + }; } private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) { - mSourceInfo = other.mSourceInfo; - mBackupResolveInfo = other.mBackupResolveInfo; - mResolvedIntent = other.mResolvedIntent; - mShortcutInfo = other.mShortcutInfo; - mAppTarget = other.mAppTarget; - mDisplayIcon = other.mDisplayIcon; - mFillInIntent = fillInIntent; - mFillInFlags = flags; - mModifiedScore = other.mModifiedScore; - mIsPinned = other.mIsPinned; - mReferrerFillInIntent = other.mReferrerFillInIntent; + this( + other.mSourceInfo, + other.mBackupResolveInfo, + other.mResolvedIntent, + other.mChooserTargetComponentName, + other.mChooserTargetUnsanitizedTitle, + other.mChooserTargetIcon, + other.mChooserTargetIntentExtras, + other.mModifiedScore, + other.mShortcutInfo, + other.mAppTarget, + other.mReferrerFillInIntent, + fillInIntent, + flags); + } - mChooserTargetComponentName = other.mChooserTargetComponentName; - mChooserTargetUnsanitizedTitle = other.mChooserTargetUnsanitizedTitle; - mChooserTargetIcon = other.mChooserTargetIcon; - mChooserTargetIntentExtras = other.mChooserTargetIntentExtras; + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new SelectableTargetInfo(this, fillInIntent, flags); + } - mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle); + @Override + public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { + return mHashProvider.getHashedTargetIdForMetrics(context); } @Override @@ -166,7 +297,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { @Override public boolean isSuspended() { - return (mSourceInfo != null) && mSourceInfo.isSuspended(); + return mIsSuspended; } @Override @@ -187,13 +318,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { @Override public ComponentName getResolvedComponentName() { - if (mSourceInfo != null) { - return mSourceInfo.getResolvedComponentName(); - } else if (mBackupResolveInfo != null) { - return new ComponentName(mBackupResolveInfo.activityInfo.packageName, - mBackupResolveInfo.activityInfo.name); - } - return null; + return mResolvedComponentName; } @Override @@ -206,58 +331,24 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { return mChooserTargetIcon; } - private Intent getBaseIntentToSend() { - Intent result = getResolvedIntent(); - if (result == null) { - Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); - } else { - result = new Intent(result); - if (mFillInIntent != null) { - result.fillIn(mFillInIntent, mFillInFlags); - } - result.fillIn(mReferrerFillInIntent, 0); - } - return result; - } - @Override public boolean start(Activity activity, Bundle options) { - throw new RuntimeException("ChooserTargets should be started as caller."); + return mActivityStarter.start(activity, options); } @Override public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { - final Intent intent = getBaseIntentToSend(); - if (intent == null) { - return false; - } - intent.setComponent(getChooserTargetComponentName()); - intent.putExtras(mChooserTargetIntentExtras); - TargetInfo.prepareIntentForCrossProfileLaunch(intent, 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(getChooserTargetComponentName().getPackageName()); - activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId); - return true; + return mActivityStarter.startAsCaller(activity, options, userId); } @Override public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - throw new RuntimeException("ChooserTargets should be started as caller."); + return mActivityStarter.startAsUser(activity, options, user); } @Override public ResolveInfo getResolveInfo() { - return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; + return mResolveInfo; } @Override @@ -272,12 +363,8 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { } @Override - public Drawable getDisplayIcon() { - return mDisplayIcon; - } - - public void setDisplayIcon(Drawable icon) { - mDisplayIcon = icon; + public IconHolder getDisplayIconHolder() { + return mDisplayIconHolder; } @Override @@ -292,19 +379,9 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { return mAppTarget; } - @Override - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new SelectableTargetInfo(this, fillInIntent, flags); - } - @Override public List getAllSourceIntents() { - final List results = new ArrayList<>(); - if (mSourceInfo != null) { - // We only queried the service for the first one in our sourceinfo. - results.add(mSourceInfo.getAllSourceIntents().get(0)); - } - return results; + return mAllSourceIntents; } @Override @@ -312,21 +389,49 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { return mIsPinned; } - @Override - public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { - final String plaintext = - getChooserTargetComponentName().getPackageName() - + mChooserTargetUnsanitizedTitle; - return HashedStringCache.getInstance().hashString( - context, - HASHED_STRING_CACHE_TAG, - plaintext, - mMaxHashSaltDays); - } - private static String sanitizeDisplayLabel(CharSequence label) { SpannableStringBuilder sb = new SpannableStringBuilder(label); sb.clearSpans(); return sb.toString(); } + + private static List getAllSourceIntents(@Nullable DisplayResolveInfo sourceInfo) { + final List results = new ArrayList<>(); + if (sourceInfo != null) { + // We only queried the service for the first one in our sourceinfo. + results.add(sourceInfo.getAllSourceIntents().get(0)); + } + return results; + } + + private static ComponentName getResolvedComponentName( + @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) { + if (sourceInfo != null) { + return sourceInfo.getResolvedComponentName(); + } else if (backupResolveInfo != null) { + return new ComponentName( + backupResolveInfo.activityInfo.packageName, + backupResolveInfo.activityInfo.name); + } + return null; + } + + @Nullable + private static Intent getBaseIntentToSend( + @Nullable Intent resolvedIntent, + Intent fillInIntent, + int fillInFlags, + Intent referrerFillInIntent) { + Intent result = resolvedIntent; + if (result == null) { + Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); + } else { + result = new Intent(result); + if (fillInIntent != null) { + result.fillIn(fillInIntent, fillInFlags); + } + result.fillIn(referrerFillInIntent, 0); + } + return result; + } } diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java index 0e100d4f..72dd1b0b 100644 --- a/java/src/com/android/intentresolver/chooser/TargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java @@ -42,6 +42,42 @@ import java.util.Objects; * A single target as represented in the chooser. */ public interface TargetInfo { + + /** + * Container for a {@link TargetInfo}'s (potentially) mutable icon state. This is provided to + * encapsulate the state so that the {@link TargetInfo} itself can be "immutable" (in some + * sense) as long as it always returns the same {@link IconHolder} instance. + * + * TODO: move "stateful" responsibilities out to clients; for more info see the Javadoc comment + * on {@link #getDisplayIconHolder()}. + */ + interface IconHolder { + /** @return the icon (if it's already loaded, or statically available), or null. */ + @Nullable + Drawable getDisplayIcon(); + + /** + * @param icon the icon to return on subsequent calls to {@link #getDisplayIcon()}. + * Implementations may discard this request as a no-op if they don't support setting. + */ + void setDisplayIcon(Drawable icon); + } + + /** A simple mutable-container implementation of {@link IconHolder}. */ + final class SettableIconHolder implements IconHolder { + @Nullable + private Drawable mDisplayIcon; + + @Nullable + public Drawable getDisplayIcon() { + return mDisplayIcon; + } + + public void setDisplayIcon(Drawable icon) { + mDisplayIcon = icon; + } + } + /** * Get the resolved intent that represents this target. Note that this may not be the * intent that will be launched by calling one of the start methods provided; @@ -135,16 +171,21 @@ public interface TargetInfo { CharSequence getExtendedInfo(); /** - * @return The drawable that should be used to represent this target including badge + * @return the {@link IconHolder} for the icon used to represent this target, including badge. + * + * TODO: while the {@link TargetInfo} may be immutable in always returning the same instance of + * {@link IconHolder} here, the holder itself is mutable state, and could become a problem if we + * ever rely on {@link TargetInfo} immutability elsewhere. Ideally, the {@link TargetInfo} + * should provide an immutable "spec" that tells clients how to load the appropriate + * icon, while leaving the load itself to some external component. */ - @Nullable - Drawable getDisplayIcon(); + IconHolder getDisplayIconHolder(); /** * @return true if display icon is available. */ default boolean hasDisplayIcon() { - return getDisplayIcon() != null; + return getDisplayIconHolder().getDisplayIcon() != null; } /** * Clone this target with the given fill-in information. -- cgit v1.2.3-59-g8ed1b