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)
+    }
+}