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