summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Matt Casey <mrcasey@google.com> 2023-03-02 20:32:22 +0000
committer Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> 2023-03-02 20:32:22 +0000
commit8a081608bb679f5f922e101d8971220c21ac5d07 (patch)
treeb130179b70809c4d5871ccbd4eecca64b3a7601c /java
parent5f3b784bd81039cdfc4a2d3582d794bea0662b99 (diff)
parent5e719ed805bd2c2f08e5ebf5ee34062fd8bbb2fd (diff)
Merge "Allow refinement of any matching source intent." into tm-qpr-dev am: a115be39e6 am: a2cd2f9753 am: 5e719ed805
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/21560196 Change-Id: I1470ac46398d2bde07bae7177860b2f05b9b827b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java33
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java42
-rw-r--r--java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java139
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java27
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java56
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java19
-rw-r--r--java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt117
-rw-r--r--java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt189
8 files changed, 462 insertions, 160 deletions
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
index 5b5c1d32..3ddc1c7c 100644
--- a/java/src/com/android/intentresolver/ChooserRefinementManager.java
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -40,12 +40,6 @@ import java.util.function.Consumer;
* additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to
* convert the format of the payload, or lazy-download some data that was deferred in the original
* call).
- *
- * TODO(b/262805893): this currently requires the result to be a refinement of <em>the best</em>
- * match for the user's selected target among the initially-provided source intents (according to
- * their originally-provided priority order). In order to support alternate formats/actions, we
- * should instead require it to refine <em>any</em> of the source intents -- presumably, the first
- * in priority order that matches according to {@link Intent#filterEquals()}.
*/
public final class ChooserRefinementManager {
private static final String TAG = "ChooserRefinement";
@@ -88,10 +82,12 @@ public final class ChooserRefinementManager {
mRefinementResultReceiver = new RefinementResultReceiver(
refinedIntent -> {
destroy();
- TargetInfo refinedTarget = getValidRefinedTarget(selectedTarget, refinedIntent);
+ TargetInfo refinedTarget =
+ selectedTarget.tryToCloneWithAppliedRefinement(refinedIntent);
if (refinedTarget != null) {
mOnSelectionRefined.accept(refinedTarget);
} else {
+ Log.e(TAG, "Failed to apply refinement to any matching source intent");
mOnRefinementCancelled.run();
}
},
@@ -195,27 +191,4 @@ public final class ChooserRefinementManager {
return receiverForSending;
}
}
-
- private static TargetInfo getValidRefinedTarget(
- TargetInfo originalTarget, Intent proposedRefinement) {
- if (originalTarget == null) {
- // TODO: this legacy log message doesn't seem to describe the real condition we just
- // checked; probably this method should never be invoked with a null target.
- Log.e(TAG, "Refinement result intent did not match any known targets; canceling");
- return null;
- }
- if (!checkProposalRefinesSourceIntent(originalTarget, proposedRefinement)) {
- Log.e(TAG, "Refinement " + proposedRefinement + " has no match in " + originalTarget);
- return null;
- }
- return originalTarget.cloneFilledIn(proposedRefinement, 0); // TODO: select the right base.
- }
-
- // TODO: return the actual match, to use as the base that we fill in? Or, if that's handled by
- // `TargetInfo.cloneFilledIn()`, just let it be nullable (it already is?) and don't bother doing
- // this pre-check.
- private static boolean checkProposalRefinesSourceIntent(
- TargetInfo originalTarget, Intent proposedMatch) {
- return originalTarget.getAllSourceIntents().stream().anyMatch(proposedMatch::filterEquals);
- }
}
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index 0bbd6901..29be6dc6 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -96,25 +96,22 @@ public class DisplayResolveInfo implements TargetInfo {
final ActivityInfo ai = mResolveInfo.activityInfo;
mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
- final Intent intent = new Intent(resolvedIntent);
- intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
- | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
- intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
- mResolvedIntent = intent;
+ mResolvedIntent = createResolvedIntent(resolvedIntent, ai);
}
private DisplayResolveInfo(
DisplayResolveInfo other,
- Intent fillInIntent,
- int flags,
+ @Nullable Intent baseIntentToSend,
TargetPresentationGetter presentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mIsSuspended = other.mIsSuspended;
mDisplayLabel = other.mDisplayLabel;
mExtendedInfo = other.mExtendedInfo;
- mResolvedIntent = new Intent(other.mResolvedIntent);
- mResolvedIntent.fillIn(fillInIntent, flags);
+
+ mResolvedIntent = createResolvedIntent(
+ baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,
+ mResolveInfo.activityInfo);
mPresentationGetter = presentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
@@ -132,6 +129,14 @@ public class DisplayResolveInfo implements TargetInfo {
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
+ private static Intent createResolvedIntent(Intent resolvedIntent, ActivityInfo ai) {
+ final Intent result = new Intent(resolvedIntent);
+ result.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+ | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ result.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+ return result;
+ }
+
@Override
public final boolean isDisplayResolveInfo() {
return true;
@@ -167,12 +172,21 @@ public class DisplayResolveInfo implements TargetInfo {
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return cloneFilledInInternal(fillInIntent, flags);
- }
+ @Nullable
+ public DisplayResolveInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
- protected final DisplayResolveInfo cloneFilledInInternal(Intent fillInIntent, int flags) {
- return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter);
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return new DisplayResolveInfo(this, merged, mPresentationGetter);
}
@Override
diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
index 38991c78..2d9683e1 100644
--- a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
@@ -16,6 +16,7 @@
package com.android.intentresolver.chooser;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
@@ -27,7 +28,6 @@ import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.HashedStringCache;
-import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -84,6 +84,15 @@ public final class ImmutableTargetInfo implements TargetInfo {
private ComponentName mResolvedComponentName;
@Nullable
+ private Intent mResolvedIntent;
+
+ @Nullable
+ private Intent mBaseIntentToSend;
+
+ @Nullable
+ private Intent mTargetIntent;
+
+ @Nullable
private ComponentName mChooserTargetComponentName;
@Nullable
@@ -101,20 +110,29 @@ public final class ImmutableTargetInfo implements TargetInfo {
@Nullable
private Intent mReferrerFillInIntent;
- private Intent mResolvedIntent;
- private Intent mTargetIntent;
+ @Nullable
private TargetActivityStarter mActivityStarter;
+
+ @Nullable
private ResolveInfo mResolveInfo;
+
+ @Nullable
private CharSequence mDisplayLabel;
+
+ @Nullable
private CharSequence mExtendedInfo;
+
+ @Nullable
private IconHolder mDisplayIconHolder;
- private List<Intent> mSourceIntents;
- private List<DisplayResolveInfo> mAllDisplayTargets;
+
private boolean mIsSuspended;
private boolean mIsPinned;
private float mModifiedScore = -0.1f;
private LegacyTargetType mLegacyType = LegacyTargetType.NOT_LEGACY_TARGET;
+ private ImmutableList<Intent> mAlternateSourceIntents = ImmutableList.of();
+ private ImmutableList<DisplayResolveInfo> mAllDisplayTargets = ImmutableList.of();
+
/**
* Configure an {@link Intent} to be built in to the output target as the resolution for the
* requested target data.
@@ -125,6 +143,17 @@ public final class ImmutableTargetInfo implements TargetInfo {
}
/**
+ * Configure an {@link Intent} to be built in to the output target as the "base intent to
+ * send," which may be a refinement of any of our source targets. This is private because
+ * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever
+ * expanded, the builder should probably be responsible for enforcing the refinement check.
+ */
+ private Builder setBaseIntentToSend(Intent baseIntent) {
+ mBaseIntentToSend = baseIntent;
+ return this;
+ }
+
+ /**
* Configure an {@link Intent} to be built in to the output as the "target intent."
*/
public Builder setTargetIntent(Intent targetIntent) {
@@ -192,15 +221,33 @@ public final class ImmutableTargetInfo implements TargetInfo {
return this;
}
- /** Configure the list of source intents to be built in to the output target. */
+ /** Configure the list of alternate source intents we could resolve for this target. */
+ public Builder setAlternateSourceIntents(List<Intent> sourceIntents) {
+ mAlternateSourceIntents = immutableCopyOrEmpty(sourceIntents);
+ return this;
+ }
+
+ /**
+ * Configure the full list of source intents we could resolve for this target. This is
+ * effectively the same as calling {@link #setResolvedIntent()} with the first element of
+ * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those
+ * fields on the builder if there are no corresponding elements in the list).
+ */
public Builder setAllSourceIntents(List<Intent> sourceIntents) {
- mSourceIntents = sourceIntents;
+ if ((sourceIntents == null) || sourceIntents.isEmpty()) {
+ setResolvedIntent(null);
+ setAlternateSourceIntents(null);
+ return this;
+ }
+
+ setResolvedIntent(sourceIntents.get(0));
+ setAlternateSourceIntents(sourceIntents.subList(1, sourceIntents.size()));
return this;
}
/** Configure the list of display targets to be built in to the output target. */
public Builder setAllDisplayTargets(List<DisplayResolveInfo> targets) {
- mAllDisplayTargets = targets;
+ mAllDisplayTargets = immutableCopyOrEmpty(targets);
return this;
}
@@ -246,28 +293,27 @@ public final class ImmutableTargetInfo implements TargetInfo {
return this;
}
- Builder setLegacyType(LegacyTargetType legacyType) {
+ Builder setLegacyType(@NonNull LegacyTargetType legacyType) {
mLegacyType = legacyType;
return this;
}
- /**
- * Construct an {@code ImmutableTargetInfo} with the current builder data, where the
- * provided intent is used to fill in missing values from the resolved intent before the
- * target is (potentially) ever launched.
- *
- * @see android.content.Intent#fillIn(Intent, int)
- */
- public ImmutableTargetInfo buildWithFillInIntent(
- @Nullable Intent fillInIntent, int fillInFlags) {
- Intent baseIntentToSend = mResolvedIntent;
- if (baseIntentToSend == null) {
- Log.w(TAG, "No base intent to send");
- } else {
+ /** Construct an {@code ImmutableTargetInfo} with the current builder data. */
+ public ImmutableTargetInfo build() {
+ List<Intent> sourceIntents = new ArrayList<>();
+ if (mResolvedIntent != null) {
+ sourceIntents.add(mResolvedIntent);
+ }
+ if (mAlternateSourceIntents != null) {
+ sourceIntents.addAll(mAlternateSourceIntents);
+ }
+
+ Intent baseIntentToSend = mBaseIntentToSend;
+ if ((baseIntentToSend == null) && !sourceIntents.isEmpty()) {
+ baseIntentToSend = sourceIntents.get(0);
+ }
+ if (baseIntentToSend != null) {
baseIntentToSend = new Intent(baseIntentToSend);
- if (fillInIntent != null) {
- baseIntentToSend.fillIn(fillInIntent, fillInFlags);
- }
if (mReferrerFillInIntent != null) {
baseIntentToSend.fillIn(mReferrerFillInIntent, 0);
}
@@ -275,7 +321,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
return new ImmutableTargetInfo(
baseIntentToSend,
- mResolvedIntent,
+ ImmutableList.copyOf(sourceIntents),
mTargetIntent,
mReferrerFillInIntent,
mResolvedComponentName,
@@ -285,7 +331,6 @@ public final class ImmutableTargetInfo implements TargetInfo {
mDisplayLabel,
mExtendedInfo,
mDisplayIconHolder,
- mSourceIntents,
mAllDisplayTargets,
mIsSuspended,
mIsPinned,
@@ -296,11 +341,6 @@ public final class ImmutableTargetInfo implements TargetInfo {
mHashProvider,
mLegacyType);
}
-
- /** Construct an {@code ImmutableTargetInfo} with the current builder data. */
- public ImmutableTargetInfo build() {
- return buildWithFillInIntent(null, 0);
- }
}
@Nullable
@@ -325,14 +365,13 @@ public final class ImmutableTargetInfo implements TargetInfo {
private final TargetHashProvider mHashProvider;
private final Intent mBaseIntentToSend;
- private final Intent mResolvedIntent;
+ private final ImmutableList<Intent> mSourceIntents;
private final Intent mTargetIntent;
private final TargetActivityStarter mActivityStarter;
private final ResolveInfo mResolveInfo;
private final CharSequence mDisplayLabel;
private final CharSequence mExtendedInfo;
private final IconHolder mDisplayIconHolder;
- private final ImmutableList<Intent> mSourceIntents;
private final ImmutableList<DisplayResolveInfo> mAllDisplayTargets;
private final boolean mIsSuspended;
private final boolean mIsPinned;
@@ -347,6 +386,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
/** Construct a {@link Builder} pre-initialized to match this target. */
public Builder toBuilder() {
return newBuilder()
+ .setBaseIntentToSend(getBaseIntentToSend())
.setResolvedIntent(getResolvedIntent())
.setTargetIntent(getTargetIntent())
.setReferrerFillInIntent(getReferrerFillInIntent())
@@ -375,13 +415,26 @@ public final class ImmutableTargetInfo implements TargetInfo {
}
@Override
- public ImmutableTargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return toBuilder().buildWithFillInIntent(fillInIntent, flags);
+ @Nullable
+ public ImmutableTargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
+
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return toBuilder().setBaseIntentToSend(merged).build();
}
@Override
public Intent getResolvedIntent() {
- return mResolvedIntent;
+ return (mSourceIntents.isEmpty() ? null : mSourceIntents.get(0));
}
@Override
@@ -408,11 +461,13 @@ public final class ImmutableTargetInfo implements TargetInfo {
@Override
public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+ // TODO: make sure that the component name is set in all cases
return mActivityStarter.startAsCaller(this, activity, options, userId);
}
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ // TODO: make sure that the component name is set in all cases
return mActivityStarter.startAsUser(this, activity, options, user);
}
@@ -531,7 +586,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
private ImmutableTargetInfo(
Intent baseIntentToSend,
- Intent resolvedIntent,
+ ImmutableList<Intent> sourceIntents,
Intent targetIntent,
@Nullable Intent referrerFillInIntent,
@Nullable ComponentName resolvedComponentName,
@@ -541,8 +596,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
CharSequence displayLabel,
CharSequence extendedInfo,
IconHolder iconHolder,
- @Nullable List<Intent> sourceIntents,
- @Nullable List<DisplayResolveInfo> allDisplayTargets,
+ ImmutableList<DisplayResolveInfo> allDisplayTargets,
boolean isSuspended,
boolean isPinned,
float modifiedScore,
@@ -552,7 +606,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
@Nullable TargetHashProvider hashProvider,
LegacyTargetType legacyType) {
mBaseIntentToSend = baseIntentToSend;
- mResolvedIntent = resolvedIntent;
+ mSourceIntents = sourceIntents;
mTargetIntent = targetIntent;
mReferrerFillInIntent = referrerFillInIntent;
mResolvedComponentName = resolvedComponentName;
@@ -562,8 +616,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
mDisplayLabel = displayLabel;
mExtendedInfo = extendedInfo;
mDisplayIconHolder = iconHolder;
- mSourceIntents = immutableCopyOrEmpty(sourceIntents);
- mAllDisplayTargets = immutableCopyOrEmpty(allDisplayTargets);
+ mAllDisplayTargets = allDisplayTargets;
mIsSuspended = isSuspended;
mIsPinned = isPinned;
mModifiedScore = modifiedScore;
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
index 0938c55e..b97e6b45 100644
--- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -21,7 +21,10 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
+import androidx.annotation.Nullable;
+
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -93,10 +96,19 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- ArrayList<DisplayResolveInfo> targetInfos = new ArrayList<>(mTargetInfos.size());
- for (int i = 0, size = mTargetInfos.size(); i < size; i++) {
- targetInfos.add(mTargetInfos.get(i).cloneFilledInInternal(fillInIntent, flags));
+ @Nullable
+ public MultiDisplayResolveInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ final int size = mTargetInfos.size();
+ ArrayList<DisplayResolveInfo> targetInfos = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ DisplayResolveInfo target = mTargetInfos.get(i);
+ DisplayResolveInfo targetClone = (i == mSelected)
+ ? target.tryToCloneWithAppliedRefinement(proposedRefinement)
+ : new DisplayResolveInfo(target);
+ if (targetClone == null) {
+ return null;
+ }
+ targetInfos.add(targetClone);
}
MultiDisplayResolveInfo clone = new MultiDisplayResolveInfo(targetInfos);
clone.mSelected = mSelected;
@@ -117,4 +129,11 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
public Intent getTargetIntent() {
return mTargetInfos.get(mSelected).getTargetIntent();
}
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ return hasSelected()
+ ? mTargetInfos.get(mSelected).getAllSourceIntents()
+ : Collections.emptyList();
+ }
}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index df27c2b0..1fbe2da7 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -78,7 +78,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
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;
@@ -91,12 +90,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
private final TargetActivityStarter mActivityStarter;
/**
- * A refinement intent from the caller, if any (see
- * {@link Intent#EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER})
- */
- private final Intent mFillInIntent;
-
- /**
* An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null})
* in its extended data under the key {@link Intent#EXTRA_REFERRER}.
*/
@@ -159,6 +152,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
sourceInfo,
backupResolveInfo,
resolvedIntent,
+ null,
chooserTargetComponentName,
chooserTargetUnsanitizedTitle,
chooserTargetIcon,
@@ -166,15 +160,14 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
modifiedScore,
shortcutInfo,
appTarget,
- referrerFillInIntent,
- /* fillInIntent = */ null,
- /* fillInFlags = */ 0);
+ referrerFillInIntent);
}
private SelectableTargetInfo(
@Nullable DisplayResolveInfo sourceInfo,
@Nullable ResolveInfo backupResolveInfo,
Intent resolvedIntent,
+ @Nullable Intent baseIntentToSend,
ComponentName chooserTargetComponentName,
CharSequence chooserTargetUnsanitizedTitle,
Icon chooserTargetIcon,
@@ -182,9 +175,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
float modifiedScore,
@Nullable ShortcutInfo shortcutInfo,
@Nullable AppTarget appTarget,
- Intent referrerFillInIntent,
- @Nullable Intent fillInIntent,
- int fillInFlags) {
+ Intent referrerFillInIntent) {
mSourceInfo = sourceInfo;
mBackupResolveInfo = backupResolveInfo;
mResolvedIntent = resolvedIntent;
@@ -192,8 +183,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
mShortcutInfo = shortcutInfo;
mAppTarget = appTarget;
mReferrerFillInIntent = referrerFillInIntent;
- mFillInIntent = fillInIntent;
- mFillInFlags = fillInFlags;
mChooserTargetComponentName = chooserTargetComponentName;
mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
mChooserTargetIcon = chooserTargetIcon;
@@ -209,9 +198,8 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
mAllSourceIntents = getAllSourceIntents(sourceInfo);
mBaseIntentToSend = getBaseIntentToSend(
+ baseIntentToSend,
mResolvedIntent,
- mFillInIntent,
- mFillInFlags,
mReferrerFillInIntent);
mHashProvider = context -> {
@@ -262,11 +250,12 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
};
}
- private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
+ private SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend) {
this(
other.mSourceInfo,
other.mBackupResolveInfo,
other.mResolvedIntent,
+ baseIntentToSend,
other.mChooserTargetComponentName,
other.mChooserTargetUnsanitizedTitle,
other.mChooserTargetIcon,
@@ -274,14 +263,25 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
other.mModifiedScore,
other.mShortcutInfo,
other.mAppTarget,
- other.mReferrerFillInIntent,
- fillInIntent,
- flags);
+ other.mReferrerFillInIntent);
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new SelectableTargetInfo(this, fillInIntent, flags);
+ @Nullable
+ public TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
+
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return new SelectableTargetInfo(this, merged);
}
@Override
@@ -418,18 +418,14 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
@Nullable
private static Intent getBaseIntentToSend(
- @Nullable Intent resolvedIntent,
- Intent fillInIntent,
- int fillInFlags,
+ @Nullable Intent providedBase,
+ @Nullable Intent fallbackBase,
Intent referrerFillInIntent) {
- Intent result = resolvedIntent;
+ Intent result = (providedBase != null) ? providedBase : fallbackBase;
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 69f58a7b..2f48704c 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -182,10 +182,25 @@ public interface TargetInfo {
default boolean hasDisplayIcon() {
return getDisplayIconHolder().getDisplayIcon() != null;
}
+
/**
- * Clone this target with the given fill-in information.
+ * Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager}
+ * received from the caller's refinement flow. This may succeed only if the target has a source
+ * intent that matches the filtering parameters of the proposed refinement (according to
+ * {@link Intent#filterEquals()}). Then the first such match is the "base intent," and the
+ * proposed refinement is merged into that base (via {@link Intent#fillIn()}; this can never
+ * result in a change to the {@link Intent#filterEquals()} status of the base, but may e.g. add
+ * new "extras" that weren't previously given in the base intent).
+ *
+ * @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of
+ * merging the refinement into the best-matching source intent, if possible. If there is no
+ * suitable match for the proposed refinement, or if merging fails for any other reason, this
+ * returns null.
+ *
+ * @see android.content.Intent#fillIn(Intent, int)
*/
- TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
+ @Nullable
+ TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement);
/**
* @return the list of supported source intents deduped against this single target
diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
index 4989a3f1..e9c755d3 100644
--- a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
+++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
@@ -88,7 +88,7 @@ class ImmutableTargetInfoTest {
.setDisplayLabel(displayLabel)
.setExtendedInfo(extendedInfo)
.setDisplayIconHolder(displayIconHolder)
- .setAllSourceIntents(listOf(sourceIntent1, sourceIntent2))
+ .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2))
.setAllDisplayTargets(listOf(displayTarget1, displayTarget2))
.setIsSuspended(true)
.setIsPinned(true)
@@ -108,7 +108,8 @@ class ImmutableTargetInfoTest {
assertThat(info.displayLabel).isEqualTo(displayLabel)
assertThat(info.extendedInfo).isEqualTo(extendedInfo)
assertThat(info.displayIconHolder).isEqualTo(displayIconHolder)
- assertThat(info.allSourceIntents).containsExactly(sourceIntent1, sourceIntent2)
+ assertThat(info.allSourceIntents).containsExactly(
+ resolvedIntent, sourceIntent1, sourceIntent2)
assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2)
assertThat(info.isSuspended).isTrue()
assertThat(info.isPinned).isTrue()
@@ -140,7 +141,7 @@ class ImmutableTargetInfoTest {
.setDisplayLabel(displayLabel)
.setExtendedInfo(extendedInfo)
.setDisplayIconHolder(displayIconHolder)
- .setAllSourceIntents(listOf(sourceIntent1, sourceIntent2))
+ .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2))
.setAllDisplayTargets(listOf(displayTarget1, displayTarget2))
.setIsSuspended(true)
.setIsPinned(true)
@@ -162,7 +163,8 @@ class ImmutableTargetInfoTest {
assertThat(info.displayLabel).isEqualTo(displayLabel)
assertThat(info.extendedInfo).isEqualTo(extendedInfo)
assertThat(info.displayIconHolder).isEqualTo(displayIconHolder)
- assertThat(info.allSourceIntents).containsExactly(sourceIntent1, sourceIntent2)
+ assertThat(info.allSourceIntents).containsExactly(
+ resolvedIntent, sourceIntent1, sourceIntent2)
assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2)
assertThat(info.isSuspended).isTrue()
assertThat(info.isPinned).isTrue()
@@ -204,25 +206,25 @@ class ImmutableTargetInfoTest {
}
@Test
- fun testBaseIntentToSend_fillsInFromCloneRequestIntent() {
+ fun testBaseIntentToSend_fillsInFromRefinementIntent() {
val originalIntent = Intent()
- originalIntent.setPackage("original")
+ originalIntent.putExtra("ORIGINAL", true)
- val cloneFillInIntent = Intent("CLONE_FILL_IN")
- cloneFillInIntent.setPackage("clone")
+ val refinementIntent = Intent()
+ refinementIntent.putExtra("REFINEMENT", true)
val originalInfo = ImmutableTargetInfo.newBuilder()
.setResolvedIntent(originalIntent)
.build()
- val info = originalInfo.cloneFilledIn(cloneFillInIntent, 0)
+ val info = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent)
- assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Only fill if empty.
- assertThat(info.baseIntentToSend.action).isEqualTo("CLONE_FILL_IN")
+ assertThat(info.baseIntentToSend.getBooleanExtra("ORIGINAL", false)).isTrue()
+ assertThat(info.baseIntentToSend.getBooleanExtra("REFINEMENT", false)).isTrue()
}
@Test
- fun testBaseIntentToSend_twoFillInSourcesFavorsCloneRequest() {
- val originalIntent = Intent()
+ fun testBaseIntentToSend_twoFillInSourcesFavorsRefinementRequest() {
+ val originalIntent = Intent("REFINE_ME")
originalIntent.setPackage("original")
val referrerFillInIntent = Intent("REFERRER_FILL_IN")
@@ -234,43 +236,92 @@ class ImmutableTargetInfoTest {
.setReferrerFillInIntent(referrerFillInIntent)
.build()
- val cloneFillInIntent = Intent("CLONE_FILL_IN")
- cloneFillInIntent.setPackage("clone")
+ val refinementIntent = Intent("REFINE_ME")
+ refinementIntent.setPackage("original") // Has to match for refinement.
- val info = infoWithReferrerFillIn.cloneFilledIn(cloneFillInIntent, 0)
+ val info = infoWithReferrerFillIn.tryToCloneWithAppliedRefinement(refinementIntent)
assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Set all along.
- assertThat(info.baseIntentToSend.action).isEqualTo("CLONE_FILL_IN") // Clone wins.
+ assertThat(info.baseIntentToSend.action).isEqualTo("REFINE_ME") // Refinement wins.
assertThat(info.baseIntentToSend.type).isEqualTo("test/referrer") // Left for referrer.
}
@Test
- fun testBaseIntentToSend_doubleCloningPreservesReferrerFillInButNotOriginalCloneFillIn() {
- val originalIntent = Intent()
+ fun testBaseIntentToSend_doubleRefinementPreservesReferrerFillInButNotOriginalRefinement() {
+ val originalIntent = Intent("REFINE_ME")
val referrerFillInIntent = Intent("REFERRER_FILL_IN")
- val cloneFillInIntent1 = Intent()
- cloneFillInIntent1.setPackage("clone1")
- val cloneFillInIntent2 = Intent()
- cloneFillInIntent2.setType("test/clone2")
+ referrerFillInIntent.putExtra("TEST", "REFERRER")
+ val refinementIntent1 = Intent("REFINE_ME")
+ refinementIntent1.putExtra("TEST1", "1")
+ val refinementIntent2 = Intent("REFINE_ME")
+ refinementIntent2.putExtra("TEST2", "2")
val originalInfo = ImmutableTargetInfo.newBuilder()
.setResolvedIntent(originalIntent)
.setReferrerFillInIntent(referrerFillInIntent)
.build()
- val clone1 = originalInfo.cloneFilledIn(cloneFillInIntent1, 0)
- val clone2 = clone1.cloneFilledIn(cloneFillInIntent2, 0) // Clone-of-clone.
+ val refined1 = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent1)
+ val refined2 = refined1.tryToCloneWithAppliedRefinement(refinementIntent2) // Cloned clone.
// Both clones get the same values filled in from the referrer intent.
- assertThat(clone1.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN")
- assertThat(clone2.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN")
- // Each clone has the respective value that was set in the fill-in request.
- assertThat(clone1.baseIntentToSend.getPackage()).isEqualTo("clone1")
- assertThat(clone2.baseIntentToSend.type).isEqualTo("test/clone2")
- // The clones don't have the data from each other's fill-in requests, even though the intent
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER")
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER")
+ // Each clone has the respective value that was set in their own refinement request.
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST1")).isEqualTo("1")
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST2")).isEqualTo("2")
+ // The clones don't have the data from each other's refinements, even though the intent
// field is empty (thus able to be populated by filling-in).
- assertThat(clone1.baseIntentToSend.type).isNull()
- assertThat(clone2.baseIntentToSend.getPackage()).isNull()
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST2")).isNull()
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST1")).isNull()
+ }
+
+ @Test
+ fun testBaseIntentToSend_refinementToAlternateSourceIntent() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+ val targetAlternate = Intent("REFINE_ME")
+ targetAlternate.putExtra("targetAlternate", true)
+ val extraMatch = Intent("REFINE_ME")
+ extraMatch.putExtra("extraMatch", true)
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setAllSourceIntents(listOf(
+ originalIntent, mismatchedAlternate, targetAlternate, extraMatch))
+ .build()
+
+ val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
+ refinement.putExtra("refinement", true)
+
+ val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement)
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("refinement", false)).isTrue()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("targetAlternate", false))
+ .isTrue()
+ // None of the other source intents got merged in (not even the later one that matched):
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("originalIntent", false))
+ .isFalse()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("mismatchedAlternate", false))
+ .isFalse()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("extraMatch", false)).isFalse()
+ }
+
+ @Test
+ fun testBaseIntentToSend_noSourceIntentMatchingProposedRefinement() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setAllSourceIntents(listOf(originalIntent, mismatchedAlternate))
+ .build()
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
}
@Test
diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
index e9dbe00e..dddbcccb 100644
--- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
+++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -26,13 +26,19 @@ import android.graphics.drawable.AnimatedVectorDrawable
import android.os.UserHandle
import android.test.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.ResolverDataProvider
import com.android.intentresolver.createChooserTarget
import com.android.intentresolver.createShortcutInfo
import com.android.intentresolver.mock
-import com.android.intentresolver.ResolverDataProvider
+import com.android.intentresolver.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
class TargetInfoTest {
private val context = InstrumentationRegistry.getInstrumentation().getContext()
@@ -143,6 +149,42 @@ class TargetInfoTest {
}
@Test
+ fun testSelectableTargetInfo_noSourceIntentMatchingProposedRefinement() {
+ val resolvedIntent = Intent("DONT_REFINE_ME")
+ resolvedIntent.putExtra("resolvedIntent", true)
+
+ val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ resolvedIntent,
+ ResolverDataProvider.createResolveInfo(1, 0),
+ "label",
+ "extended info",
+ resolvedIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ val chooserTarget = createChooserTarget(
+ "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
+ val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
+ val appTarget = AppTarget(
+ AppTargetId("id"),
+ chooserTarget.componentName.packageName,
+ chooserTarget.componentName.className,
+ UserHandle.CURRENT)
+
+ val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
+ baseDisplayInfo,
+ mock(),
+ resolvedIntent,
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
+ }
+
+ @Test
fun testNewDisplayResolveInfo() {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
@@ -163,6 +205,64 @@ class TargetInfoTest {
}
@Test
+ fun test_DisplayResolveInfo_refinementToAlternateSourceIntent() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+ val targetAlternate = Intent("REFINE_ME")
+ targetAlternate.putExtra("targetAlternate", true)
+ val extraMatch = Intent("REFINE_ME")
+ extraMatch.putExtra("extraMatch", true)
+
+ val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ResolverDataProvider.createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ originalInfo.addAlternateSourceIntent(mismatchedAlternate)
+ originalInfo.addAlternateSourceIntent(targetAlternate)
+ originalInfo.addAlternateSourceIntent(extraMatch)
+
+ val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
+ refinement.putExtra("refinement", true)
+
+ val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement)
+ // Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`.
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("refinement", false)).isTrue()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("targetAlternate", false))
+ .isTrue()
+ // None of the other source intents got merged in (not even the later one that matched):
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("originalIntent", false))
+ .isFalse()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("mismatchedAlternate", false))
+ .isFalse()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("extraMatch", false)).isFalse()
+ }
+
+ @Test
+ fun testDisplayResolveInfo_noSourceIntentMatchingProposedRefinement() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+
+ val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ResolverDataProvider.createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ originalInfo.addAlternateSourceIntent(mismatchedAlternate)
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
+ }
+
+ @Test
fun testNewMultiDisplayResolveInfo() {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
@@ -204,12 +304,93 @@ class TargetInfoTest {
assertThat(multiTargetInfo.hasSelected()).isTrue()
assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo)
- val multiTargetInfoClone = multiTargetInfo.cloneFilledIn(Intent(), 0)
- assertThat(multiTargetInfoClone).isInstanceOf(MultiDisplayResolveInfo::class.java)
- assertThat((multiTargetInfoClone as MultiDisplayResolveInfo).hasSelected())
+ val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent)
+ assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java)
+ assertThat((refined as MultiDisplayResolveInfo).hasSelected())
.isEqualTo(multiTargetInfo.hasSelected())
// TODO: consider exercising activity-start behavior.
// TODO: consider exercising DisplayResolveInfo base class behavior.
}
+
+ @Test
+ fun testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTarget() {
+ val sendImage = Intent("SEND").apply { type = "image/png" }
+ val sendUri = Intent("SEND").apply { type = "text/uri" }
+
+ val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0)
+
+ val imageOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image",
+ "Sends only images",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null)
+
+ val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendUri,
+ resolveInfo,
+ "Send Text",
+ "Sends only text",
+ sendUri,
+ /* resolveInfoPresentationGetter= */ null)
+
+ val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image or Text",
+ "Sends images or text",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null
+ ).apply {
+ addAlternateSourceIntent(sendUri)
+ }
+
+ val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget)
+ )
+
+ multiTargetInfo.setSelected(0)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOnlyTarget.allSourceIntents)
+
+ multiTargetInfo.setSelected(1)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(textOnlyTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(textOnlyTarget.allSourceIntents)
+
+ multiTargetInfo.setSelected(2)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOrTextTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOrTextTarget.allSourceIntents)
+ }
+
+ @Test
+ fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() {
+ val refined = Intent("SEND")
+ val sendImage = Intent("SEND")
+ val targetOne = spy(
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ ResolverDataProvider.createResolveInfo(1, 0),
+ "Target One",
+ "Target One",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null
+ )
+ )
+ val targetTwo = mock<DisplayResolveInfo> {
+ whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this)
+ }
+
+ val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(targetOne, targetTwo)
+ )
+
+ multiTargetInfo.setSelected(1)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo)
+
+ multiTargetInfo.tryToCloneWithAppliedRefinement(refined)
+ verify(targetTwo, times(1)).tryToCloneWithAppliedRefinement(refined)
+ verify(targetOne, never()).tryToCloneWithAppliedRefinement(any())
+ }
}