Refactor ChooserRequestParameters usage Creates ChooserRequest data class Uses validation lib to implement parsing of source data Introduces ChooserViewModel as a new target to begin migration of control flow, data and dependencies out of ChooserActivity and into smaller testable units. Test: atest IntentResolver-tests-activity:com.android.intentresolver.v2 Bug: 309960444 Change-Id: I39b3517ec9e17525441d349b3da139ad5956c600
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt index 10ee5af..4c781a4 100644 --- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -19,14 +19,10 @@ import android.content.Intent import androidx.annotation.MainThread import androidx.lifecycle.ViewModel -import com.android.intentresolver.ChooserRequestParameters /** A contract for the preview view model. Added for testing. */ abstract class BasePreviewViewModel : ViewModel() { - @MainThread - abstract fun createOrReuseProvider( - targetIntent: Intent - ): PreviewDataProvider + @MainThread abstract fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider @MainThread abstract fun createOrReuseImageLoader(): ImageLoader }
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 6350756..9acc468 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras -import com.android.intentresolver.ChooserRequestParameters import com.android.intentresolver.R import com.android.intentresolver.inject.Background import dagger.hilt.android.lifecycle.HiltViewModel @@ -45,9 +44,7 @@ private var imageLoader: ImagePreviewImageLoader? = null @MainThread - override fun createOrReuseProvider( - targetIntent: Intent - ): PreviewDataProvider = + override fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider = previewDataProvider ?: PreviewDataProvider( viewModelScope + dispatcher,
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt index 7062da3..b968641 100644 --- a/java/src/com/android/intentresolver/v2/ActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
@@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 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.v2 import android.content.Intent @@ -18,8 +33,6 @@ interface ActivityLogic : CommonActivityLogic { /** The intent for the target. This will always come before additional targets, if any. */ val targetIntent: Intent - /** Whether the intent is for home. */ - val resolvingHome: Boolean /** Custom title to display. */ val title: CharSequence? /** Resource ID for the title to display when there is no custom title. */
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index e093058..a71de19 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2024 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. @@ -95,7 +95,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.SavedStateHandleSupport; import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.viewmodel.CreationExtras; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; @@ -104,7 +106,6 @@ import com.android.intentresolver.ChooserGridLayoutManager; import com.android.intentresolver.ChooserListAdapter; import com.android.intentresolver.ChooserRefinementManager; -import com.android.intentresolver.ChooserRequestParameters; import com.android.intentresolver.ChooserStackedAppDialogFragment; import com.android.intentresolver.ChooserTargetActionsDialogFragment; import com.android.intentresolver.EnterTransitionAnimationDelegate; @@ -147,6 +148,9 @@ import com.android.intentresolver.v2.platform.ImageEditor; import com.android.intentresolver.v2.platform.NearbyShare; import com.android.intentresolver.v2.ui.ActionTitle; +import com.android.intentresolver.v2.ui.model.CallerInfo; +import com.android.intentresolver.v2.ui.model.ChooserRequest; +import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel; import com.android.intentresolver.widget.ImagePreviewView; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; @@ -311,31 +315,46 @@ private boolean mFinishWhenStopped = false; private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); + private ChooserViewModel mViewModel; @VisibleForTesting - protected ActivityLogic createActivityLogic() { + protected ChooserActivityLogic createActivityLogic(ChooserRequest chooserRequest) { return new ChooserActivityLogic( TAG, /* activity = */ this, - this::onWorkProfileStatusUpdated); + this::onWorkProfileStatusUpdated, + chooserRequest); + } + + @NonNull + @Override + public CreationExtras getDefaultViewModelCreationExtras() { + CreationExtras extras = super.getDefaultViewModelCreationExtras(); + // Inserts a CallerInfo into the Bundle at stored at DEFAULT_ARGS_KEY + Bundle defaultArgs = requireNonNull(extras.get(SavedStateHandleSupport.DEFAULT_ARGS_KEY)); + defaultArgs.putParcelable(CallerInfo.SAVED_STATE_HANDLE_KEY, + new CallerInfo(getLaunchedFromUid(), + getLaunchedFromPackage(), + requireNonNull(getReferrer()))); + return extras; } @Override protected final void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - if (isFinishing()) { - // Performing a clean exit: - // Skip initializing any additional resources. + setTheme(R.style.Theme_DeviceDefault_Chooser); + Tracer.INSTANCE.markLaunched(); + mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); + if (!mViewModel.init()) { + finish(); return; } - setTheme(R.style.Theme_DeviceDefault_Chooser); - mLogic = createActivityLogic(); - Tracer.INSTANCE.markLaunched(); + mLogic = createActivityLogic(mViewModel.getChooserRequest()); + init(); } - @Override - protected final void onPostCreate(@Nullable Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); + private void init() { mIntentReceivedTime.set(System.currentTimeMillis()); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); @@ -345,21 +364,16 @@ mShouldDisplayLandscape = shouldDisplayLandscape(getResources().getConfiguration().orientation); - ChooserRequestParameters chooserRequest = getChooserRequest(); - if (chooserRequest == null) { - finish(); - return; - } - + ChooserRequest chooserRequest = mViewModel.getChooserRequest(); setRetainInOnStop(chooserRequest.shouldRetainInOnStop()); createProfileRecords( new AppPredictorFactory( this, - chooserRequest.getSharedText(), - chooserRequest.getTargetIntentFilter(), + Objects.toString(chooserRequest.getSharedText(), null), + chooserRequest.getShareTargetFilter(), mAppPredictionAvailable ), - chooserRequest.getTargetIntentFilter() + chooserRequest.getShareTargetFilter() ); Intent intent = mLogic.getTargetIntent(); @@ -493,8 +507,7 @@ mLogic.getReferrerPackageName(), chooserRequest.getTargetType(), chooserRequest.getCallerChooserTargets().size(), - (chooserRequest.getInitialIntents() == null) - ? 0 : chooserRequest.getInitialIntents().length, + chooserRequest.getInitialIntents().size(), isWorkProfile(), mChooserContentPreviewUi.getPreferredContentPreview(), chooserRequest.getTargetAction(), @@ -502,8 +515,6 @@ chooserRequest.getModifyShareAction() != null ); mEnterTransitionAnimationDelegate.postponeTransition(); - - restore(savedInstanceState); } private void restore(@Nullable Bundle savedInstanceState) { @@ -1151,15 +1162,6 @@ ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// - @Nullable - private ChooserRequestParameters getChooserRequest() { - return ((ChooserActivityLogic) mLogic).getChooserRequestParameters(); - } - - private ChooserRequestParameters requireChooserRequest() { - return requireNonNull(getChooserRequest()); - } - private AnnotatedUserHandles requireAnnotatedUserHandles() { return requireNonNull(mLogic.getAnnotatedUserHandles()); } @@ -1234,7 +1236,7 @@ } protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean isSendAction = requireChooserRequest().isSendActionTarget(); + final boolean isSendAction = mViewModel.getChooserRequest().isSendActionTarget(); final EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState( @@ -1504,7 +1506,7 @@ } final Intent intent = getIntent(); if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() - && !mLogic.getResolvingHome() && !mRetainInOnStop) { + && !mRetainInOnStop) { // This resolver is in the unusual situation where it has been // launched at the top of a new task. We don't let it be added // to the recent tasks shown to the user, and we need to make sure @@ -1550,10 +1552,7 @@ @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { - ChooserRequestParameters chooserRequest = getChooserRequest(); - if (chooserRequest == null) { - return defIntent; - } + ChooserRequest chooserRequest = mViewModel.getChooserRequest(); Intent result = defIntent; if (chooserRequest.getReplacementExtras() != null) { @@ -1578,7 +1577,7 @@ } public void onActivityStarted(TargetInfo cti) { - ChooserRequestParameters chooserRequest = requireChooserRequest(); + ChooserRequest chooserRequest = mViewModel.getChooserRequest(); if (chooserRequest.getChosenComponentSender() != null) { final ComponentName target = cti.getResolvedComponentName(); if (target != null) { @@ -1595,7 +1594,7 @@ } private void addCallerChooserTargets() { - ChooserRequestParameters chooserRequest = requireChooserRequest(); + ChooserRequest chooserRequest = mViewModel.getChooserRequest(); if (!chooserRequest.getCallerChooserTargets().isEmpty()) { // Send the caller's chooser targets only to the default profile. if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) { @@ -1637,8 +1636,9 @@ // TODO: implement these type-conditioned behaviors polymorphically, and consider moving // the logic into `ChooserTargetActionsDialogFragment.show()`. boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned(); - IntentFilter intentFilter = targetInfo.isSelectableTargetInfo() - ? requireChooserRequest().getTargetIntentFilter() : null; + IntentFilter intentFilter; + intentFilter = targetInfo.isSelectableTargetInfo() + ? mViewModel.getChooserRequest().getShareTargetFilter() : null; String shortcutTitle = targetInfo.isSelectableTargetInfo() ? targetInfo.getDisplayLabel().toString() : null; String shortcutIdKey = targetInfo.getDirectShareShortcutId(); @@ -1658,7 +1658,7 @@ protected boolean onTargetSelected(TargetInfo target) { if (mRefinementManager.maybeHandleSelection( target, - requireChooserRequest().getRefinementIntentSender(), + mViewModel.getChooserRequest().getRefinementIntentSender(), getApplication(), getMainThreadHandler())) { return false; @@ -1732,7 +1732,7 @@ targetInfo.getResolveInfo().activityInfo.processName, which, /* directTargetAlsoRanked= */ getRankedPosition(targetInfo), - requireChooserRequest().getCallerChooserTargets().size(), + mViewModel.getChooserRequest().getCallerChooserTargets().size(), targetInfo.getHashedTargetIdForMetrics(this), targetInfo.isPinned(), mIsSuccessfullySelected, @@ -1839,7 +1839,7 @@ if (targetIntent == null) { return; } - Intent originalTargetIntent = new Intent(requireChooserRequest().getTargetIntent()); + Intent originalTargetIntent = new Intent(mViewModel.getChooserRequest().getTargetIntent()); // Our TargetInfo implementations add associated component to the intent, let's do the same // for the sake of the comparison below. if (targetIntent.getComponent() != null) { @@ -1938,7 +1938,7 @@ @Override public boolean isComponentFiltered(ComponentName name) { - return requireChooserRequest().getFilteredComponentNames().contains(name); + return mViewModel.getChooserRequest().getFilteredComponentNames().contains(name); } @Override @@ -1955,7 +1955,7 @@ List<ResolveInfo> rList, boolean filterLastUsed, UserHandle userHandle) { - ChooserRequestParameters parameters = requireChooserRequest(); + ChooserRequest parameters = mViewModel.getChooserRequest(); ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, @@ -2104,11 +2104,11 @@ } private ChooserActionFactory createChooserActionFactory() { - ChooserRequestParameters request = requireChooserRequest(); + ChooserRequest request = mViewModel.getChooserRequest(); return new ChooserActionFactory( this, request.getTargetIntent(), - request.getReferrerPackageName(), + request.getLaunchedFromPackage(), request.getChooserActions(), request.getModifyShareAction(), mImageEditor, @@ -2473,7 +2473,7 @@ * @return true if we want to show the content preview area */ protected boolean shouldShowContentPreview() { - ChooserRequestParameters chooserRequest = getChooserRequest(); + ChooserRequest chooserRequest = mViewModel.getChooserRequest(); return (chooserRequest != null) && chooserRequest.isSendActionTarget(); }
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt index a8150f5..f605488 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
@@ -1,11 +1,9 @@ package com.android.intentresolver.v2 -import android.app.Activity import android.content.Intent -import android.util.Log import androidx.activity.ComponentActivity import androidx.annotation.OpenForTesting -import com.android.intentresolver.ChooserRequestParameters +import com.android.intentresolver.v2.ui.model.ChooserRequest private const val TAG = "ChooserActivityLogic" @@ -13,14 +11,14 @@ * Activity logic for [ChooserActivity]. * * TODO: Make this class no longer open once [ChooserActivity] no longer needs to cast to access - * [chooserRequestParameters]. For now, this class being open is better than using reflection - * there. + * [chooserRequest]. For now, this class being open is better than using reflection there. */ @OpenForTesting open class ChooserActivityLogic( tag: String, activity: ComponentActivity, - onWorkProfileStatusUpdated: () -> Unit + onWorkProfileStatusUpdated: () -> Unit, + private val chooserRequest: ChooserRequest? = null, ) : ActivityLogic, CommonActivityLogic by CommonActivityLogicImpl( @@ -29,30 +27,16 @@ onWorkProfileStatusUpdated, ) { - val chooserRequestParameters: ChooserRequestParameters? = - try { - ChooserRequestParameters( - (activity as Activity).intent, - referrerPackageName, - (activity as Activity).referrer, - ) - } catch (e: IllegalArgumentException) { - Log.e(tag, "Caller provided invalid Chooser request parameters", e) - null - } + override val targetIntent: Intent = chooserRequest?.targetIntent ?: Intent() - override val targetIntent: Intent = chooserRequestParameters?.targetIntent ?: Intent() + override val title: CharSequence? = chooserRequest?.title - override val resolvingHome: Boolean = false + override val defaultTitleResId: Int = chooserRequest?.defaultTitleResource ?: 0 - override val title: CharSequence? = chooserRequestParameters?.title - - override val defaultTitleResId: Int = chooserRequestParameters?.defaultTitleResource ?: 0 - - override val initialIntents: List<Intent>? = chooserRequestParameters?.initialIntents?.toList() + override val initialIntents: List<Intent>? = chooserRequest?.initialIntents?.toList() override val payloadIntents: List<Intent> = buildList { add(targetIntent) - chooserRequestParameters?.additionalTargets?.let { addAll(it) } + chooserRequest?.additionalTargets?.let { addAll(it) } } }
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 9672e9d..0e526b4 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java
@@ -106,6 +106,7 @@ import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.ext.IntentExtKt; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; @@ -141,6 +142,7 @@ protected ActivityLogic mLogic; protected TargetDataLoader mTargetDataLoader; + private boolean mResolvingHome; private Button mAlwaysButton; private Button mOnceButton; @@ -223,7 +225,7 @@ } @VisibleForTesting - protected ActivityLogic createActivityLogic() { + protected ResolverActivityLogic createActivityLogic() { return new ResolverActivityLogic( TAG, /* activity = */ this, @@ -235,6 +237,7 @@ super.onCreate(savedInstanceState); setTheme(R.style.Theme_DeviceDefault_Resolver); mLogic = createActivityLogic(); + mResolvingHome = IntentExtKt.isHomeIntent(getIntent()); mTargetDataLoader = new DefaultTargetDataLoader( this, getLifecycle(), @@ -242,11 +245,6 @@ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, /* defaultValue = */ false) ); - } - - @Override - protected final void onPostCreate(@Nullable Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); init(); restore(savedInstanceState); } @@ -486,7 +484,7 @@ } final Intent intent = getIntent(); if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction() - && !mLogic.getResolvingHome()) { + && !mResolvingHome) { // This resolver is in the unusual situation where it has been // launched at the top of a new task. We don't let it be added // to the recent tasks shown to the user, and we need to make sure @@ -532,7 +530,7 @@ } ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter() .resolveInfoForPosition(which, hasIndexBeenFiltered); - if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) { + if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString(); Toast.makeText(this, mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName), @@ -1133,7 +1131,7 @@ } protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { - final ActionTitle title = mLogic.getResolvingHome() + final ActionTitle title = mResolvingHome ? ActionTitle.HOME : ActionTitle.forAction(intent.getAction()); @@ -1198,7 +1196,6 @@ @Override protected final void onStart() { super.onStart(); - this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); if (hasWorkProfile()) { mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this);
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt index cf84304..1335304 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
@@ -36,10 +36,6 @@ intent } - override val resolvingHome: Boolean = - targetIntent.action == Intent.ACTION_MAIN && - targetIntent.categories.singleOrNull() == Intent.CATEGORY_HOME - override val title: CharSequence? = null override val defaultTitleResId: Int = 0
diff --git a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt new file mode 100644 index 0000000..7aa8e03 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt
@@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 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.v2.ext + +import android.content.Intent +import java.util.function.Predicate + +/** Applies an operation on this Intent if matches the given filter. */ +inline fun Intent.ifMatch( + predicate: Predicate<Intent>, + crossinline block: Intent.() -> Unit +): Intent { + if (predicate.test(this)) { + apply(block) + } + return this +} + +/** True if the Intent has one of the specified actions. */ +fun Intent.hasAction(vararg actions: String): Boolean = action in actions + +/** True if the Intent has a single matching category. */ +fun Intent.hasSingleCategory(category: String) = categories.singleOrNull() == category + +/** True if the Intent resolves to the special Home (Launcher) component */ +fun Intent.isHomeIntent() = hasAction(Intent.ACTION_MAIN) && hasSingleCategory(Intent.CATEGORY_HOME)
diff --git a/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt b/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt new file mode 100644 index 0000000..9addeef --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt
@@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 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.v2.ui.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +data class CallerInfo( + val launchedFromUid: Int, + val launchedFomPackage: String?, + /* logged to metrics, forwarded to outgoing intent */ + val referrer: Uri +) : Parcelable { + constructor( + source: Parcel + ) : this( + launchedFromUid = source.readInt(), + launchedFomPackage = source.readString(), + checkNotNull(source.readParcelable()) + ) + + override fun describeContents() = 0 /* flags */ + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeInt(launchedFromUid) + dest.writeString(launchedFomPackage) + dest.writeParcelable(referrer, 0) + } + + companion object { + const val SAVED_STATE_HANDLE_KEY = "com.android.intentresolver.CALLER_INFO" + + @JvmStatic + @Suppress("unused") + val CREATOR = + object : Parcelable.Creator<CallerInfo> { + override fun newArray(size: Int) = arrayOfNulls<CallerInfo>(size) + override fun createFromParcel(source: Parcel) = CallerInfo(source) + } + } +} + +inline fun <reified T> Parcel.readParcelable(): T? { + return readParcelable(T::class.java.classLoader, T::class.java) +}
diff --git a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt new file mode 100644 index 0000000..2fbf94a --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt
@@ -0,0 +1,180 @@ +/* + * Copyright (C) 2024 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.v2.ui.model + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.IntentFilter +import android.content.IntentSender +import android.os.Bundle +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import androidx.annotation.StringRes +import com.android.intentresolver.v2.ext.hasAction + +const val MAX_CHOOSER_ACTIONS = 5 +const val MAX_INITIAL_INTENTS = 2 + +/** All of the things that are consumed from an incoming share Intent (+Extras). */ +data class ChooserRequest( + /** Required. Represents the content being sent. */ + val targetIntent: Intent, + + /** The action from [targetIntent] as retrieved with [Intent.getAction]. */ + val targetAction: String?, + + /** + * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the + * canonical "Share" actions. When handling other actions, this flag controls behavioral and + * visual changes. + */ + val isSendActionTarget: Boolean, + + /** The top-level content type as retrieved using [Intent.getType]. */ + val targetType: String?, + + /** The package name of the app which started the current activity instance. */ + val launchedFromPackage: String, + + /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */ + val title: CharSequence? = null, + + /** A String resource ID to load when [title] is null. */ + @get:StringRes val defaultTitleResource: Int = 0, + + /** + * An empty intent which carries an extra of [Intent.EXTRA_REFERRER]. To be merged with outgoing + * intents. This provides the original referrer value to the target. + */ + val referrerFillInIntent: Intent, + + /** + * Choices to exclude from results. + * + * Any resolved intents with a component in this list will be omitted before presentation. + */ + val filteredComponentNames: List<ComponentName> = emptyList(), + + /** + * App provided shortcut share intents (aka "direct share targets") + * + * Normally share shortcuts are published and consumed using + * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow + * apps to directly inject the same information. + * + * Historical note: This option was initially integrated with other results from the + * ChooserTargetService API (since deprecated and removed), hence the name and data format. + * These are more correctly called "Share Shortcuts" now. + */ + val callerChooserTargets: List<ChooserTarget> = emptyList(), + + /** + * Actions the user may perform. These are presented as separate affordances from the main list + * of choices. Selecting a choice is a terminal action which results in finishing. The item + * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate. + */ + val chooserActions: List<ChooserAction> = emptyList(), + + /** + * An action to start an Activity which for user updating of shared content. Selection is a + * terminal action, closing the current activity and launching the target of the action. + */ + val modifyShareAction: ChooserAction? = null, + + /** + * When false the host activity will be [finished][android.app.Activity.finish] when stopped. + */ + @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false, + + /** + * Intents which contain alternate representations of the content being shared. Any results from + * resolving these _alternate_ intents are included with the results of the primary intent as + * additional choices (e.g. share as image content vs. link to content). + */ + val additionalTargets: List<Intent> = emptyList(), + + /** + * Alternate [extras][Intent.getExtras] to substitute when launching a selected app. + * + * For a given app (by package name), the Bundle describes what parameters to substitute when + * that app is selected. + * + * // TODO: Map<String, Bundle> + */ + val replacementExtras: Bundle? = null, + + /** + * App-supplied choices to be presented first in the list. + * + * Custom labels and icons may be supplied using + * [LabeledIntent][android.content.pm.LabeledIntent]. + * + * Limit 2. + */ + val initialIntents: List<Intent> = emptyList(), + + /** + * Provides for callers to be notified when a component is selected. + * + * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the + * [ComponentName] of the item. + */ + val chosenComponentSender: IntentSender? = null, + + /** + * Provides a mechanism for callers to post-process a target when a selection is made. + * + * The received intent will contain: + * * **EXTRA_INTENT** The chosen target + * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target + * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a + * mechanism for the caller to return information. An updated intent to send must be included + * as [Intent.EXTRA_INTENT]. + */ + val refinementIntentSender: IntentSender? = null, + + /** + * Contains the text content to share supplied by the source app. + * + * TODO: Constrain length? + */ + val sharedText: CharSequence? = null, + + /** + * Supplied to + * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to + * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType] + * are considered for matching share shortcuts currently. + */ + val shareTargetFilter: IntentFilter? = null +) { + + /** Constructs an instance from only the required values. */ + constructor( + targetIntent: Intent, + referrerPackageName: String + ) : this( + targetIntent, + targetIntent.action, + targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), + targetIntent.type, + referrerPackageName, + referrerFillInIntent = + Intent().apply { putExtra(Intent.EXTRA_REFERRER, referrerPackageName) } + ) +}
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt new file mode 100644 index 0000000..6878be5 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
@@ -0,0 +1,157 @@ +/* + * Copyright (C) 2024 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.v2.ui.viewmodel + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.Intent.EXTRA_ALTERNATE_INTENTS +import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS +import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION +import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_TARGETS +import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER +import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS +import android.content.Intent.EXTRA_INITIAL_INTENTS +import android.content.Intent.EXTRA_INTENT +import android.content.Intent.EXTRA_REFERRER +import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS +import android.content.Intent.EXTRA_TEXT +import android.content.Intent.EXTRA_TITLE +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT +import android.content.IntentFilter +import android.content.IntentSender +import android.os.Bundle +import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget +import com.android.intentresolver.ChooserActivity +import com.android.intentresolver.R +import com.android.intentresolver.util.hasValidIcon +import com.android.intentresolver.v2.ext.hasAction +import com.android.intentresolver.v2.ext.ifMatch +import com.android.intentresolver.v2.ui.model.CallerInfo +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.ui.model.MAX_CHOOSER_ACTIONS +import com.android.intentresolver.v2.ui.model.MAX_INITIAL_INTENTS +import com.android.intentresolver.v2.validation.types.IntentOrUri +import com.android.intentresolver.v2.validation.types.array +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +private fun Intent.hasSendAction() = hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) + +internal fun Intent.maybeAddSendActionFlags() = + ifMatch(Intent::hasSendAction) { + addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + } + +fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) = + validateFrom(source) { + val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags() + + val isSendAction = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) + + val additionalTargets = + optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } + ?: emptyList() + + val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS)) + + val (customTitle, defaultTitleResource) = + if (isSendAction) { + ignored( + value<CharSequence>(EXTRA_TITLE), + "deprecated in P. You may wish to set a preview title by using EXTRA_TITLE " + + "property of the wrapped EXTRA_INTENT." + ) + null to R.string.chooseActivity + } else { + val custom = optional(value<CharSequence>(EXTRA_TITLE)) + custom to (custom?.let { 0 } ?: R.string.chooseActivity) + } + + val initialIntents = + optional(array<Intent>(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map { + it.maybeAddSendActionFlags() + } + ?: emptyList() + + val chosenComponentSender = + optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER)) + + val refinementIntentSender = + optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + + val filteredComponents = + optional(array<ComponentName>(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList() + + @Suppress("DEPRECATION") + val callerChooserTargets = + optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) ?: emptyList() + + val retainInOnStop = + optional(value<Boolean>(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false + + val sharedText = optional(value<CharSequence>(EXTRA_TEXT)) + + val chooserActions = + optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS)) + ?.filter { hasValidIcon(it) } + ?.take(MAX_CHOOSER_ACTIONS) + ?: emptyList() + + val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) + + val referrerFillIn = Intent().putExtra(EXTRA_REFERRER, callerInfo.referrer) + + ChooserRequest( + targetIntent = targetIntent, + targetAction = targetIntent.action, + isSendActionTarget = isSendAction, + targetType = targetIntent.type, + launchedFromPackage = + requireNotNull(callerInfo.launchedFomPackage) { + "launchedFromPackage was null, See Activity.getLaunchedFromPackage()" + }, + title = customTitle, + defaultTitleResource = defaultTitleResource, + referrerFillInIntent = referrerFillIn, + filteredComponentNames = filteredComponents, + callerChooserTargets = callerChooserTargets, + chooserActions = chooserActions, + modifyShareAction = modifyShareAction, + shouldRetainInOnStop = retainInOnStop, + additionalTargets = additionalTargets, + replacementExtras = replacementExtras, + initialIntents = initialIntents, + chosenComponentSender = chosenComponentSender, + refinementIntentSender = refinementIntentSender, + sharedText = sharedText, + shareTargetFilter = targetIntent.toShareTargetFilter() + ) + } + +private fun Intent.toShareTargetFilter(): IntentFilter? { + return type?.let { + IntentFilter().apply { + action?.also { addAction(it) } + addDataType(it) + } + } +}
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt new file mode 100644 index 0000000..663235c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
@@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.v2.ui.viewmodel + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.android.intentresolver.v2.ui.model.CallerInfo +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.ValidationResult +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +private const val TAG = "ChooserViewModel" + +@HiltViewModel +class ChooserViewModel +@Inject +constructor( + private val args: SavedStateHandle, +) : ViewModel() { + + private val callerInfo: CallerInfo = + requireNotNull(args[CallerInfo.SAVED_STATE_HANDLE_KEY]) { + "CallerInfo missing in SavedStateHandle! (${CallerInfo.SAVED_STATE_HANDLE_KEY})" + } + + /** The result of reading and validating the inputs provided in savedState. */ + private val status: ValidationResult<ChooserRequest> = readChooserRequest(callerInfo, args::get) + + val chooserRequest: ChooserRequest by lazy { status.getOrThrow() } + + fun init(): Boolean { + Log.i(TAG, "viewModel init") + if (!status.isSuccess()) { + status.reportToLogcat(TAG) + return false + } + Log.i(TAG, "request = $chooserRequest") + return true + } +}
diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt index 092cabe..856a521 100644 --- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
@@ -26,7 +26,7 @@ fun getOrThrow(): T = checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } - fun <T> reportToLogcat(tag: String) { + fun reportToLogcat(tag: String) { findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } } }
diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java index 0b26890..e7c8cce 100644 --- a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
@@ -40,6 +40,7 @@ import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.intentresolver.v2.ui.model.ChooserRequest; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.List; @@ -54,12 +55,14 @@ private UsageStatsManager mUsm; @Override - protected final ActivityLogic createActivityLogic() { + protected final ChooserActivityLogic createActivityLogic(ChooserRequest chooserRequest) { return new TestChooserActivityLogic( - "ChooserWrapper", - /* activity = */ this, - this::onWorkProfileStatusUpdated, - sOverrides); + "ChooserWrapper", + /* activity = */ this, + this::onWorkProfileStatusUpdated, + chooserRequest, + sOverrides.annotatedUserHandles, + sOverrides.mWorkProfileAvailability); } // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at
diff --git a/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java index d06b792..9eaf926 100644 --- a/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java
@@ -61,7 +61,7 @@ new CountingIdlingResource("LoadLabelTask"); @Override - protected final ActivityLogic createActivityLogic() { + protected final ResolverActivityLogic createActivityLogic() { return new TestResolverActivityLogic( "ResolverWrapper", this,
diff --git a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt index 0849e51..3c22254 100644 --- a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt +++ b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt
@@ -3,25 +3,26 @@ import androidx.activity.ComponentActivity import com.android.intentresolver.AnnotatedUserHandles import com.android.intentresolver.WorkProfileAvailabilityManager +import com.android.intentresolver.v2.ui.model.ChooserRequest /** Activity logic for use when testing [ChooserActivity]. */ class TestChooserActivityLogic( tag: String, activity: ComponentActivity, onWorkProfileStatusUpdated: () -> Unit, - private val overrideData: ChooserActivityOverrideData, + chooserRequest: ChooserRequest? = null, + private val annotatedUserHandlesOverride: AnnotatedUserHandles?, + private val workProfileAvailabilityOverride: WorkProfileAvailabilityManager?, ) : ChooserActivityLogic( tag, activity, onWorkProfileStatusUpdated, + chooserRequest, ) { + override val annotatedUserHandles: AnnotatedUserHandles? + get() = annotatedUserHandlesOverride ?: super.annotatedUserHandles - override val annotatedUserHandles: AnnotatedUserHandles? by lazy { - overrideData.annotatedUserHandles - } - - override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy { - overrideData.mWorkProfileAvailability ?: super.workProfileAvailabilityManager - } + override val workProfileAvailabilityManager: WorkProfileAvailabilityManager + get() = workProfileAvailabilityOverride ?: super.workProfileAvailabilityManager }
diff --git a/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt b/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt new file mode 100644 index 0000000..6a16168 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/ext/IntentExtTest.kt
@@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 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.v2.ext + +import android.content.Intent +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import java.util.function.Predicate +import org.junit.Test + +class IntentExtTest { + + private val hasSendAction = + Predicate<Intent> { + it?.action == Intent.ACTION_SEND || it?.action == Intent.ACTION_SEND_MULTIPLE + } + + @Test + fun hasAction() { + val sendIntent = Intent(Intent.ACTION_SEND) + assertThat(sendIntent.hasAction(Intent.ACTION_SEND)).isTrue() + assertThat(sendIntent.hasAction(Intent.ACTION_VIEW)).isFalse() + } + + @Test + fun hasSingleCategory() { + val intent = Intent().addCategory(Intent.CATEGORY_HOME) + assertThat(intent.hasSingleCategory(Intent.CATEGORY_HOME)).isTrue() + assertThat(intent.hasSingleCategory(Intent.CATEGORY_DEFAULT)).isFalse() + + intent.addCategory(Intent.CATEGORY_TEST) + assertThat(intent.hasSingleCategory(Intent.CATEGORY_TEST)).isFalse() + } + + @Test + fun ifMatch_matched() { + val sendIntent = Intent(Intent.ACTION_SEND) + val sendMultipleIntent = Intent(Intent.ACTION_SEND_MULTIPLE) + + sendIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) } + sendMultipleIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) } + assertWithMessage("sendIntent flags") + .that(sendIntent.flags) + .isEqualTo(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) + assertWithMessage("sendMultipleIntent flags") + .that(sendMultipleIntent.flags) + .isEqualTo(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) + } + + @Test + fun ifMatch_notMatched() { + val viewIntent = Intent(Intent.ACTION_VIEW) + + viewIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) } + assertWithMessage("viewIntent flags").that(viewIntent.flags).isEqualTo(0) + } +}
diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt new file mode 100644 index 0000000..bcc1054 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt
@@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 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.v2.ui.viewmodel + +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.EXTRA_INTENT +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import com.android.intentresolver.v2.ui.model.CallerInfo +import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@Suppress("DEPRECATION") +class ChooserRequestTest { + + private val callerInfo = + CallerInfo( + launchedFromUid = 10000, + launchedFomPackage = "com.android.example", + referrer = "android-app://com.android.example".toUri() + ) + + @Test + fun missingIntent() { + val args = bundleOf() + + val result = readChooserRequest(callerInfo, args::get) + + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly(RequiredValueMissing(EXTRA_INTENT, Intent::class)) + } + + @Test + fun minimal() { + val args = bundleOf(EXTRA_INTENT to Intent(ACTION_SEND)) + + val result = readChooserRequest(callerInfo, args::get) + + assertThat(result).value().isNotNull() + val value: ChooserRequest = result.getOrThrow() + assertThat(value.launchedFromPackage).isEqualTo(callerInfo.launchedFomPackage) + assertThat(result).findings().isEmpty() + } +}