diff options
| author | 2023-02-07 18:37:20 +0000 | |
|---|---|---|
| committer | 2023-02-07 18:37:20 +0000 | |
| commit | e82f56e5baaecfbb5339b2beafb6e103a2f68331 (patch) | |
| tree | da5a9ef040a0850ba6ff5c488f09880eeea5628a /java | |
| parent | 276b55adbeb06f273303ef83008a095911ffa8cb (diff) | |
| parent | 2d6ec2c5793302a8adae46e614eab9d94798c658 (diff) | |
Merge "Introduce ImmutableTargetInfo." into tm-qpr-dev am: 0d66c83c4d am: 2d6ec2c579
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/21161321
Change-Id: I4df5764b5939506f890c990826327552683a1042
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/chooser/ImmutableTargetInfo.java | 596 | ||||
| -rw-r--r-- | java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt | 496 |
2 files changed, 1092 insertions, 0 deletions
diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java new file mode 100644 index 00000000..315cea4d --- /dev/null +++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.chooser; + +import android.annotation.Nullable; +import android.app.Activity; +import android.app.prediction.AppTarget; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +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; + +import com.android.intentresolver.ResolverActivity; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of {@link TargetInfo} with immutable data. Any modifications must be made by + * creating a new instance (e.g., via {@link ImmutableTargetInfo#toBuilder()}). + */ +public final class ImmutableTargetInfo implements TargetInfo { + private static final String TAG = "TargetInfo"; + + /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */ + public interface TargetHashProvider { + /** Request a hash for the specified {@code target}. */ + HashedStringCache.HashResult getHashedTargetIdForMetrics( + TargetInfo target, Context context); + } + + /** Delegate interface to request that the target be launched by a particular API. */ + public interface TargetActivityStarter { + /** + * Request that the delegate use the {@link Activity#startActivity()} API to launch the + * specified {@code target}. + * + * @return true if the target was launched successfully. + */ + boolean start(TargetInfo target, Activity activity, Bundle options); + + + /** + * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the + * specified {@code target}. + * + * @return true if the target was launched successfully. + */ + boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId); + + /** + * Request that the delegate use the {@link Activity#startAsUser()} API to launch the + * specified {@code target}. + * + * @return true if the target was launched successfully. + */ + boolean startAsUser(TargetInfo target, Activity activity, Bundle options, UserHandle user); + } + + enum LegacyTargetType { + NOT_LEGACY_TARGET, + EMPTY_TARGET_INFO, + PLACEHOLDER_TARGET_INFO, + SELECTABLE_TARGET_INFO, + DISPLAY_RESOLVE_INFO, + MULTI_DISPLAY_RESOLVE_INFO + }; + + /** Builder API to construct {@code ImmutableTargetInfo} instances. */ + public static class Builder { + @Nullable + private ComponentName mResolvedComponentName; + + @Nullable + private ComponentName mChooserTargetComponentName; + + @Nullable + private ShortcutInfo mDirectShareShortcutInfo; + + @Nullable + private AppTarget mDirectShareAppTarget; + + @Nullable + private DisplayResolveInfo mDisplayResolveInfo; + + @Nullable + private TargetHashProvider mHashProvider; + + @Nullable + private Intent mReferrerFillInIntent; + + private Intent mResolvedIntent; + private Intent mTargetIntent; + private TargetActivityStarter mActivityStarter; + private ResolveInfo mResolveInfo; + private CharSequence mDisplayLabel; + private CharSequence mExtendedInfo; + 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; + + /** + * Configure an {@link Intent} to be built in to the output target as the resolution for the + * requested target data. + */ + public Builder setResolvedIntent(Intent resolvedIntent) { + mResolvedIntent = resolvedIntent; + return this; + } + + /** + * Configure an {@link Intent} to be built in to the output as the "target intent." + */ + public Builder setTargetIntent(Intent targetIntent) { + mTargetIntent = targetIntent; + return this; + } + + /** + * Configure a fill-in intent provided by the referrer to be used in populating the launch + * intent if the output target is ever selected. + * + * @see android.content.Intent#fillIn(Intent, int) + */ + public Builder setReferrerFillInIntent(@Nullable Intent referrerFillInIntent) { + mReferrerFillInIntent = referrerFillInIntent; + return this; + } + + /** + * Configure a {@link ComponentName} to be built in to the output target, as the real + * component we were able to resolve on this device given the available target data. + */ + public Builder setResolvedComponentName(@Nullable ComponentName resolvedComponentName) { + mResolvedComponentName = resolvedComponentName; + return this; + } + + /** + * Configure a {@link ComponentName} to be built in to the output target, as the component + * supposedly associated with a {@link ChooserTarget} from which the builder data is being + * derived. + */ + public Builder setChooserTargetComponentName(@Nullable ComponentName componentName) { + mChooserTargetComponentName = componentName; + return this; + } + + /** Configure the {@link TargetActivityStarter} to be built in to the output target. */ + public Builder setActivityStarter(TargetActivityStarter activityStarter) { + mActivityStarter = activityStarter; + return this; + } + + /** Configure the {@link ResolveInfo} to be built in to the output target. */ + public Builder setResolveInfo(ResolveInfo resolveInfo) { + mResolveInfo = resolveInfo; + return this; + } + + /** Configure the display label to be built in to the output target. */ + public Builder setDisplayLabel(CharSequence displayLabel) { + mDisplayLabel = displayLabel; + return this; + } + + /** Configure the extended info to be built in to the output target. */ + public Builder setExtendedInfo(CharSequence extendedInfo) { + mExtendedInfo = extendedInfo; + return this; + } + + /** Configure the {@link IconHolder} to be built in to the output target. */ + public Builder setDisplayIconHolder(IconHolder displayIconHolder) { + mDisplayIconHolder = displayIconHolder; + return this; + } + + /** Configure the list of source intents to be built in to the output target. */ + public Builder setAllSourceIntents(List<Intent> sourceIntents) { + mSourceIntents = sourceIntents; + return this; + } + + /** Configure the list of display targets to be built in to the output target. */ + public Builder setAllDisplayTargets(List<DisplayResolveInfo> targets) { + mAllDisplayTargets = targets; + return this; + } + + /** Configure the is-suspended status to be built in to the output target. */ + public Builder setIsSuspended(boolean isSuspended) { + mIsSuspended = isSuspended; + return this; + } + + /** Configure the is-pinned status to be built in to the output target. */ + public Builder setIsPinned(boolean isPinned) { + mIsPinned = isPinned; + return this; + } + + /** Configure the modified score to be built in to the output target. */ + public Builder setModifiedScore(float modifiedScore) { + mModifiedScore = modifiedScore; + return this; + } + + /** Configure the {@link ShortcutInfo} to be built in to the output target. */ + public Builder setDirectShareShortcutInfo(@Nullable ShortcutInfo shortcutInfo) { + mDirectShareShortcutInfo = shortcutInfo; + return this; + } + + /** Configure the {@link AppTarget} to be built in to the output target. */ + public Builder setDirectShareAppTarget(@Nullable AppTarget appTarget) { + mDirectShareAppTarget = appTarget; + return this; + } + + /** Configure the {@link DisplayResolveInfo} to be built in to the output target. */ + public Builder setDisplayResolveInfo(@Nullable DisplayResolveInfo displayResolveInfo) { + mDisplayResolveInfo = displayResolveInfo; + return this; + } + + /** Configure the {@link TargetHashProvider} to be built in to the output target. */ + public Builder setHashProvider(@Nullable TargetHashProvider hashProvider) { + mHashProvider = hashProvider; + return this; + } + + Builder setLegacyType(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 { + baseIntentToSend = new Intent(baseIntentToSend); + if (fillInIntent != null) { + baseIntentToSend.fillIn(fillInIntent, fillInFlags); + } + if (mReferrerFillInIntent != null) { + baseIntentToSend.fillIn(mReferrerFillInIntent, 0); + } + } + + return new ImmutableTargetInfo( + baseIntentToSend, + mResolvedIntent, + mTargetIntent, + mReferrerFillInIntent, + mResolvedComponentName, + mChooserTargetComponentName, + mActivityStarter, + mResolveInfo, + mDisplayLabel, + mExtendedInfo, + mDisplayIconHolder, + mSourceIntents, + mAllDisplayTargets, + mIsSuspended, + mIsPinned, + mModifiedScore, + mDirectShareShortcutInfo, + mDirectShareAppTarget, + mDisplayResolveInfo, + mHashProvider, + mLegacyType); + } + + /** Construct an {@code ImmutableTargetInfo} with the current builder data. */ + public ImmutableTargetInfo build() { + return buildWithFillInIntent(null, 0); + } + } + + @Nullable + private final Intent mReferrerFillInIntent; + + @Nullable + private final ComponentName mResolvedComponentName; + + @Nullable + private final ComponentName mChooserTargetComponentName; + + @Nullable + private final ShortcutInfo mDirectShareShortcutInfo; + + @Nullable + private final AppTarget mDirectShareAppTarget; + + @Nullable + private final DisplayResolveInfo mDisplayResolveInfo; + + @Nullable + private final TargetHashProvider mHashProvider; + + private final Intent mBaseIntentToSend; + private final Intent mResolvedIntent; + 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; + private final float mModifiedScore; + private final LegacyTargetType mLegacyType; + + /** Construct a {@link Builder}. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** Construct a {@link Builder} pre-initialized to match this target. */ + public Builder toBuilder() { + return newBuilder() + .setResolvedIntent(getResolvedIntent()) + .setTargetIntent(getTargetIntent()) + .setReferrerFillInIntent(getReferrerFillInIntent()) + .setResolvedComponentName(getResolvedComponentName()) + .setChooserTargetComponentName(getChooserTargetComponentName()) + .setActivityStarter(mActivityStarter) + .setResolveInfo(getResolveInfo()) + .setDisplayLabel(getDisplayLabel()) + .setExtendedInfo(getExtendedInfo()) + .setDisplayIconHolder(getDisplayIconHolder()) + .setAllSourceIntents(getAllSourceIntents()) + .setAllDisplayTargets(getAllDisplayTargets()) + .setIsSuspended(isSuspended()) + .setIsPinned(isPinned()) + .setModifiedScore(getModifiedScore()) + .setDirectShareShortcutInfo(getDirectShareShortcutInfo()) + .setDirectShareAppTarget(getDirectShareAppTarget()) + .setDisplayResolveInfo(getDisplayResolveInfo()) + .setHashProvider(getHashProvider()) + .setLegacyType(mLegacyType); + } + + @VisibleForTesting + Intent getBaseIntentToSend() { + return mBaseIntentToSend; + } + + @Override + public ImmutableTargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return toBuilder().buildWithFillInIntent(fillInIntent, flags); + } + + @Override + public Intent getResolvedIntent() { + return mResolvedIntent; + } + + @Override + public Intent getTargetIntent() { + return mTargetIntent; + } + + @Nullable + public Intent getReferrerFillInIntent() { + return mReferrerFillInIntent; + } + + @Override + @Nullable + public ComponentName getResolvedComponentName() { + return mResolvedComponentName; + } + + @Override + @Nullable + public ComponentName getChooserTargetComponentName() { + return mChooserTargetComponentName; + } + + @Override + public boolean start(Activity activity, Bundle options) { + return mActivityStarter.start(this, activity, options); + } + + @Override + public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + return mActivityStarter.startAsCaller(this, activity, options, userId); + } + + @Override + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + return mActivityStarter.startAsUser(this, activity, options, user); + } + + @Override + public ResolveInfo getResolveInfo() { + return mResolveInfo; + } + + @Override + public CharSequence getDisplayLabel() { + return mDisplayLabel; + } + + @Override + public CharSequence getExtendedInfo() { + return mExtendedInfo; + } + + @Override + public IconHolder getDisplayIconHolder() { + return mDisplayIconHolder; + } + + @Override + public List<Intent> getAllSourceIntents() { + return mSourceIntents; + } + + @Override + public ArrayList<DisplayResolveInfo> getAllDisplayTargets() { + ArrayList<DisplayResolveInfo> targets = new ArrayList<>(); + targets.addAll(mAllDisplayTargets); + return targets; + } + + @Override + public boolean isSuspended() { + return mIsSuspended; + } + + @Override + public boolean isPinned() { + return mIsPinned; + } + + @Override + public float getModifiedScore() { + return mModifiedScore; + } + + @Override + @Nullable + public ShortcutInfo getDirectShareShortcutInfo() { + return mDirectShareShortcutInfo; + } + + @Override + @Nullable + public AppTarget getDirectShareAppTarget() { + return mDirectShareAppTarget; + } + + @Override + @Nullable + public DisplayResolveInfo getDisplayResolveInfo() { + return mDisplayResolveInfo; + } + + @Override + public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { + return (mHashProvider == null) + ? null : mHashProvider.getHashedTargetIdForMetrics(this, context); + } + + @VisibleForTesting + @Nullable + TargetHashProvider getHashProvider() { + return mHashProvider; + } + + @Override + public boolean isEmptyTargetInfo() { + return mLegacyType == LegacyTargetType.EMPTY_TARGET_INFO; + } + + @Override + public boolean isPlaceHolderTargetInfo() { + return mLegacyType == LegacyTargetType.PLACEHOLDER_TARGET_INFO; + } + + @Override + public boolean isNotSelectableTargetInfo() { + return isEmptyTargetInfo() || isPlaceHolderTargetInfo(); + } + + @Override + public boolean isSelectableTargetInfo() { + return mLegacyType == LegacyTargetType.SELECTABLE_TARGET_INFO; + } + + @Override + public boolean isChooserTargetInfo() { + return isNotSelectableTargetInfo() || isSelectableTargetInfo(); + } + + @Override + public boolean isMultiDisplayResolveInfo() { + return mLegacyType == LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO; + } + + @Override + public boolean isDisplayResolveInfo() { + return (mLegacyType == LegacyTargetType.DISPLAY_RESOLVE_INFO) + || isMultiDisplayResolveInfo(); + } + + private ImmutableTargetInfo( + Intent baseIntentToSend, + Intent resolvedIntent, + Intent targetIntent, + @Nullable Intent referrerFillInIntent, + @Nullable ComponentName resolvedComponentName, + @Nullable ComponentName chooserTargetComponentName, + TargetActivityStarter activityStarter, + ResolveInfo resolveInfo, + CharSequence displayLabel, + CharSequence extendedInfo, + IconHolder iconHolder, + @Nullable List<Intent> sourceIntents, + @Nullable List<DisplayResolveInfo> allDisplayTargets, + boolean isSuspended, + boolean isPinned, + float modifiedScore, + @Nullable ShortcutInfo directShareShortcutInfo, + @Nullable AppTarget directShareAppTarget, + @Nullable DisplayResolveInfo displayResolveInfo, + @Nullable TargetHashProvider hashProvider, + LegacyTargetType legacyType) { + mBaseIntentToSend = baseIntentToSend; + mResolvedIntent = resolvedIntent; + mTargetIntent = targetIntent; + mReferrerFillInIntent = referrerFillInIntent; + mResolvedComponentName = resolvedComponentName; + mChooserTargetComponentName = chooserTargetComponentName; + mActivityStarter = activityStarter; + mResolveInfo = resolveInfo; + mDisplayLabel = displayLabel; + mExtendedInfo = extendedInfo; + mDisplayIconHolder = iconHolder; + mSourceIntents = immutableCopyOrEmpty(sourceIntents); + mAllDisplayTargets = immutableCopyOrEmpty(allDisplayTargets); + mIsSuspended = isSuspended; + mIsPinned = isPinned; + mModifiedScore = modifiedScore; + mDirectShareShortcutInfo = directShareShortcutInfo; + mDirectShareAppTarget = directShareAppTarget; + mDisplayResolveInfo = displayResolveInfo; + mHashProvider = hashProvider; + mLegacyType = legacyType; + } + + private static <E> ImmutableList<E> immutableCopyOrEmpty(@Nullable List<E> source) { + return (source == null) ? ImmutableList.of() : ImmutableList.copyOf(source); + } +} diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt new file mode 100644 index 00000000..4d825f6b --- /dev/null +++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + *3 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.chooser + +import android.app.Activity +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ResolveInfo +import android.os.Bundle +import android.os.UserHandle +import com.android.intentresolver.createShortcutInfo +import com.android.intentresolver.mock +import com.android.intentresolver.ResolverActivity +import com.android.intentresolver.ResolverDataProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ImmutableTargetInfoTest { + private val resolvedIntent = Intent("resolved") + private val targetIntent = Intent("target") + private val referrerFillInIntent = Intent("referrer_fillin") + private val resolvedComponentName = ComponentName("resolved", "component") + private val chooserTargetComponentName = ComponentName("chooser", "target") + private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0) + private val displayLabel: CharSequence = "Display Label" + private val extendedInfo: CharSequence = "Extended Info" + private val displayIconHolder: TargetInfo.IconHolder = mock() + private val sourceIntent1 = Intent("source1") + private val sourceIntent2 = Intent("source2") + private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo( + Intent("display1"), + ResolverDataProvider.createResolveInfo(2, 0), + "display1 label", + "display1 extended info", + Intent("display1_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo( + Intent("display2"), + ResolverDataProvider.createResolveInfo(3, 0), + "display2 label", + "display2 extended info", + Intent("display2_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val directShareShortcutInfo = createShortcutInfo( + "shortcutid", ResolverDataProvider.createComponentName(4), 4) + private val directShareAppTarget = AppTarget( + AppTargetId("apptargetid"), + "test.directshare", + "target", + UserHandle.CURRENT) + private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( + Intent("displayresolve"), + ResolverDataProvider.createResolveInfo(5, 0), + "displayresolve label", + "displayresolve extended info", + Intent("display_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val hashProvider: ImmutableTargetInfo.TargetHashProvider = mock() + + @Test + fun testBasicProperties() { // Fields that are reflected back w/o logic. + // TODO: we could consider passing copies of all the values into the builder so that we can + // verify that they're not mutated (e.g. no extras added to the intents). For now that + // should be obvious from the implementation. + val info = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(resolvedIntent) + .setTargetIntent(targetIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .setResolvedComponentName(resolvedComponentName) + .setChooserTargetComponentName(chooserTargetComponentName) + .setResolveInfo(resolveInfo) + .setDisplayLabel(displayLabel) + .setExtendedInfo(extendedInfo) + .setDisplayIconHolder(displayIconHolder) + .setAllSourceIntents(listOf(sourceIntent1, sourceIntent2)) + .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) + .setIsSuspended(true) + .setIsPinned(true) + .setModifiedScore(42.0f) + .setDirectShareShortcutInfo(directShareShortcutInfo) + .setDirectShareAppTarget(directShareAppTarget) + .setDisplayResolveInfo(displayResolveInfo) + .setHashProvider(hashProvider) + .build() + + assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) + assertThat(info.targetIntent).isEqualTo(targetIntent) + assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) + assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) + assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) + assertThat(info.resolveInfo).isEqualTo(resolveInfo) + assertThat(info.displayLabel).isEqualTo(displayLabel) + assertThat(info.extendedInfo).isEqualTo(extendedInfo) + assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) + assertThat(info.allSourceIntents).containsExactly(sourceIntent1, sourceIntent2) + assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) + assertThat(info.isSuspended).isTrue() + assertThat(info.isPinned).isTrue() + assertThat(info.modifiedScore).isEqualTo(42.0f) + assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) + assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) + assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + assertThat(info.hashProvider).isEqualTo(hashProvider) + } + + @Test + fun testToBuilderPreservesBasicProperties() { + // Note this is set up exactly as in `testBasicProperties`, but the assertions will be made + // against a *copy* of the object instead. + val infoToCopyFrom = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(resolvedIntent) + .setTargetIntent(targetIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .setResolvedComponentName(resolvedComponentName) + .setChooserTargetComponentName(chooserTargetComponentName) + .setResolveInfo(resolveInfo) + .setDisplayLabel(displayLabel) + .setExtendedInfo(extendedInfo) + .setDisplayIconHolder(displayIconHolder) + .setAllSourceIntents(listOf(sourceIntent1, sourceIntent2)) + .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) + .setIsSuspended(true) + .setIsPinned(true) + .setModifiedScore(42.0f) + .setDirectShareShortcutInfo(directShareShortcutInfo) + .setDirectShareAppTarget(directShareAppTarget) + .setDisplayResolveInfo(displayResolveInfo) + .setHashProvider(hashProvider) + .build() + + val info = infoToCopyFrom.toBuilder().build() + + assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) + assertThat(info.targetIntent).isEqualTo(targetIntent) + assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) + assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) + assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) + assertThat(info.resolveInfo).isEqualTo(resolveInfo) + assertThat(info.displayLabel).isEqualTo(displayLabel) + assertThat(info.extendedInfo).isEqualTo(extendedInfo) + assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) + assertThat(info.allSourceIntents).containsExactly(sourceIntent1, sourceIntent2) + assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) + assertThat(info.isSuspended).isTrue() + assertThat(info.isPinned).isTrue() + assertThat(info.modifiedScore).isEqualTo(42.0f) + assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) + assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) + assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + assertThat(info.hashProvider).isEqualTo(hashProvider) + } + + @Test + fun testBaseIntentToSend_defaultsToResolvedIntent() { + val info = ImmutableTargetInfo.newBuilder().setResolvedIntent(resolvedIntent).build() + assertThat(info.baseIntentToSend.filterEquals(resolvedIntent)).isTrue() + } + + @Test + fun testBaseIntentToSend_fillsInFromReferrerIntent() { + val originalIntent = Intent() + originalIntent.setPackage("original") + + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + referrerFillInIntent.setPackage("referrer") + + val info = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Only fill if empty. + assertThat(info.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN") + } + + @Test + fun testBaseIntentToSend_fillsInFromCloneRequestIntent() { + val originalIntent = Intent() + originalIntent.setPackage("original") + + val cloneFillInIntent = Intent("CLONE_FILL_IN") + cloneFillInIntent.setPackage("clone") + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .build() + val info = originalInfo.cloneFilledIn(cloneFillInIntent, 0) + + assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Only fill if empty. + assertThat(info.baseIntentToSend.action).isEqualTo("CLONE_FILL_IN") + } + + @Test + fun testBaseIntentToSend_twoFillInSourcesFavorsCloneRequest() { + val originalIntent = Intent() + originalIntent.setPackage("original") + + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + referrerFillInIntent.setPackage("referrer_pkg") + referrerFillInIntent.setType("test/referrer") + + val infoWithReferrerFillIn = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + val cloneFillInIntent = Intent("CLONE_FILL_IN") + cloneFillInIntent.setPackage("clone") + + val info = infoWithReferrerFillIn.cloneFilledIn(cloneFillInIntent, 0) + + assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Set all along. + assertThat(info.baseIntentToSend.action).isEqualTo("CLONE_FILL_IN") // Clone wins. + assertThat(info.baseIntentToSend.type).isEqualTo("test/referrer") // Left for referrer. + } + + @Test + fun testBaseIntentToSend_doubleCloningPreservesReferrerFillInButNotOriginalCloneFillIn() { + val originalIntent = Intent() + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + val cloneFillInIntent1 = Intent() + cloneFillInIntent1.setPackage("clone1") + val cloneFillInIntent2 = Intent() + cloneFillInIntent2.setType("test/clone2") + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + val clone1 = originalInfo.cloneFilledIn(cloneFillInIntent1, 0) + val clone2 = clone1.cloneFilledIn(cloneFillInIntent2, 0) // Clone-of-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 + // field is empty (thus able to be populated by filling-in). + assertThat(clone1.baseIntentToSend.type).isNull() + assertThat(clone2.baseIntentToSend.getPackage()).isNull() + } + + @Test + fun testLegacySubclassRelationships_empty() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.EMPTY_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isTrue() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isTrue() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_placeholder() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.PLACEHOLDER_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isTrue() + assertThat(info.isNotSelectableTargetInfo).isTrue() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_selectable() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.SELECTABLE_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isTrue() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_displayResolveInfo() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.DISPLAY_RESOLVE_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isTrue() + } + + @Test + fun testLegacySubclassRelationships_multiDisplayResolveInfo() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isTrue() + assertThat(info.isDisplayResolveInfo).isTrue() + } + + @Test + fun testActivityStarter_correctNumberOfInvocations_start() { + val activityStarter = object : TestActivityStarter() { + override fun startAsCaller( + target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { + throw RuntimeException("Wrong API used: startAsCaller") + } + + override fun startAsUser( + target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle + ): Boolean { + throw RuntimeException("Wrong API used: startAsUser") + } + } + + val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val activity: Activity = mock() + val options = Bundle() + options.putInt("TEST_KEY", 1) + + info.start(activity, options) + + assertThat(activityStarter.totalInvocations).isEqualTo(1) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) + assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) + assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) + assertThat(activityStarter.lastInvocationUserId).isNull() + assertThat(activityStarter.lastInvocationAsCaller).isFalse() + } + + @Test + fun testActivityStarter_correctNumberOfInvocations_startAsCaller() { + val activityStarter = object : TestActivityStarter() { + override fun start(target: TargetInfo, activity: Activity, options: Bundle): Boolean { + throw RuntimeException("Wrong API used: start") + } + + override fun startAsUser( + target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle + ): Boolean { + throw RuntimeException("Wrong API used: startAsUser") + } + } + + val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val activity: ResolverActivity = mock() + val options = Bundle() + options.putInt("TEST_KEY", 1) + + info.startAsCaller(activity, options, 42) + + assertThat(activityStarter.totalInvocations).isEqualTo(1) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) + assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) + assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) + assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) + assertThat(activityStarter.lastInvocationAsCaller).isTrue() + } + + @Test + fun testActivityStarter_correctNumberOfInvocations_startAsUser() { + val activityStarter = object : TestActivityStarter() { + override fun start(target: TargetInfo, activity: Activity, options: Bundle): Boolean { + throw RuntimeException("Wrong API used: start") + } + + override fun startAsCaller( + target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { + throw RuntimeException("Wrong API used: startAsCaller") + } + } + + val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val activity: Activity = mock() + val options = Bundle() + options.putInt("TEST_KEY", 1) + + info.startAsUser(activity, options, UserHandle.of(42)) + + assertThat(activityStarter.totalInvocations).isEqualTo(1) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) + assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) + assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) + assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) + assertThat(activityStarter.lastInvocationAsCaller).isFalse() + } + + @Test + fun testActivityStarter_invokedWithRespectiveTargetInfoAfterCopy() { + val activityStarter = TestActivityStarter() + val info1 = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val info2 = info1.toBuilder().build() + + info1.start(mock(), Bundle()) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info1) + info2.start(mock(), Bundle()) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) + info2.startAsCaller(mock(), Bundle(), 42) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) + info2.startAsUser(mock(), Bundle(), UserHandle.of(42)) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) + + assertThat(activityStarter.totalInvocations).isEqualTo(4) // Instance is still shared. + } +} + +private open class TestActivityStarter : ImmutableTargetInfo.TargetActivityStarter { + var totalInvocations = 0 + var lastInvocationTargetInfo: TargetInfo? = null + var lastInvocationActivity: Activity? = null + var lastInvocationOptions: Bundle? = null + var lastInvocationUserId: Integer? = null + var lastInvocationAsCaller = false + + override fun start(target: TargetInfo, activity: Activity, options: Bundle): Boolean { + ++totalInvocations + lastInvocationTargetInfo = target + lastInvocationActivity = activity + lastInvocationOptions = options + lastInvocationUserId = null + lastInvocationAsCaller = false + return true + } + + override fun startAsCaller( + target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { + ++totalInvocations + lastInvocationTargetInfo = target + lastInvocationActivity = activity + lastInvocationOptions = options + lastInvocationUserId = Integer(userId) + lastInvocationAsCaller = true + return true + } + + override fun startAsUser( + target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle): Boolean { + ++totalInvocations + lastInvocationTargetInfo = target + lastInvocationActivity = activity + lastInvocationOptions = options + lastInvocationUserId = Integer(user.identifier) + lastInvocationAsCaller = false + return true + } +} |