summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Mark Renouf <mrenouf@google.com> 2024-01-29 14:10:07 -0500
committer Mark Renouf <mrenouf@google.com> 2024-02-03 10:43:48 -0500
commit29bd514e948c60b1dae2ae0fabc0d15adb2b0950 (patch)
tree6df50dbe87f34139d6ba818987ca777ac012b356
parent0ccdb68644e8e531c67cfc73a56dd9c95352829f (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
-rw-r--r--java/src/com/android/intentresolver/v2/ActivityLogic.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java58
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt21
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivity.java114
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt30
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt7
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt5
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt38
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt68
-rw-r--r--java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt7
-rw-r--r--java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt59
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java4
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt3
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt2
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt44
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt87
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestTest.kt120
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)
+ }
+}