Introduce `AnnotatedUserHandles` class.

This component is a container for precomputed `UserHandle` values
that should be consistent wherever they're referenced throughout
a chooser/resolver session.

This includes some low-hanging integrations in `ChooserActivity` and
`ResolverActivity` that seemed unobjectionable and suitable for "pure"
refactoring -- i.e. the same handles are ultimately evaluated from the
same expressions, and I don't immediately plan to change the legacy
logic. Once this is checked in, we can proceed to looking at some of
the more complex/refactorable applications of `UserHandle` and
eventually integrate this component more thoroughly. First follow-up
priority is test coverage; existing coverage validates our typical
behavior as observed in the activities, but it would be great if we
could validate our understanding with thorough unit tests directly
against the `AnnotatedUserHandles` API.

Test: `atest IntentResolverUnitTests`
Change-Id: I36116d8c7156b7d30e777dd3c609c7e883ffc042
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
new file mode 100644
index 0000000..b4365b8
--- /dev/null
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 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.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+/**
+ * Helper class to precompute the (immutable) designations of various user handles in the system
+ * that may contribute to the current Sharesheet session.
+ */
+public final class AnnotatedUserHandles {
+    /** The user id of the app that started the share activity. */
+    public final int userIdOfCallingApp;
+
+    /**
+     * The {@link UserHandle} that launched Sharesheet.
+     * TODO: I believe this would always be the handle corresponding to {@code userIdOfCallingApp}
+     * except possibly if the caller used {@link Activity#startActivityAsUser()} to launch
+     * Sharesheet as a different user than they themselves were running as. Verify and document.
+     */
+    public final UserHandle userHandleSharesheetLaunchedAs;
+
+    /**
+     * The {@link UserHandle} that owns the "personal tab" in a tabbed share UI (or the *only* 'tab'
+     * in a non-tabbed UI).
+     *
+     * This is never a work or clone user, but may either be the root user (0) or a "secondary"
+     * multi-user profile (i.e., one that's not root, work, nor clone). This is a "secondary"
+     * profile only when that user is the active "foreground" user.
+     *
+     * In the current implementation, we can assert that this is the root user (0) any time we
+     * display a tabbed UI (i.e., any time `workProfileUserHandle` is non-null), or any time that we
+     * have a clone profile. This note is only provided for informational purposes; clients should
+     * avoid making any reliances on that assumption.
+     */
+    public final UserHandle personalProfileUserHandle;
+
+    /**
+     * The {@link UserHandle} that owns the "work tab" in a tabbed share UI. This is (an arbitrary)
+     * one of the "managed" profiles associated with {@link personalProfileUserHandle}.
+     */
+    @Nullable
+    public final UserHandle workProfileUserHandle;
+
+    /**
+     * The {@link UserHandle} of the clone profile belonging to {@link personalProfileUserHandle}.
+     */
+    @Nullable
+    public final UserHandle cloneProfileUserHandle;
+
+    /**
+     * The "tab owner" user handle (i.e., either {@link personalProfileUserHandle} or
+     * {@link workProfileUserHandle}) that either matches or owns the profile of the
+     * {@link userHandleSharesheetLaunchedAs}.
+     *
+     * In the current implementation, we can assert that this is the same as
+     * `userHandleSharesheetLaunchedAs` except when the latter is the clone profile; then this is
+     * the "personal" profile owning that clone profile (which we currently know must belong to
+     * user 0, but clients should avoid making any reliances on that assumption).
+     */
+    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());
+
+        personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
+
+        UserManager userManager = forShareActivity.getSystemService(UserManager.class);
+        workProfileUserHandle = getWorkProfileForUser(userManager, personalProfileUserHandle);
+        cloneProfileUserHandle = getCloneProfileForUser(userManager, personalProfileUserHandle);
+
+        tabOwnerUserHandleForLaunch = (userHandleSharesheetLaunchedAs == workProfileUserHandle)
+                ? workProfileUserHandle : personalProfileUserHandle;
+    }
+
+    @Nullable
+    private static UserHandle getWorkProfileForUser(
+            UserManager userManager, UserHandle profileOwnerUserHandle) {
+        return userManager.getProfiles(profileOwnerUserHandle.getIdentifier()).stream()
+                .filter(info -> info.isManagedProfile()).findFirst()
+                .map(info -> info.getUserHandle()).orElse(null);
+    }
+
+    @Nullable
+    private static UserHandle getCloneProfileForUser(
+            UserManager userManager, UserHandle profileOwnerUserHandle) {
+        return null;  // Not yet supported in framework.
+    }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 3a7d4e6..a355bef 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1694,7 +1694,7 @@
                 mPm,
                 getTargetIntent(),
                 getReferrerPackageName(),
