From ad6c99c6d25b7710bcf650d1a0bb1f7c8996f9ad Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Mon, 30 Jan 2023 09:35:49 -0500 Subject: 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 --- .../intentresolver/AnnotatedUserHandles.java | 113 +++++++++++++++++++++ .../android/intentresolver/ChooserActivity.java | 2 +- .../android/intentresolver/ResolverActivity.java | 51 ++++------ 3 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 java/src/com/android/intentresolver/AnnotatedUserHandles.java (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java new file mode 100644 index 00000000..b4365b84 --- /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 3a7d4e68..a355bef8 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1694,7 +1694,7 @@ public class ChooserActivity extends ResolverActivity implements 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 5f8f3da8..d431d57b 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -163,7 +163,6 @@ public class ResolverActivity extends FragmentActivity implements 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 @@ public class ResolverActivity extends FragmentActivity implements private BroadcastReceiver mWorkProfileStateReceiver; private UserHandle mHeaderCreatorUser; - private Supplier 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 mLazyAnnotatedUserHandles = () -> { + final AnnotatedUserHandles result = new AnnotatedUserHandles(this); + mLazyAnnotatedUserHandles = () -> result; return result; }; @@ -395,12 +400,9 @@ public class ResolverActivity extends FragmentActivity implements // 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 @@ public class ResolverActivity extends FragmentActivity implements 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(); } - @Nullable - protected UserHandle getWorkProfileUserHandle() { - return mLazyWorkProfileUserHandle.get(); + protected final UserHandle getPersonalProfileUserHandle() { + return getAnnotatedUserHandles().personalProfileUserHandle; } + // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. @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; + protected UserHandle getWorkProfileUserHandle() { + return getAnnotatedUserHandles().workProfileUserHandle; } private boolean hasWorkProfile() { @@ -1494,7 +1486,8 @@ public class ResolverActivity extends FragmentActivity implements 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 @@ public class ResolverActivity extends FragmentActivity implements mPm, getTargetIntent(), getReferrerPackageName(), - mLaunchedFromUid, + getAnnotatedUserHandles().userIdOfCallingApp, userHandle); } -- cgit v1.2.3-59-g8ed1b