diff options
author | 2024-01-29 14:10:07 -0500 | |
---|---|---|
committer | 2024-02-03 10:43:48 -0500 | |
commit | 29bd514e948c60b1dae2ae0fabc0d15adb2b0950 (patch) | |
tree | 6df50dbe87f34139d6ba818987ca777ac012b356 | |
parent | 0ccdb68644e8e531c67cfc73a56dd9c95352829f (diff) |
Adds ResolverRequest, moves handing to tested code
This formalizes the inputs to ResolverActivity, replacing
the equivalent inline code. Fields that were temporarily
routed through 'ActivityLogic' are now removed.
Bug: 300157408
Test: atest IntentResolver-tests-activity
Test: atest IntentResolver-tests-unit:ResolveRequestTest
Change-Id: I79d9fa21b91d0ce9b008af12ba3bffbd60e91a38
17 files changed, 478 insertions, 225 deletions
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt index b9686418..62ace0da 100644 --- a/java/src/com/android/intentresolver/v2/ActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt @@ -15,7 +15,6 @@ */ package com.android.intentresolver.v2 -import android.content.Intent import android.os.UserHandle import android.os.UserManager import android.util.Log @@ -30,18 +29,7 @@ import com.android.intentresolver.WorkProfileAvailabilityManager * activity, including test activities, but all implementations should delegate to a * CommonActivityLogic implementation. */ -interface ActivityLogic : CommonActivityLogic { - /** The intent for the target. This will always come before additional targets, if any. */ - val targetIntent: Intent - /** Custom title to display. */ - val title: CharSequence? - /** Resource ID for the title to display when there is no custom title. */ - val defaultTitleResId: Int - /** Intents received to be processed. */ - val initialIntents: List<Intent>? - /** The intents for potential actual targets. [targetIntent] must be first. */ - val payloadIntents: List<Intent> -} +interface ActivityLogic : CommonActivityLogic /** * Logic that is common to all IntentResolver activities. Anything that is the same across @@ -50,14 +38,13 @@ interface ActivityLogic : CommonActivityLogic { interface CommonActivityLogic { /** The tag to use when logging. */ val tag: String + /** A reference to the activity owning, and used by, this logic. */ val activity: ComponentActivity - /** The name of the referring package. */ - val referrerPackageName: String? - /** User manager system service. */ - val userManager: UserManager + /** Current [UserHandle]s retrievable by type. */ val annotatedUserHandles: AnnotatedUserHandles? + /** Monitors for changes to work profile availability. */ val workProfileAvailabilityManager: WorkProfileAvailabilityManager } @@ -73,16 +60,7 @@ class CommonActivityLogicImpl( onWorkProfileStatusUpdated: () -> Unit, ) : CommonActivityLogic { - override val referrerPackageName: String? = - activity.referrer.let { - if (ANDROID_APP_URI_SCHEME == it?.scheme) { - it.host - } else { - null - } - } - - override val userManager: UserManager = activity.getSystemService()!! + private val userManager: UserManager = activity.getSystemService()!! override val annotatedUserHandles: AnnotatedUserHandles? = try { @@ -98,8 +76,4 @@ class CommonActivityLogicImpl( annotatedUserHandles?.workProfileUserHandle, onWorkProfileStatusUpdated, ) - - companion object { - private const val ANDROID_APP_URI_SCHEME = "android-app" - } } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index c1184a80..29a792f6 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -322,12 +322,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private ChooserViewModel mViewModel; @VisibleForTesting - protected ChooserActivityLogic createActivityLogic(ChooserRequest chooserRequest) { + protected ChooserActivityLogic createActivityLogic() { return new ChooserActivityLogic( TAG, /* activity = */ this, - this::onWorkProfileStatusUpdated, - chooserRequest); + this::onWorkProfileStatusUpdated); } @NonNull @@ -355,7 +354,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements finish(); return; } - mLogic = createActivityLogic(mViewModel.getChooserRequest()); + mLogic = createActivityLogic(); init(); } @@ -381,14 +380,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements chooserRequest.getShareTargetFilter() ); - Intent intent = mLogic.getTargetIntent(); - List<Intent> initialIntents = mLogic.getInitialIntents(); - - // Calling UID did not have valid permissions - if (mLogic.getAnnotatedUserHandles() == null) { - finish(); - return; - } + Intent intent = mViewModel.getChooserRequest().getTargetIntent(); + List<Intent> initialIntents = mViewModel.getChooserRequest().getInitialIntents(); mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), @@ -509,7 +502,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements Log.d(TAG, "System Time Cost is " + systemCost); } getEventLog().logShareStarted( - mLogic.getReferrerPackageName(), + chooserRequest.getReferrerPackage(), chooserRequest.getTargetType(), chooserRequest.getCallerChooserTargets().size(), chooserRequest.getInitialIntents().size(), @@ -714,9 +707,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } } - CharSequence title = mLogic.getTitle() != null - ? mLogic.getTitle() - : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId()); + CharSequence title = mViewModel.getChooserRequest().getTitle() != null + ? mViewModel.getChooserRequest().getTitle() + : getTitleForAction(mViewModel.getChooserRequest().getTargetIntent(), + mViewModel.getChooserRequest().getDefaultTitleResource()); if (!TextUtils.isEmpty(title)) { final TextView titleView = findViewById(com.android.internal.R.id.title); @@ -815,7 +809,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } // If needed, show that intent is forwarded // from managed profile to owner or other way around. - String profileSwitchMessage = mIntentForwarding.forwardMessageFor(mLogic.getTargetIntent()); + String profileSwitchMessage = mIntentForwarding.forwardMessageFor( + mViewModel.getChooserRequest().getTargetIntent()); if (profileSwitchMessage != null) { Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); } @@ -1283,7 +1278,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements boolean filterLastUsed) { ChooserGridAdapter adapter = createChooserGridAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mViewModel.getChooserRequest().getPayloadIntents(), initialIntents, rList, filterLastUsed, @@ -1314,7 +1309,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements int selectedProfile = findSelectedProfile(); ChooserGridAdapter personalAdapter = createChooserGridAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mViewModel.getChooserRequest().getPayloadIntents(), selectedProfile == PROFILE_PERSONAL ? initialIntents : null, rList, filterLastUsed, @@ -1322,7 +1317,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements ); ChooserGridAdapter workAdapter = createChooserGridAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mViewModel.getChooserRequest().getPayloadIntents(), selectedProfile == PROFILE_WORK ? initialIntents : null, rList, filterLastUsed, @@ -1823,7 +1818,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (info != null) { sendClickToAppPredictor(info); final ResolveInfo ri = info.getResolveInfo(); - Intent targetIntent = mLogic.getTargetIntent(); + Intent targetIntent = mViewModel.getChooserRequest().getTargetIntent(); if (ri != null && ri.activityInfo != null && targetIntent != null) { ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); @@ -1959,7 +1954,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } } - @VisibleForTesting public ChooserGridAdapter createChooserGridAdapter( Context context, List<Intent> payloadIntents, @@ -1967,7 +1961,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements List<ResolveInfo> rList, boolean filterLastUsed, UserHandle userHandle) { - ChooserRequest parameters = mViewModel.getChooserRequest(); + ChooserRequest request = mViewModel.getChooserRequest(); ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, @@ -1976,8 +1970,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements filterLastUsed, createListController(userHandle), userHandle, - mLogic.getTargetIntent(), - parameters.getReferrerFillInIntent(), + request.getTargetIntent(), + request.getReferrerFillInIntent(), mMaxTargetsPerRow ); @@ -2081,8 +2075,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator( this, - mLogic.getTargetIntent(), - mLogic.getReferrerPackageName(), + mViewModel.getChooserRequest().getTargetIntent(), + mViewModel.getChooserRequest().getLaunchedFromPackage(), appPredictor, userHandle, getEventLog(), @@ -2092,8 +2086,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements resolverComparator = new ResolverRankerServiceResolverComparator( this, - mLogic.getTargetIntent(), - mLogic.getReferrerPackageName(), + mViewModel.getChooserRequest().getTargetIntent(), + mViewModel.getChooserRequest().getReferrerPackage(), null, getEventLog(), getResolverRankerServiceUserHandleList(userHandle), @@ -2103,9 +2097,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements return new ChooserListController( this, mPackageManager, - mLogic.getTargetIntent(), - mLogic.getReferrerPackageName(), - mActivityLaunch.getFromUid(), + mViewModel.getChooserRequest().getTargetIntent(), + mViewModel.getChooserRequest().getReferrerPackage(), + requireAnnotatedUserHandles().userIdOfCallingApp, resolverComparator, getQueryIntentsUser(userHandle)); } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt index f6054885..84b7d9a9 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt @@ -1,11 +1,7 @@ package com.android.intentresolver.v2 -import android.content.Intent import androidx.activity.ComponentActivity import androidx.annotation.OpenForTesting -import com.android.intentresolver.v2.ui.model.ChooserRequest - -private const val TAG = "ChooserActivityLogic" /** * Activity logic for [ChooserActivity]. @@ -18,25 +14,10 @@ open class ChooserActivityLogic( tag: String, activity: ComponentActivity, onWorkProfileStatusUpdated: () -> Unit, - private val chooserRequest: ChooserRequest? = null, ) : ActivityLogic, CommonActivityLogic by CommonActivityLogicImpl( tag, activity, onWorkProfileStatusUpdated, - ) { - - override val targetIntent: Intent = chooserRequest?.targetIntent ?: Intent() - - override val title: CharSequence? = chooserRequest?.title - - override val defaultTitleResId: Int = chooserRequest?.defaultTitleResource ?: 0 - - override val initialIntents: List<Intent>? = chooserRequest?.initialIntents?.toList() - - override val payloadIntents: List<Intent> = buildList { - add(targetIntent) - 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 b8638ba4..77d1dbf5 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -25,11 +25,10 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs; +import static com.android.intentresolver.v2.ui.viewmodel.ResolverRequestReaderKt.readResolverRequest; import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; -import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; -import static java.util.Objects.requireNonNullElse; import android.app.ActivityThread; import android.app.VoiceInteractor.PickOptionRequest; @@ -105,13 +104,15 @@ import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSele import com.android.intentresolver.v2.MultiProfilePagerAdapter.ProfileType; import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; +import com.android.intentresolver.v2.domain.model.Profile; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; 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.v2.ui.model.ActivityLaunch; +import com.android.intentresolver.v2.ui.model.ResolverRequest; +import com.android.intentresolver.v2.validation.ValidationResult; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; @@ -144,10 +145,11 @@ import javax.inject.Inject; public class ResolverActivity extends Hilt_ResolverActivity implements ResolverListAdapter.ResolverListCommunicator { + @Inject public PackageManager mPackageManager; @Inject public ActivityLaunch mActivityLaunch; @Inject public DevicePolicyResources mDevicePolicyResources; @Inject public IntentForwarding mIntentForwarding; - @Inject public PackageManager mPackageManager; + private ResolverRequest mResolverRequest; protected ActivityLogic mLogic; protected TargetDataLoader mTargetDataLoader; @@ -185,32 +187,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements protected ResolverMultiProfilePagerAdapter mMultiProfilePagerAdapter; - - // Intent extra for connected audio devices - public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"; - - /** - * Integer extra to indicate which profile should be automatically selected. - * <p>Can only be used if there is a work profile. - * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. - */ - protected static final String EXTRA_SELECTED_PROFILE = - "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE"; - - /** - * {@link UserHandle} extra to indicate the user of the user that the starting intent - * originated from. - * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()}, - * as there are edge cases when the intent resolver is launched in the other profile. - * For example, when we have 0 resolved apps in current profile and multiple resolved - * apps in the other profile, opening a link from the current profile launches the intent - * resolver in the other one. b/148536209 for more info. - */ - static final String EXTRA_CALLING_USER = - "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; - - protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; - protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; + public static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; + public static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; private UserHandle mHeaderCreatorUser; @@ -234,7 +212,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } @VisibleForTesting - protected ResolverActivityLogic createActivityLogic() { + protected ActivityLogic createActivityLogic() { return new ResolverActivityLogic( TAG, /* activity = */ this, @@ -261,22 +239,24 @@ public class ResolverActivity extends Hilt_ResolverActivity implements finish(); } + ValidationResult<ResolverRequest> result = readResolverRequest(mActivityLaunch); + if (!result.isSuccess()) { + result.reportToLogcat(TAG); + finish(); + } + mResolverRequest = result.getOrThrow(); mLogic = createActivityLogic(); - mResolvingHome = IntentExtKt.isHomeIntent(getIntent()); + mResolvingHome = mResolverRequest.isResolvingHome(); mTargetDataLoader = new DefaultTargetDataLoader( this, getLifecycle(), - getIntent().getBooleanExtra( - ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, - /* defaultValue = */ false) - ); + mResolverRequest.isAudioCaptureDevice()); init(); restore(savedInstanceState); } private void init() { - Intent intent = mLogic.getTargetIntent(); - List<Intent> initialIntents = mLogic.getInitialIntents(); + Intent intent = mResolverRequest.getIntent(); // The last argument of createResolverListAdapter is whether to do special handling // of the last used choice to highlight it in the list. We need to always @@ -289,8 +269,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements boolean filterLastUsed = !isVoiceInteraction() && !hasWorkProfile() && !hasCloneProfile(); mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( - requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), - /* resolutionList = */ null, + new Intent[0], + /* resolutionList = */ mResolverRequest.getResolutionList(), filterLastUsed ); if (configureContentView(mTargetDataLoader)) { @@ -764,8 +744,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements ResolverRankerServiceResolverComparator resolverComparator = new ResolverRankerServiceResolverComparator( this, - mLogic.getTargetIntent(), - mLogic.getReferrerPackageName(), + mResolverRequest.getIntent(), + mActivityLaunch.getReferrerPackage(), null, null, getResolverRankerServiceUserHandleList(userHandle), @@ -773,8 +753,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements return new ResolverListController( this, mPackageManager, - mLogic.getTargetIntent(), - mLogic.getReferrerPackageName(), + mActivityLaunch.getIntent(), + mActivityLaunch.getReferrerPackage(), mActivityLaunch.getFromUid(), resolverComparator, getQueryIntentsUser(userHandle)); @@ -920,7 +900,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements filterLastUsed, createListController(userHandle), userHandle, - mLogic.getTargetIntent(), + mResolverRequest.getIntent(), this, initialIntentsUserSpace, mTargetDataLoader); @@ -964,7 +944,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements boolean filterLastUsed) { ResolverListAdapter personalAdapter = createResolverListAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mResolverRequest.getPayloadIntents(), initialIntents, resolutionList, filterLastUsed, @@ -987,9 +967,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } private UserHandle getIntentUser() { - return getIntent().hasExtra(EXTRA_CALLING_USER) - ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) - : requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch; + return Objects.requireNonNullElse(mResolverRequest.getCallingUser(), + requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch); } private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( @@ -1018,7 +997,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements // resolver list. So filterLastUsed should be false for the other profile. ResolverListAdapter personalAdapter = createResolverListAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mResolverRequest.getPayloadIntents(), selectedProfile == PROFILE_PERSONAL ? initialIntents : null, resolutionList, (filterLastUsed && UserHandle.myUserId() @@ -1028,7 +1007,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements UserHandle workProfileUserHandle = requireAnnotatedUserHandles().workProfileUserHandle; ResolverListAdapter workAdapter = createResolverListAdapter( /* context */ this, - mLogic.getPayloadIntents(), + mResolverRequest.getPayloadIntents(), selectedProfile == PROFILE_WORK ? initialIntents : null, resolutionList, (filterLastUsed && UserHandle.myUserId() @@ -1060,20 +1039,17 @@ public class ResolverActivity extends Hilt_ResolverActivity implements /** * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied. - * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE} - * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} */ final int getSelectedProfileExtra() { - int selectedProfile = -1; - if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) { - selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1); - if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) { - throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value " - + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or " - + "ResolverActivity.PROFILE_WORK."); - } + Profile.Type selected = mResolverRequest.getSelectedProfile(); + if (selected == null) { + return -1; + } + switch (selected) { + case PERSONAL: return PROFILE_PERSONAL; + case WORK: return PROFILE_WORK; + default: return -1; } - return selectedProfile; } protected final @ProfileType int getCurrentProfile() { @@ -1302,9 +1278,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements if (!hasRecordPermission) { // OK, we know the record permission, is this a capture device - boolean hasAudioCapture = - getIntent().getBooleanExtra( - ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false); + boolean hasAudioCapture = mResolverRequest.isAudioCaptureDevice(); enabled = !hasAudioCapture; } } @@ -1491,7 +1465,8 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } // If needed, show that intent is forwarded // from managed profile to owner or other way around. - String profileSwitchMessage = mIntentForwarding.forwardMessageFor(mLogic.getTargetIntent()); + String profileSwitchMessage = + mIntentForwarding.forwardMessageFor(mResolverRequest.getIntent()); if (profileSwitchMessage != null) { Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show(); } @@ -1771,10 +1746,9 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } } - - CharSequence title = mLogic.getTitle() != null - ? mLogic.getTitle() - : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId()); + CharSequence title = mResolverRequest.getTitle() != null + ? mResolverRequest.getTitle() + : getTitleForAction(mResolverRequest.getIntent(), 0); if (!TextUtils.isEmpty(title)) { final TextView titleView = findViewById(com.android.internal.R.id.title); diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt index 13353041..7eb63ab3 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt @@ -1,6 +1,5 @@ package com.android.intentresolver.v2 -import android.content.Intent import androidx.activity.ComponentActivity import androidx.annotation.OpenForTesting @@ -16,31 +15,4 @@ open class ResolverActivityLogic( tag, activity, onWorkProfileStatusUpdated, - ) { - - final override val targetIntent: Intent = let { - val intent = Intent(activity.intent) - intent.setComponent(null) - // The resolver activity is set to be hidden from recent tasks. - // we don't want this attribute to be propagated to the next activity - // being launched. Note that if the original Intent also had this - // flag set, we are now losing it. That should be a very rare case - // and we can live with this. - intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv()) - - // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate - // side, which means we want to open the target app on the same side as ResolverActivity. - if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT != 0) { - intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT.inv()) - } - intent - } - - override val title: CharSequence? = null - - override val defaultTitleResId: Int = 0 - - override val initialIntents: List<Intent>? = null - - override val payloadIntents: List<Intent> = listOf(targetIntent) -} + ) diff --git a/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt index fd25ea42..e5f342d9 100644 --- a/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt +++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt @@ -29,7 +29,7 @@ data class ActivityLaunch( /** The identifier for the sending app and user */ val fromUid: Int, /** The package of the sending app */ - val fromPackage: String?, + val fromPackage: String, /** The referrer as supplied to the activity. */ val referrer: Uri? ) : Parcelable { @@ -38,10 +38,13 @@ data class ActivityLaunch( ) : this( intent = source.requireParcelable(), fromUid = source.readInt(), - fromPackage = source.readString(), + fromPackage = requireNotNull(source.readString()), referrer = source.readParcelable() ) + /** A package name from referrer, if it is an android-app URI */ + val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority + override fun describeContents() = 0 /* flags */ override fun writeToParcel(dest: Parcel, flags: Int) { diff --git a/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt index 3311467e..bb8f3a54 100644 --- a/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt +++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt @@ -33,7 +33,10 @@ object ActivityLaunchModule { return ActivityLaunch( activity.intent, activity.launchedFromUid, - activity.launchedFromPackage, + requireNotNull(activity.launchedFromPackage) { + "activity.launchedFromPackage was null. This is expected to be non-null for " + + "any system-signed application!" + }, activity.referrer ) } diff --git a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt index 2fbf94a2..d41d0874 100644 --- a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt +++ b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt @@ -19,16 +19,17 @@ 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_REFERRER import android.content.IntentFilter import android.content.IntentSender +import android.net.Uri 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 +const val ANDROID_APP_SCHEME = "android-app" /** All of the things that are consumed from an incoming share Intent (+Extras). */ data class ChooserRequest( @@ -58,10 +59,10 @@ data class ChooserRequest( @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. + * The referrer value as received by the caller. It may have been supplied via [EXTRA_REFERRER] + * or synthesized from callerPackageName. This value is merged into outgoing intents. */ - val referrerFillInIntent: Intent, + val referrer: Uri?, /** * Choices to exclude from results. @@ -163,18 +164,29 @@ data class ChooserRequest( */ val shareTargetFilter: IntentFilter? = null ) { + val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority + + fun getReferrerFillInIntent(): Intent { + return Intent().apply { + referrerPackage?.also { pkg -> + putExtra(EXTRA_REFERRER, Uri.parse("$ANDROID_APP_SCHEME://$pkg")) + } + } + } + + val payloadIntents = listOf(targetIntent) + additionalTargets /** Constructs an instance from only the required values. */ constructor( targetIntent: Intent, - referrerPackageName: String + launchedFromPackage: String, + referrer: Uri? ) : this( - targetIntent, - targetIntent.action, - targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), - targetIntent.type, - referrerPackageName, - referrerFillInIntent = - Intent().apply { putExtra(Intent.EXTRA_REFERRER, referrerPackageName) } + targetIntent = targetIntent, + targetAction = targetIntent.action, + isSendActionTarget = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), + targetType = targetIntent.type, + launchedFromPackage = launchedFromPackage, + referrer = referrer ) } diff --git a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt new file mode 100644 index 00000000..5abfb602 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt @@ -0,0 +1,68 @@ +/* + * 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.Intent +import android.content.pm.ResolveInfo +import android.os.UserHandle +import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.ext.isHomeIntent + +/** All of the things that are consumed from an incoming Intent Resolution request (+Extras). */ +data class ResolverRequest( + /** The intent to be resolved to a target. */ + val intent: Intent, + + /** + * Supplied by the system to indicate which profile should be selected by default. This is + * required since ResolverActivity may be launched as either the originating OR target user when + * resolving a cross profile intent. + * + * Valid values are: [PERSONAL][Profile.Type.PERSONAL] and [WORK][Profile.Type.WORK] and null + * when the intent is not a forwarded cross-profile intent. + */ + val selectedProfile: Profile.Type?, + + /** + * When handing a cross profile forwarded intent, this is the user which started the original + * intent. This is required to allow ResolverActivity to be launched as the target user under + * some conditions. + */ + val callingUser: UserHandle?, + + /** + * Indicates if resolving actions for a connected device which has audio capture capability + * (e.g. is a USB Microphone). + * + * When used to handle a connected device, ResolverActivity uses this signal to present a + * warning when a resolved application does not hold the RECORD_AUDIO permission. (If selected + * the app would be able to capture audio directly via the device, bypassing audio API + * permissions.) + */ + val isAudioCaptureDevice: Boolean = false, + + /** A list of a resolved activity targets. This list overrides normal intent resolution. */ + val resolutionList: List<ResolveInfo>? = null, + + /** A customized title for the resolver interface. */ + val title: String? = null, +) { + val isResolvingHome = intent.isHomeIntent() + + /** For compatibility with existing code shared between chooser/resolver. */ + val payloadIntents: List<Intent> = listOf(intent) +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt index 33868aaf..45e2ea64 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -46,14 +46,15 @@ import com.android.intentresolver.v2.ext.hasAction import com.android.intentresolver.v2.ext.ifMatch import com.android.intentresolver.v2.ui.model.ActivityLaunch 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.ValidationResult 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 const val MAX_CHOOSER_ACTIONS = 5 +private const val MAX_INITIAL_INTENTS = 2 + private fun Intent.hasSendAction() = hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) internal fun Intent.maybeAddSendActionFlags() = @@ -134,7 +135,7 @@ fun readChooserRequest(launch: ActivityLaunch): ValidationResult<ChooserRequest> }, title = customTitle, defaultTitleResource = defaultTitleResource, - referrerFillInIntent = referrerFillIn, + referrer = launch.referrer, filteredComponentNames = filteredComponents, callerChooserTargets = callerChooserTargets, chooserActions = chooserActions, diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt new file mode 100644 index 00000000..fc9f1e01 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.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.viewmodel + +import android.os.Bundle +import android.os.UserHandle +import com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL +import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK +import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.ui.model.ActivityLaunch +import com.android.intentresolver.v2.ui.model.ResolverRequest +import com.android.intentresolver.v2.validation.Validation +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +const val EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER" +const val EXTRA_SELECTED_PROFILE = + "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE" +const val EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device" + +fun readResolverRequest(launch: ActivityLaunch): ValidationResult<ResolverRequest> { + @Suppress("DEPRECATION") + return validateFrom((launch.intent.extras ?: Bundle())::get) { + val callingUser = optional(value<UserHandle>(EXTRA_CALLING_USER)) + val selectedProfile = checkSelectedProfile() + val audioDevice = optional(value<Boolean>(EXTRA_IS_AUDIO_CAPTURE_DEVICE)) ?: false + ResolverRequest(launch.intent, selectedProfile, callingUser, audioDevice) + } +} + +private fun Validation.checkSelectedProfile(): Profile.Type? { + return when (val selected = optional(value<Int>(EXTRA_SELECTED_PROFILE))) { + null -> null + PROFILE_PERSONAL -> Profile.Type.PERSONAL + PROFILE_WORK -> Profile.Type.WORK + else -> + error( + EXTRA_SELECTED_PROFILE + + " has invalid value ($selected)." + + " Must be either ResolverActivity.PROFILE_PERSONAL ($PROFILE_PERSONAL)" + + " or ResolverActivity.PROFILE_WORK ($PROFILE_WORK)." + ) + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java index 64c8e49d..07e6e7b4 100644 --- a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java @@ -40,7 +40,6 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; 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; @@ -55,12 +54,11 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW private UsageStatsManager mUsm; @Override - protected final ChooserActivityLogic createActivityLogic(ChooserRequest chooserRequest) { + protected final ChooserActivityLogic createActivityLogic() { return new TestChooserActivityLogic( "ChooserWrapper", /* activity = */ this, this::onWorkProfileStatusUpdated, - chooserRequest, sOverrides.annotatedUserHandles, sOverrides.mWorkProfileAvailability); } diff --git a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt index 3c22254a..fe649819 100644 --- a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt +++ b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt @@ -3,14 +3,12 @@ package com.android.intentresolver.v2 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, - chooserRequest: ChooserRequest? = null, private val annotatedUserHandlesOverride: AnnotatedUserHandles?, private val workProfileAvailabilityOverride: WorkProfileAvailabilityManager?, ) : @@ -18,7 +16,6 @@ class TestChooserActivityLogic( tag, activity, onWorkProfileStatusUpdated, - chooserRequest, ) { override val annotatedUserHandles: AnnotatedUserHandles? get() = annotatedUserHandlesOverride ?: super.annotatedUserHandles diff --git a/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt b/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt index d674bbc2..7dd15dbe 100644 --- a/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt +++ b/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt @@ -36,6 +36,6 @@ class TestActivityLaunchModule { companion object { const val LAUNCHED_FROM_PACKAGE = "example.com" const val LAUNCHED_FROM_UID = 1234 - val REFERRER: Uri = Uri.parse("android-app://$LAUNCHED_FROM_PACKAGE") + val REFERRER: Uri = Uri.fromParts(ANDROID_APP_SCHEME, LAUNCHED_FROM_PACKAGE, "") } } diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt index 3e9f43da..25eac220 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt @@ -21,6 +21,7 @@ import android.content.Intent.ACTION_CHOOSER import android.content.Intent.EXTRA_TEXT import android.net.Uri import com.android.intentresolver.v2.ext.toParcelAndBack +import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Test @@ -28,7 +29,7 @@ class ActivityLaunchTest { @Test fun testDefaultValues() { - val input = ActivityLaunch(Intent(ACTION_CHOOSER), 0, null, null) + val input = ActivityLaunch(Intent(ACTION_CHOOSER), 0, "example.com", null) val output = input.toParcelAndBack() @@ -46,7 +47,46 @@ class ActivityLaunchTest { assertEquals(input, output) } - fun assertEquals(expected: ActivityLaunch, actual: ActivityLaunch) { + @Test + fun testReferrerPackage_withAppReferrer_usesReferrer() { + val launch1 = + ActivityLaunch( + intent = Intent(), + fromUid = 1000, + fromPackage = "other.example.com", + referrer = Uri.parse("android-app://app.example.com") + ) + + assertThat(launch1.referrerPackage).isEqualTo("app.example.com") + } + + @Test + fun testReferrerPackage_httpReferrer_isNull() { + val launch = + ActivityLaunch( + intent = Intent(), + fromUid = 1000, + fromPackage = "example.com", + referrer = Uri.parse("http://some.other.value") + ) + + assertThat(launch.referrerPackage).isNull() + } + + @Test + fun testReferrerPackage_nullReferrer_isNull() { + val launch = + ActivityLaunch( + intent = Intent(), + fromUid = 1000, + fromPackage = "example.com", + referrer = null + ) + + assertThat(launch.referrerPackage).isNull() + } + + private fun assertEquals(expected: ActivityLaunch, actual: ActivityLaunch) { // Test fields separately: Intent does not override equals() assertWithMessage("%s.filterEquals(%s)", actual.intent, expected.intent) .that(actual.intent.filterEquals(expected.intent)) 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 index 29bb5cbd..3174c5f6 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt @@ -18,7 +18,11 @@ package com.android.intentresolver.v2.ui.viewmodel import android.content.Intent import android.content.Intent.ACTION_CHOOSER 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_INTENT +import android.content.Intent.EXTRA_REFERRER +import android.net.Uri import androidx.core.net.toUri import androidx.core.os.bundleOf import com.android.intentresolver.v2.ui.model.ActivityLaunch @@ -28,21 +32,27 @@ import com.android.intentresolver.v2.validation.ValidationResultSubject.Companio import com.google.common.truth.Truth.assertThat import org.junit.Test -@Suppress("DEPRECATION") -class ChooserRequestTest { +private fun createLaunch( + targetIntent: Intent?, + referrer: Uri? = null, + additionalIntents: List<Intent>? = null +) = + ActivityLaunch( + Intent(ACTION_CHOOSER).apply { + targetIntent?.also { putExtra(EXTRA_INTENT, it) } + additionalIntents?.also { putExtra(EXTRA_ALTERNATE_INTENTS, it.toTypedArray()) } + }, + fromUid = 10000, + fromPackage = "com.android.example", + referrer = referrer ?: "android-app://com.android.example".toUri() + ) - val intent = Intent(ACTION_CHOOSER) - private val mActivityLaunch = - ActivityLaunch( - intent, - fromUid = 10000, - fromPackage = "com.android.example", - referrer = "android-app://com.android.example".toUri() - ) +class ChooserRequestTest { @Test fun missingIntent() { - val result = readChooserRequest(mActivityLaunch) + val launch = createLaunch(targetIntent = null) + val result = readChooserRequest(launch) assertThat(result).value().isNull() assertThat(result) @@ -51,14 +61,61 @@ class ChooserRequestTest { } @Test - fun minimal() { - intent.putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) + fun referrerFillIn() { + val referrer = Uri.parse("android-app://example.com") + val launch = createLaunch(targetIntent = Intent(ACTION_SEND), referrer) + launch.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) + + val result = readChooserRequest(launch) + + val fillIn = result.value?.getReferrerFillInIntent() + assertThat(fillIn?.hasExtra(EXTRA_REFERRER)).isTrue() + assertThat(fillIn?.getParcelableExtra(EXTRA_REFERRER, Uri::class.java)).isEqualTo(referrer) + } + + @Test + fun referrerPackage_isNullWithNonAppReferrer() { + val referrer = Uri.parse("http://example.com") + val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) + + val launch = createLaunch(targetIntent = intent, referrer = referrer) + + val result = readChooserRequest(launch) + + assertThat(result.value?.referrerPackage).isNull() + } + + @Test + fun referrerPackage_fromAppReferrer() { + val referrer = Uri.parse("android-app://example.com") + val launch = createLaunch(targetIntent = Intent(ACTION_SEND), referrer) + + launch.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer)) + + val result = readChooserRequest(launch) + + assertThat(result.value?.referrerPackage).isEqualTo(referrer.authority) + } - val result = readChooserRequest(mActivityLaunch) + @Test + fun payloadIntents_includesTargetThenAdditional() { + val intent1 = Intent(ACTION_SEND) + val intent2 = Intent(ACTION_SEND_MULTIPLE) + val launch = createLaunch(targetIntent = intent1, additionalIntents = listOf(intent2)) + val result = readChooserRequest(launch) + + assertThat(result.value?.payloadIntents).containsExactly(intent1, intent2) + } + + @Test + fun testRequest_withOnlyRequiredValues() { + val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) + val launch = createLaunch(targetIntent = intent) + val result = readChooserRequest(launch) assertThat(result).value().isNotNull() val value: ChooserRequest = result.getOrThrow() - assertThat(value.launchedFromPackage).isEqualTo(mActivityLaunch.fromPackage) + assertThat(value.launchedFromPackage).isEqualTo(launch.fromPackage) assertThat(result).findings().isEmpty() } } diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt new file mode 100644 index 00000000..a5acb0d3 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt @@ -0,0 +1,120 @@ +/* + * 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_VIEW +import android.net.Uri +import android.os.UserHandle +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK +import com.android.intentresolver.v2.domain.model.Profile.Type.WORK +import com.android.intentresolver.v2.ui.model.ActivityLaunch +import com.android.intentresolver.v2.ui.model.ResolverRequest +import com.android.intentresolver.v2.validation.UncaughtException +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Test + +private val targetUri = Uri.parse("content://example.com/123") + +private fun createLaunch( + targetIntent: Intent, + referrer: Uri? = null, +) = + ActivityLaunch( + intent = targetIntent, + fromUid = 10000, + fromPackage = "com.android.example", + referrer = referrer ?: "android-app://com.android.example".toUri() + ) + +class ResolverRequestTest { + @Test + fun testDefaults() { + val intent = Intent(ACTION_VIEW).apply { data = targetUri } + val launch = createLaunch(intent) + + val result = readResolverRequest(launch) + assertThat(result).isSuccess() + assertThat(result).findings().isEmpty() + val value: ResolverRequest = result.getOrThrow() + + assertThat(value.intent.filterEquals(launch.intent)).isTrue() + assertThat(value.callingUser).isNull() + assertThat(value.selectedProfile).isNull() + } + + @Test + fun testInvalidSelectedProfile() { + val intent = + Intent(ACTION_VIEW).apply { + data = targetUri + putExtra(EXTRA_SELECTED_PROFILE, -1000) + } + + val launch = createLaunch(intent) + + val result = readResolverRequest(launch) + + assertThat(result).isFailure() + assertWithMessage("the first finding") + .that(result.findings.firstOrNull()) + .isInstanceOf(UncaughtException::class.java) + } + + @Test + fun payloadIntents_includesOnlyTarget() { + val intent2 = Intent(Intent.ACTION_SEND_MULTIPLE) + val intent1 = + Intent(Intent.ACTION_SEND).apply { + putParcelableArrayListExtra(Intent.EXTRA_ALTERNATE_INTENTS, arrayListOf(intent2)) + } + val launch = createLaunch(targetIntent = intent1) + + val result = readResolverRequest(launch) + + // Assert that payloadIntents does NOT include EXTRA_ALTERNATE_INTENTS + // that is only supported for Chooser and should be not be added here. + assertThat(result.value?.payloadIntents).containsExactly(intent1) + } + + @Test + fun testAllValues() { + val intent = Intent(ACTION_VIEW).apply { data = Uri.parse("content://example.com/123") } + val launch = createLaunch(targetIntent = intent) + + launch.intent.putExtras( + bundleOf( + EXTRA_CALLING_USER to UserHandle.of(123), + EXTRA_SELECTED_PROFILE to PROFILE_WORK, + EXTRA_IS_AUDIO_CAPTURE_DEVICE to true, + ) + ) + + val result = readResolverRequest(launch) + + assertThat(result).value().isNotNull() + val value: ResolverRequest = result.getOrThrow() + + assertThat(value.intent.filterEquals(launch.intent)).isTrue() + assertThat(value.isAudioCaptureDevice).isTrue() + assertThat(value.callingUser).isEqualTo(UserHandle.of(123)) + assertThat(value.selectedProfile).isEqualTo(WORK) + } +} |