-                mLaunchedFromUid,
+                getAnnotatedUserHandles().userIdOfCallingApp,
                 userHandle,
                 resolverComparator);
     }
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 5f8f3da..d431d57 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -163,7 +163,6 @@
     protected boolean mSupportsAlwaysUseOption;
     protected ResolverDrawerLayout mResolverDrawerLayout;
     protected PackageManager mPm;
-    protected int mLaunchedFromUid;
 
     private static final String TAG = "ResolverActivity";
     private static final boolean DEBUG = false;
@@ -223,9 +222,15 @@
     private BroadcastReceiver mWorkProfileStateReceiver;
     private UserHandle mHeaderCreatorUser;
 
-    private Supplier<UserHandle> mLazyWorkProfileUserHandle = () -> {
-        final UserHandle result = fetchWorkProfileUserProfile();
-        mLazyWorkProfileUserHandle = () -> result;
+    // User handle annotations are lazy-initialized to ensure that they're computed exactly once
+    // (even though they can't be computed prior to activity creation).
+    // TODO: use a less ad-hoc pattern for lazy initialization (by switching to Dagger or
+    // introducing a common `LazySingletonSupplier` API, etc), and/or migrate all dependents to a
+    // 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);
+        mLazyAnnotatedUserHandles = () -> result;
         return result;
     };
 
@@ -395,12 +400,9 @@
         // from managed profile to owner or other way around.
         setProfileSwitchMessage(intent.getContentUserHint());
 
-        mLaunchedFromUid = getLaunchedFromUid();
-        if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) {
-            // Gulp!
-            finish();
-            return;
-        }
+        // Force computation of user handle annotations in order to validate the caller ID. (See the
+        // associated TODO comment to explain why this is structured as a lazy computation.)
+        AnnotatedUserHandles unusedReferenceToHandles = mLazyAnnotatedUserHandles.get();
 
         mPm = getPackageManager();
 
@@ -699,28 +701,18 @@
         return (UserHandle.myUserId() == UserHandle.USER_SYSTEM ? PROFILE_PERSONAL : PROFILE_WORK);
     }
 
-    protected UserHandle getPersonalProfileUserHandle() {
-        return UserHandle.of(ActivityManager.getCurrentUser());
+    protected final AnnotatedUserHandles getAnnotatedUserHandles() {
+        return mLazyAnnotatedUserHandles.get();
     }
 
+    protected final UserHandle getPersonalProfileUserHandle() {
+        return getAnnotatedUserHandles().personalProfileUserHandle;
+    }
+
+    // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
     @Nullable
     protected UserHandle getWorkProfileUserHandle() {
-        return mLazyWorkProfileUserHandle.get();
-    }
-
-    @Nullable
-    private UserHandle fetchWorkProfileUserProfile() {
-        UserManager userManager = getSystemService(UserManager.class);
-        if (userManager == null) {
-            return null;
-        }
-        UserHandle result = null;
-        for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) {
-            if (userInfo.isManagedProfile()) {
-                result = userInfo.getUserHandle();
-            }
-        }
-        return result;
+        return getAnnotatedUserHandles().workProfileUserHandle;
     }
 
     private boolean hasWorkProfile() {
@@ -1494,7 +1486,8 @@
                 maybeLogCrossProfileTargetLaunch(cti, user);
             }
         } catch (RuntimeException e) {
-            Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid
+            Slog.wtf(TAG,
+                    "Unable to launch as uid " + getAnnotatedUserHandles().userIdOfCallingApp
                     + " package " + getLaunchedFromPackage() + ", while running in "
                     + ActivityThread.currentProcessName(), e);
         }
@@ -1560,7 +1553,7 @@
                 mPm,
                 getTargetIntent(),
                 getReferrerPackageName(),
-                mLaunchedFromUid,
+                getAnnotatedUserHandles().userIdOfCallingApp,
                 userHandle);
     }