Scaffolding for AnnotatedUserHandles testing.
This provides (test-only) capacities to inject different
UserHandle values than the ones we'd normally determine, so that
we can test under artificially-constructed scenarios just by
injecting the appropriate fake AnnotatedUserHandles value.
Test: `atest AnnotatedUserHandlesTest` (& presubmits/etc)
Bug: 280237072
Change-Id: Idc2f7a5a46f49f9e4c11d361e01e6943404262b2
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
index 769195e..168f36d 100644
--- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -22,6 +22,8 @@
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.VisibleForTesting;
+
/**
* Helper class to precompute the (immutable) designations of various user handles in the system
* that may contribute to the current Sharesheet session.
@@ -78,28 +80,74 @@
*/
public final UserHandle tabOwnerUserHandleForLaunch;
- public AnnotatedUserHandles(Activity forShareActivity) {
- userIdOfCallingApp = forShareActivity.getLaunchedFromUid();
- if ((userIdOfCallingApp < 0) || UserHandle.isIsolated(userIdOfCallingApp)) {
- throw new SecurityException("Can't start a resolver from uid " + userIdOfCallingApp);
- }
-
- // TODO: integrate logic for `ResolverActivity.EXTRA_CALLING_USER`.
- userHandleSharesheetLaunchedAs = UserHandle.of(UserHandle.myUserId());
+ /** Compute all handle designations for a new Sharesheet session in the specified activity. */
+ public static AnnotatedUserHandles forShareActivity(Activity shareActivity) {
+ // TODO: consider integrating logic for `ResolverActivity.EXTRA_CALLING_USER`?
+ UserHandle userHandleSharesheetLaunchedAs = UserHandle.of(UserHandle.myUserId());
// ActivityManager.getCurrentUser() refers to the current Foreground user. When clone/work
// profile is active, we always make the personal tab from the foreground user.
// Outside profiles, current foreground user is potentially the same as the sharesheet
// process's user (UserHandle.myUserId()), so we continue to create personal tab with the
// current foreground user.
- personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
+ UserHandle personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
- UserManager userManager = forShareActivity.getSystemService(UserManager.class);
- workProfileUserHandle = getWorkProfileForUser(userManager, personalProfileUserHandle);
- cloneProfileUserHandle = getCloneProfileForUser(userManager, personalProfileUserHandle);
+ UserManager userManager = shareActivity.getSystemService(UserManager.class);
- tabOwnerUserHandleForLaunch = (userHandleSharesheetLaunchedAs == workProfileUserHandle)
- ? workProfileUserHandle : personalProfileUserHandle;
+ return newBuilder()
+ .setUserIdOfCallingApp(shareActivity.getLaunchedFromUid())
+ .setUserHandleSharesheetLaunchedAs(userHandleSharesheetLaunchedAs)
+ .setPersonalProfileUserHandle(personalProfileUserHandle)
+ .setWorkProfileUserHandle(
+ getWorkProfileForUser(userManager, personalProfileUserHandle))
+ .setCloneProfileUserHandle(
+ getCloneProfileForUser(userManager, personalProfileUserHandle))
+ .build();
+ }
+
+ @VisibleForTesting static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the {@link UserHandle} to use when querying resolutions for intents in a
+ * {@link ResolverListController} configured for the provided {@code userHandle}.
+ */
+ public UserHandle getQueryIntentsUser(UserHandle userHandle) {
+ // In case launching app is in clonedProfile, and we are building the personal tab, intent
+ // resolution will be attempted as clonedUser instead of user 0. This is because intent
+ // resolution from user 0 and clonedUser is not guaranteed to return same results.
+ // We do not care about the case when personal adapter is started with non-root user
+ // (secondary user case), as clone profile is guaranteed to be non-active in that case.
+ UserHandle queryIntentsUser = userHandle;
+ if (isLaunchedAsCloneProfile() && userHandle.equals(personalProfileUserHandle)) {
+ queryIntentsUser = cloneProfileUserHandle;
+ }
+ return queryIntentsUser;
+ }
+
+ private Boolean isLaunchedAsCloneProfile() {
+ return userHandleSharesheetLaunchedAs.equals(cloneProfileUserHandle);
+ }
+
+ private AnnotatedUserHandles(
+ int userIdOfCallingApp,
+ UserHandle userHandleSharesheetLaunchedAs,
+ UserHandle personalProfileUserHandle,
+ @Nullable UserHandle workProfileUserHandle,
+ @Nullable UserHandle cloneProfileUserHandle) {
+ if ((userIdOfCallingApp < 0) || UserHandle.isIsolated(userIdOfCallingApp)) {
+ throw new SecurityException("Can't start a resolver from uid " + userIdOfCallingApp);
+ }
+
+ this.userIdOfCallingApp = userIdOfCallingApp;
+ this.userHandleSharesheetLaunchedAs = userHandleSharesheetLaunchedAs;
+ this.personalProfileUserHandle = personalProfileUserHandle;
+ this.workProfileUserHandle = workProfileUserHandle;
+ this.cloneProfileUserHandle = cloneProfileUserHandle;
+ this.tabOwnerUserHandleForLaunch =
+ (userHandleSharesheetLaunchedAs == workProfileUserHandle)
+ ? workProfileUserHandle : personalProfileUserHandle;
}
@Nullable
@@ -124,24 +172,46 @@
.orElse(null);
}
- /**
- * Returns the {@link UserHandle} to use when querying resolutions for intents in a
- * {@link ResolverListController} configured for the provided {@code userHandle}.
- */
- public UserHandle getQueryIntentsUser(UserHandle userHandle) {
- // In case launching app is in clonedProfile, and we are building the personal tab, intent
- // resolution will be attempted as clonedUser instead of user 0. This is because intent
- // resolution from user 0 and clonedUser is not guaranteed to return same results.
- // We do not care about the case when personal adapter is started with non-root user
- // (secondary user case), as clone profile is guaranteed to be non-active in that case.
- UserHandle queryIntentsUser = userHandle;
- if (isLaunchedAsCloneProfile() && userHandle.equals(personalProfileUserHandle)) {
- queryIntentsUser = cloneProfileUserHandle;
- }
- return queryIntentsUser;
- }
+ @VisibleForTesting
+ static class Builder {
+ private int mUserIdOfCallingApp;
+ private UserHandle mUserHandleSharesheetLaunchedAs;
+ private UserHandle mPersonalProfileUserHandle;
+ private UserHandle mWorkProfileUserHandle;
+ private UserHandle mCloneProfileUserHandle;
- private Boolean isLaunchedAsCloneProfile() {
- return userHandleSharesheetLaunchedAs.equals(cloneProfileUserHandle);
+ public Builder setUserIdOfCallingApp(int id) {
+ mUserIdOfCallingApp = id;
+ return this;
+ }
+
+ public Builder setUserHandleSharesheetLaunchedAs(UserHandle user) {
+ mUserHandleSharesheetLaunchedAs = user;
+ return this;
+ }
+
+ public Builder setPersonalProfileUserHandle(UserHandle user) {
+ mPersonalProfileUserHandle = user;
+ return this;
+ }
+
+ public Builder setWorkProfileUserHandle(UserHandle user) {
+ mWorkProfileUserHandle = user;
+ return this;
+ }
+
+ public Builder setCloneProfileUserHandle(UserHandle user) {
+ mCloneProfileUserHandle = user;
+ return this;
+ }
+
+ public AnnotatedUserHandles build() {
+ return new AnnotatedUserHandles(
+ mUserIdOfCallingApp,
+ mUserHandleSharesheetLaunchedAs,
+ mPersonalProfileUserHandle,
+ mWorkProfileUserHandle,
+ mCloneProfileUserHandle);
+ }
}
}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index aea6c2c..ced7bf5 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -228,7 +228,7 @@
// new component whose lifecycle is limited to the "created" Activity (so that we can just hold
// the annotations as a `final` ivar, which is a better way to show immutability).
private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> {
- final AnnotatedUserHandles result = new AnnotatedUserHandles(this);
+ final AnnotatedUserHandles result = AnnotatedUserHandles.forShareActivity(this);
mLazyAnnotatedUserHandles = () -> result;
return result;
};
diff --git a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt b/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
new file mode 100644
index 0000000..a17a560
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.os.UserHandle
+
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+
+class AnnotatedUserHandlesTest {
+
+ @Test
+ fun testBasicProperties() { // Fields that are reflected back w/o logic.
+ val info = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(42)
+ .setUserHandleSharesheetLaunchedAs(UserHandle.of(116))
+ .setPersonalProfileUserHandle(UserHandle.of(117))
+ .setWorkProfileUserHandle(UserHandle.of(118))
+ .setCloneProfileUserHandle(UserHandle.of(119))
+ .build()
+
+ assertThat(info.userIdOfCallingApp).isEqualTo(42)
+ assertThat(info.userHandleSharesheetLaunchedAs.identifier).isEqualTo(116)
+ assertThat(info.personalProfileUserHandle.identifier).isEqualTo(117)
+ assertThat(info.workProfileUserHandle.identifier).isEqualTo(118)
+ assertThat(info.cloneProfileUserHandle.identifier).isEqualTo(119)
+ }
+
+ @Test
+ fun testWorkTabInitiallySelectedWhenLaunchedFromWorkProfile() {
+ val info = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(42)
+ .setPersonalProfileUserHandle(UserHandle.of(101))
+ .setWorkProfileUserHandle(UserHandle.of(202))
+ .setUserHandleSharesheetLaunchedAs(UserHandle.of(202))
+ .build()
+
+ assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(202)
+ }
+
+ @Test
+ fun testPersonalTabInitiallySelectedWhenLaunchedFromPersonalProfile() {
+ val info = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(42)
+ .setPersonalProfileUserHandle(UserHandle.of(101))
+ .setWorkProfileUserHandle(UserHandle.of(202))
+ .setUserHandleSharesheetLaunchedAs(UserHandle.of(101))
+ .build()
+
+ assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101)
+ }
+
+ @Test
+ fun testPersonalTabInitiallySelectedWhenLaunchedFromOtherProfile() {
+ val info = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(42)
+ .setPersonalProfileUserHandle(UserHandle.of(101))
+ .setWorkProfileUserHandle(UserHandle.of(202))
+ .setUserHandleSharesheetLaunchedAs(UserHandle.of(303))
+ .build()
+
+ assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101)
+ }
+}