diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/providers/media/photopicker/data/UserManagerState.java | 393 |
1 files changed, 226 insertions, 167 deletions
diff --git a/src/com/android/providers/media/photopicker/data/UserManagerState.java b/src/com/android/providers/media/photopicker/data/UserManagerState.java index 58b70e87e..de6af1b0c 100644 --- a/src/com/android/providers/media/photopicker/data/UserManagerState.java +++ b/src/com/android/providers/media/photopicker/data/UserManagerState.java @@ -21,10 +21,12 @@ import static androidx.core.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresApi; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.UserProperties; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -44,6 +46,7 @@ import com.android.providers.media.photopicker.data.model.UserId; import com.android.providers.media.photopicker.ui.TabFragment; import com.android.providers.media.photopicker.util.CrossProfileUtils; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -54,9 +57,7 @@ import java.util.Map; * Interface to query user ids {@link UserId} */ public interface UserManagerState { - /** - * Whether there are more than 1 user profiles associated with the current user. - */ + /** Whether there are more than 1 user profiles associated with the current user. */ boolean isMultiUserProfiles(); /** @@ -73,9 +74,9 @@ public interface UserManagerState { /** * A Map of all the profiles with their cross profile allowed status from current user. - * key : userId of a profile - * Value : cross profile allowed status of a user profile corresponding to user id with - * current user . + * + * <p>key : userId of a profile Value : cross profile allowed status of a user profile + * corresponding to user id with current user . */ @NonNull Map<UserId, Boolean> getCrossProfileAllowedStatusForAll(); @@ -86,16 +87,13 @@ public interface UserManagerState { */ int getProfileCount(); - /** - * - * A {@link MutableLiveData} to check if cross profile interaction allowed or not. - */ + /** A {@link MutableLiveData} to check if cross profile interaction allowed or not. */ @NonNull MutableLiveData<Map<UserId, Boolean>> getCrossProfileAllowed(); /** - * A list of all user profile ids including current user that need to be shown - * separately in PhotoPicker + * A list of all user profile ids including current user that need to be shown separately in + * PhotoPicker */ @NonNull List<UserId> getAllUserProfileIds(); @@ -105,38 +103,32 @@ public interface UserManagerState { */ void updateProfileOffValuesAndPostCrossProfileStatus(); - /** - * Updates on/off values of all the user profiles - */ + /** Updates on/off values of all the user profiles */ void updateProfileOffValues(); - /** - * Waits for Media Provider of the user profile corresponding to userId to be available. - */ + /** Waits for Media Provider of the user profile corresponding to userId to be available. */ void waitForMediaProviderToBeAvailable(UserId userId); /** - * Get if it is allowed to access the otherUser profile from current user ( current user : - * the user profile that started the photo picker activity) - **/ + * Get if it is allowed to access the otherUser profile from current user ( current user : the + * user profile that started the photo picker activity) + */ @NonNull boolean isCrossProfileAllowedToUser(UserId otherUser); - /** - * A {@link MutableLiveData} to check if there are multiple user profiles or not - */ + /** A {@link MutableLiveData} to check if there are multiple user profiles or not */ @NonNull MutableLiveData<Boolean> getIsMultiUserProfiles(); /** - * Resets the user ids. This is usually called as a result of receiving broadcast that - * any profile has been added or removed. + * Resets the user ids. This is usually called as a result of receiving broadcast that any + * profile has been added or removed. */ void resetUserIds(); /** - * Resets the user ids and set their cross profile values. This is usually called as a result - * of receiving broadcast that any profile has been added or removed. + * Resets the user ids and set their cross profile values. This is usually called as a result of + * receiving broadcast that any profile has been added or removed. */ void resetUserIdsAndSetCrossProfileValues(Intent intent); @@ -152,39 +144,31 @@ public interface UserManagerState { void setIntentAndCheckRestrictions(Intent intent); /** - * Whether cross profile access corresponding to the userID is blocked - * by admin for the current user. + * Whether cross profile access corresponding to the userID is blocked by admin for the current + * user. */ boolean isBlockedByAdmin(UserId userId); - /** - * Whether profile corresponding to the userID is on or off. - */ + /** Whether profile corresponding to the userID is on or off. */ boolean isProfileOff(UserId userId); - /** - * A map of all user profile labels corresponding to all profile userIds - */ + /** A map of all user profile labels corresponding to all profile userIds */ Map<UserId, String> getProfileLabelsForAll(); /** * Returns whether a user should be shown in the PhotoPicker depending on its quite mode status. * - * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED}, - * {@link UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or - * {@link UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile - * should be shown in quiet mode or not. + * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED}, {@link + * UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or {@link + * UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be + * shown in quiet mode or not. */ int getShowInQuietMode(UserId userId); - /** - * A map of all user profile Icon ids corresponding to all profile userIds - */ + /** A map of all user profile Icon ids corresponding to all profile userIds */ Map<UserId, Drawable> getProfileBadgeForAll(); - /** - * Set a user as a current user profile - **/ + /** Set a user as a current user profile */ void setUserAsCurrentUserProfile(UserId userId); /** @@ -193,8 +177,7 @@ public interface UserManagerState { boolean isUserSelectedAsCurrentUserProfile(UserId userId); /** - * Creates an implementation of {@link UserManagerState}. - * Todo(b/319067964): make this singleton + * Creates an implementation of {@link UserManagerState}. Todo(b/319067964): make this singleton */ static UserManagerState create(Context context) { return new RuntimeUserManagerState(context); @@ -212,37 +195,37 @@ public interface UserManagerState { private static final int SHOW_IN_QUIET_MODE_DEFAULT = -1; private final Context mContext; - // This is the user profile that started the photo picker activity. That's why it cannot - // change in a UserIdManager instance. + // This is the user profile that started the photo picker activity. That's why + // it cannot change in a UserIdManager instance. private final UserId mCurrentUser; private final Handler mHandler; private Map<UserId, Runnable> mIsProviderAvailableRunnableMap = new HashMap<>(); - // This is the user profile selected in the photo picker. Photo picker will display media - // for this user. It could be different from mCurrentUser. + // This is the user profile selected in the photo picker. Photo picker will + // display media for this user. It could be different from mCurrentUser. private UserId mCurrentUserProfile = null; - // A map of user profile ids (Except current user) with a Boolean value that represents - // whether corresponding user profile is blocked by admin or not. - private Map<UserId , Boolean> mIsProfileBlockedByAdminMap = new HashMap<>(); + // A map of user profile ids (Except current user) with a Boolean value that + // represents whether corresponding user profile is blocked by admin or not. + private Map<UserId, Boolean> mIsProfileBlockedByAdminMap = new HashMap<>(); - // A map of user profile ids (Except current user) with a Boolean value that represents - // whether corresponding user profile is on or off. - private Map<UserId , Boolean> mProfileOffStatus = new HashMap<>(); + // A map of user profile ids (Except current user) with a Boolean value that + // represents whether corresponding user profile is on or off. + private Map<UserId, Boolean> mProfileOffStatus = new HashMap<>(); private final MutableLiveData<Boolean> mIsMultiUserProfiles = new MutableLiveData<>(); - // A list of all user profile Ids present on the device that require a separate tab to show - // in PhotoPicker. It also includes currentUser/contextUser. + // A list of all user profile Ids present on the device that require a separate + // tab to show in PhotoPicker. It also includes currentUser/contextUser. private List<UserId> mUserProfileIds = new ArrayList<>(); private UserManager mUserManager; /** * This live data will be posted every time when a user profile change occurs in the - * background such as turning on/off/adding/removing a user profile. The complete map - * will be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will - * be posted into the below mutable live data. This live data will be observed later in - * {@link TabFragment}. - **/ + * background such as turning on/off/adding/removing a user profile. The complete map will + * be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will be posted + * into the below mutable live data. This live data will be observed later in {@link + * TabFragment}. + */ private final MutableLiveData<Map<UserId, Boolean>> mCrossProfileAllowedStatus = new MutableLiveData<>(); @@ -277,8 +260,8 @@ public interface UserManagerState { return; } - // Here there could be other profiles too , that we do not want to show anywhere in - // photo picker at all. + // Here there could be other profiles too , that we do not want to show anywhere + // in photo picker at all. final List<UserHandle> userProfiles = mUserManager.getUserProfiles(); if (SdkLevel.isAtLeastV()) { for (UserHandle userHandle : userProfiles) { @@ -289,13 +272,13 @@ public interface UserManagerState { // an owner profile itself. if (getSystemUser().getIdentifier() != userHandle.getIdentifier() && userProperties.getShowInSharingSurfaces() - == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { + == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { mUserProfileIds.add(userId); } } } else { - // if sdk version is less than V, then maximum two profiles with separate tab could - // only be available + // if sdk version is less than V, then maximum two profiles with separate tab + // could only be available for (UserHandle userHandle : userProfiles) { if (mUserManager.isManagedProfile(userHandle.getIdentifier())) { mUserProfileIds.add(UserId.of(userHandle)); @@ -311,7 +294,7 @@ public interface UserManagerState { } @Override - public int getProfileCount() { + public int getProfileCount() { return mUserProfileIds.size(); } @@ -364,11 +347,11 @@ public interface UserManagerState { @Override public void setIntentAndCheckRestrictions(Intent intent) { assertMainThread(); - // The below method should be called even if only one profile is present on the device - // because we want to have current profile off value and blocked by admin values in the - // corresponding maps + // The below method should be called even if only one profile is present on the + // device because we want to have current profile off value and blocked by admin + // values + // in the corresponding maps updateCrossProfileValues(intent); - } @Override @@ -431,13 +414,14 @@ public interface UserManagerState { } } } + @Override public void waitForMediaProviderToBeAvailable(UserId userId) { assertMainThread(); - // Remove callbacks if any pre-available callbacks are present in the message queue for - // given user + // Remove callbacks if any pre-available callbacks are present in the message + // queue for given user stopWaitingForProviderToBeAvailableForUser(userId); - if (CrossProfileUtils.isMediaProviderAvailable(userId , mContext)) { + if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { mProfileOffStatus.put(userId, false); updateAndPostCrossProfileStatus(); return; @@ -446,37 +430,49 @@ public interface UserManagerState { } private void waitForProviderToBeAvailable(UserId userId, int numOfTries) { - // The runnable should make sure to post update on the live data if it is changed. - Runnable runnable = () -> { - try { - // We stop the recursive check when - // 1. the provider is available - // 2. the profile is in quiet mode, i.e. provider will not be available - // 3. after maximum retries - if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { - mProfileOffStatus.put(userId, false); - updateAndPostCrossProfileStatus(); - return; - } - - if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) { - return; - } - - if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) { - Log.d(TAG, "MediaProvider is not available. Retry after " - + PROVIDER_AVAILABILITY_CHECK_DELAY); - waitForProviderToBeAvailable(userId, numOfTries + 1); - return; - } - - Log.w(TAG, "Failed waiting for MediaProvider for user:" + userId - + " to be available"); - } catch (Exception e) { - Log.e(TAG, "An error occurred in runnable while waiting for " - + "MediaProvider for user:" + userId + " to be available", e); - } - }; + // The runnable should make sure to post update on the live data if it is + // changed. + Runnable runnable = + () -> { + try { + // We stop the recursive check when + // 1. the provider is available + // 2. the profile is in quiet mode, i.e. provider will not be available + // 3. after maximum retries + if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { + mProfileOffStatus.put(userId, false); + updateAndPostCrossProfileStatus(); + return; + } + + if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) { + return; + } + + if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) { + Log.d( + TAG, + "MediaProvider is not available. Retry after " + + PROVIDER_AVAILABILITY_CHECK_DELAY); + waitForProviderToBeAvailable(userId, numOfTries + 1); + return; + } + + Log.w( + TAG, + "Failed waiting for MediaProvider for user:" + + userId + + " to be available"); + } catch (Exception e) { + Log.e( + TAG, + "An error occurred in runnable while waiting for " + + "MediaProvider for user:" + + userId + + " to be available", + e); + } + }; mIsProviderAvailableRunnableMap.put(userId, runnable); mHandler.postDelayed(runnable, PROVIDER_AVAILABILITY_CHECK_DELAY); } @@ -536,80 +532,134 @@ public interface UserManagerState { return null; } return isCrossProfileStrategyDelegatedToParent(userHandle) - ? mUserManager.getProfileParent(userHandle) : userHandle; + ? mUserManager.getProfileParent(userHandle) + : userHandle; } - /** - * {@link #setBlockedByAdminValue(Intent)} Based on assumption that the only profiles with - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION} could be systemUser - * and managedUser(if available). + * Updates Cross Profile access for all UserProfiles in {@code mUserProfileIds} + * + * <p>This method looks at a variety of situations for each Profile and decides if the + * profile's content is accessible by the current process owner user id. + * + * <p>- UserProperties attributes for CrossProfileDelegation are checked first - + * CrossProfileIntentForwardingActivitys are resolved via the process owner's + * PackageManager, and are considered when evaluating cross profile to the target profile. * - * Todo(b/319567023):Refactor the below {@link #setBlockedByAdminValue(Intent)} to - * avoid assumptions mentioned above. + * <p>- In the event none of the above checks succeeds, the profile is considered to be + * inaccessible to the current process user, and is thus marked as "BlockedByAdmin". + * + * @param intent The intent Photopicker is currently running under, for + * CrossProfileForwardActivity checking. */ + @SuppressLint("DiscouragedPrivateApi") private void setBlockedByAdminValue(Intent intent) { if (intent == null) { - Log.e(TAG, "No intent specified to check if cross profile forwarding is" - + " allowed."); + Log.e( + TAG, + "No intent specified to check if cross profile forwarding is" + + " allowed."); return; } - // List of all user profile ids that context user cannot access - List<UserId> canNotForwardToUserProfiles = new ArrayList<>(); + Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>(); + List<UserId> delegatedFromParent = new ArrayList<>(); - /* - * List of all user profile ids that have cross profile access among themselves. - * It contains parent user and child profiles with user property - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT} - */ - List<UserId> parentOrDelegatedFromParent = new ArrayList<>(); + final PackageManager pm = mContext.getPackageManager(); - // Userprofile to check cross profile intentForwarderActivity for - UserHandle needToCheck = null; + // Resolve CrossProfile activities for all user profiles that Photopicker is + // aware of. + for (UserId userId : mUserProfileIds) { - if (mUserManager == null) { - Log.e(TAG, "Cannot obtain user manager"); - return; - } + // If the UserId is the system user, exit early. + if (userId.getIdentifier() == mCurrentUser.getIdentifier()) { + profileIsAccessibleToProcessOwner.put(userId, true); + continue; + } - for (UserId userId : mUserProfileIds) { - /* - * All user profiles with user property - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT} - * can access each other including its parent. - */ - if (userId.equals(getSystemUser()) - || isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) { - parentOrDelegatedFromParent.add(userId); - } else { - needToCheck = userId.getUserHandle(); + // This UserId delegates its strategy to the parent profile + if (isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) { + delegatedFromParent.add(userId); + continue; } - } - // When context user is a managed user , then will replace needToCheck with its parent - // to check cross profile intentForwarderActivity for. - if (needToCheck != null && needToCheck.equals(mCurrentUser.getUserHandle())) { - needToCheck = mUserManager.getProfileParent(mCurrentUser.getUserHandle()); + // Clear out the component & package before attempting to match + Intent intentToCheck = (Intent) intent.clone(); + intentToCheck.setComponent(null); + intentToCheck.setPackage(null); + + for (ResolveInfo resolveInfo : + pm.queryIntentActivities( + intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) { + + // If the activity is a CrossProfileIntentForwardingActivity, inspect its + // targetUserId to + // see if it targets the user we are currently checking for. + if (resolveInfo.isCrossProfileIntentForwarderActivity()) { + + /* + * IMPORTANT: This is a reflection based hack to ensure the profile is + * actually the installer of the CrossProfileIntentForwardingActivity. + * + * ResolveInfo.targetUserId exists, but is a hidden API not available to + * mainline modules, and no such API exists, so it is accessed via + * reflection below. All exceptions are caught to protect against + * reflection related issues such as: + * NoSuchFieldException / IllegalAccessException / SecurityException. + * + * In the event of an exception, the code fails "closed" for the current + * profile to avoid showing content that should not be visible. + */ + try { + Field targetUserIdField = + resolveInfo.getClass().getDeclaredField("targetUserId"); + targetUserIdField.setAccessible(true); + int targetUserId = (int) targetUserIdField.get(resolveInfo); + + if (targetUserId == userId.getIdentifier()) { + profileIsAccessibleToProcessOwner.put(userId, true); + + // Don't need to look further, exit the loop. + break; + } + + } catch (NoSuchFieldException + | IllegalAccessException + | SecurityException ex) { + // Couldn't check the targetUserId via reflection, so fail without + // further + // iterations. + Log.e(TAG, "Could not access targetUserId via reflection.", ex); + break; + } catch (Exception ex) { + Log.e(TAG, "Exception occurred during cross profile checks", ex); + } + } + } + // Fail case, was unable to find a suitable Activity for this user. + profileIsAccessibleToProcessOwner.putIfAbsent(userId, false); } - final PackageManager packageManager = mContext.getPackageManager(); - if (needToCheck != null && !CrossProfileUtils.isIntentAllowedCrossProfileAccessFromUser( - intent, packageManager, - getProfileToCheckCrossProfileAccess(mCurrentUser.getUserHandle()))) { - if (parentOrDelegatedFromParent.contains(UserId.of(needToCheck))) { - // if user profile cannot access its parent then all direct child profiles with - // delegated from parent will also be inaccessible. - canNotForwardToUserProfiles.addAll(parentOrDelegatedFromParent); - } else { - canNotForwardToUserProfiles.add(UserId.of(needToCheck)); - } + // For profiles that delegate their access to the parent, set the access for + // those profiles equal to the same as their parent. + for (UserId userId : delegatedFromParent) { + UserHandle parent = + mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier())); + profileIsAccessibleToProcessOwner.put( + userId, + profileIsAccessibleToProcessOwner.getOrDefault( + UserId.of(parent), /* default= */ false)); } mIsProfileBlockedByAdminMap.clear(); for (UserId userId : mUserProfileIds) { - mIsProfileBlockedByAdminMap.put(userId, - canNotForwardToUserProfiles.contains(userId)); + mIsProfileBlockedByAdminMap.put( + // Boolean inversion seems strange, but this map is the opposite of what was + // calculated, (which are blocked, rather than which are accessible) so the + // boolean needs to be inverted. + userId, + !profileIsAccessibleToProcessOwner.getOrDefault( + userId, /* default= */ false)); } } @@ -630,6 +680,7 @@ public interface UserManagerState { return profileLabels; } + private String getProfileLabel(UserHandle userHandle) { if (SdkLevel.isAtLeastV()) { Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */); @@ -641,7 +692,7 @@ public interface UserManagerState { } return userManager.getProfileLabel(); } catch (Resources.NotFoundException e) { - //Todo(b/318530691): Handle the label for the profile that is not defined + // Todo(b/318530691): Handle the label for the profile that is not defined // already } } @@ -675,7 +726,9 @@ public interface UserManagerState { } return userManager.getUserBadge(); } catch (Resources.NotFoundException e) { - //Todo(b/318530691): Handle the icon for the profile that is not defined already + // Todo(b/318530691): Handle the icon for the profile that is not defined + // already + Log.i(TAG, "failed to find resource"); } } return null; @@ -707,6 +760,7 @@ public interface UserManagerState { assertMainThread(); return mIsProfileBlockedByAdminMap.get(userId); } + @Override public boolean isProfileOff(UserId userId) { assertMainThread(); @@ -716,10 +770,15 @@ public interface UserManagerState { private void assertMainThread() { if (Looper.getMainLooper().isCurrentThread()) return; - throw new IllegalStateException("UserManagerState methods are expected to be called" - + "from main thread. " + (Looper.myLooper() == null ? "" : "Current thread " - + Looper.myLooper().getThread() + ", Main thread " - + Looper.getMainLooper().getThread())); + throw new IllegalStateException( + "UserManagerState methods are expected to be called" + + "from main thread. " + + (Looper.myLooper() == null + ? "" + : "Current thread " + + Looper.myLooper().getThread() + + ", Main thread " + + Looper.getMainLooper().getThread())); } } } |