| /* |
| * 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.documentsui; |
| |
| import static androidx.core.util.Preconditions.checkNotNull; |
| |
| import static com.android.documentsui.DevicePolicyResources.Drawables.Style.SOLID_COLORED; |
| import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; |
| import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB; |
| import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; |
| |
| import android.Manifest; |
| import android.annotation.SuppressLint; |
| import android.app.ActivityManager; |
| import android.app.admin.DevicePolicyManager; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.UserProperties; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.Log; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.RequiresPermission; |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.documentsui.base.Features; |
| import com.android.documentsui.base.UserId; |
| import com.android.documentsui.util.VersionUtils; |
| import com.android.modules.utils.build.SdkLevel; |
| |
| import com.google.common.base.Objects; |
| |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| @RequiresApi(Build.VERSION_CODES.S) |
| public interface UserManagerState { |
| |
| /** |
| * Returns the {@link UserId} of each profile which should be queried for documents. This will |
| * always include {@link UserId#CURRENT_USER}. |
| */ |
| List<UserId> getUserIds(); |
| |
| /** Returns mapping between the {@link UserId} and the label for the profile */ |
| Map<UserId, String> getUserIdToLabelMap(); |
| |
| /** |
| * Returns mapping between the {@link UserId} and the drawable badge for the profile |
| * |
| * <p>returns {@code null} for non-profile userId |
| */ |
| Map<UserId, Drawable> getUserIdToBadgeMap(); |
| |
| /** |
| * Returns a map of {@link UserId} to boolean value indicating whether the {@link |
| * UserId}.CURRENT_USER can forward {@link Intent} to that {@link UserId} |
| */ |
| Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent); |
| |
| /** |
| * Updates the state of the list of userIds and all the associated maps according the intent |
| * received in broadcast |
| * |
| * @param userId {@link UserId} for the profile for which the availability status changed |
| * @param action {@link Intent}.ACTION_PROFILE_UNAVAILABLE and {@link |
| * Intent}.ACTION_PROFILE_AVAILABLE, {@link Intent}.ACTION_PROFILE_ADDED} and {@link |
| * Intent}.ACTION_PROFILE_REMOVED} |
| */ |
| void onProfileActionStatusChange(String action, UserId userId); |
| |
| /** Sets the intent that triggered the launch of the DocsUI */ |
| void setCurrentStateIntent(Intent intent); |
| |
| /** Returns true if there are hidden profiles */ |
| boolean areHiddenInQuietModeProfilesPresent(); |
| |
| /** Creates an implementation of {@link UserManagerState}. */ |
| // TODO: b/314746383 Make this class a singleton |
| static UserManagerState create(Context context) { |
| return new RuntimeUserManagerState(context); |
| } |
| |
| /** Implementation of {@link UserManagerState} */ |
| final class RuntimeUserManagerState implements UserManagerState { |
| |
| private static final String TAG = "UserManagerState"; |
| private final Context mContext; |
| private final UserId mCurrentUser; |
| private final boolean mIsDeviceSupported; |
| private final UserManager mUserManager; |
| private final ConfigStore mConfigStore; |
| |
| /** |
| * List of all the {@link UserId} that have the {@link UserProperties.ShowInSharingSurfaces} |
| * set as `SHOW_IN_SHARING_SURFACES_SEPARATE` OR it is a system/personal user |
| */ |
| @GuardedBy("mUserIds") |
| private final List<UserId> mUserIds = new ArrayList<>(); |
| |
| /** Mapping between the {@link UserId} to the corresponding profile label */ |
| @GuardedBy("mUserIdToLabelMap") |
| private final Map<UserId, String> mUserIdToLabelMap = new HashMap<>(); |
| |
| /** Mapping between the {@link UserId} to the corresponding profile badge */ |
| @GuardedBy("mUserIdToBadgeMap") |
| private final Map<UserId, Drawable> mUserIdToBadgeMap = new HashMap<>(); |
| |
| /** |
| * Map containing {@link UserId}, other than that of the current user, as key and boolean |
| * denoting whether it is accessible by the current user or not as value |
| */ |
| @GuardedBy("mCanForwardToProfileIdMap") |
| private final Map<UserId, Boolean> mCanForwardToProfileIdMap = new HashMap<>(); |
| |
| private Intent mCurrentStateIntent; |
| |
| private final BroadcastReceiver mIntentReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| synchronized (mUserIds) { |
| mUserIds.clear(); |
| } |
| synchronized (mUserIdToLabelMap) { |
| mUserIdToLabelMap.clear(); |
| } |
| synchronized (mUserIdToBadgeMap) { |
| mUserIdToBadgeMap.clear(); |
| } |
| synchronized (mCanForwardToProfileIdMap) { |
| mCanForwardToProfileIdMap.clear(); |
| } |
| } |
| }; |
| |
| private RuntimeUserManagerState(Context context) { |
| this( |
| context, |
| UserId.CURRENT_USER, |
| Features.CROSS_PROFILE_TABS && isDeviceSupported(context), |
| DocumentsApplication.getConfigStore()); |
| } |
| |
| @VisibleForTesting |
| RuntimeUserManagerState( |
| Context context, |
| UserId currentUser, |
| boolean isDeviceSupported, |
| ConfigStore configStore) { |
| mContext = context.getApplicationContext(); |
| mCurrentUser = checkNotNull(currentUser); |
| mIsDeviceSupported = isDeviceSupported; |
| mUserManager = mContext.getSystemService(UserManager.class); |
| mConfigStore = configStore; |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); |
| filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); |
| if (SdkLevel.isAtLeastV() && mConfigStore.isPrivateSpaceInDocsUIEnabled()) { |
| filter.addAction(Intent.ACTION_PROFILE_ADDED); |
| filter.addAction(Intent.ACTION_PROFILE_REMOVED); |
| } |
| mContext.registerReceiver(mIntentReceiver, filter); |
| } |
| |
| @Override |
| public List<UserId> getUserIds() { |
| synchronized (mUserIds) { |
| if (mUserIds.isEmpty()) { |
| mUserIds.addAll(getUserIdsInternal()); |
| } |
| return mUserIds; |
| } |
| } |
| |
| @Override |
| public Map<UserId, String> getUserIdToLabelMap() { |
| synchronized (mUserIdToLabelMap) { |
| if (mUserIdToLabelMap.isEmpty()) { |
| getUserIdToLabelMapInternal(); |
| } |
| return mUserIdToLabelMap; |
| } |
| } |
| |
| @Override |
| public Map<UserId, Drawable> getUserIdToBadgeMap() { |
| synchronized (mUserIdToBadgeMap) { |
| if (mUserIdToBadgeMap.isEmpty()) { |
| getUserIdToBadgeMapInternal(); |
| } |
| return mUserIdToBadgeMap; |
| } |
| } |
| |
| @Override |
| public Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent) { |
| synchronized (mCanForwardToProfileIdMap) { |
| if (mCanForwardToProfileIdMap.isEmpty()) { |
| getCanForwardToProfileIdMapInternal(intent); |
| } |
| return mCanForwardToProfileIdMap; |
| } |
| } |
| |
| @Override |
| @SuppressLint("NewApi") |
| public void onProfileActionStatusChange(String action, UserId userId) { |
| if (!SdkLevel.isAtLeastV()) return; |
| UserProperties userProperties = |
| mUserManager.getUserProperties(UserHandle.of(userId.getIdentifier())); |
| if (userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) { |
| return; |
| } |
| if (Intent.ACTION_PROFILE_UNAVAILABLE.equals(action) |
| || Intent.ACTION_PROFILE_REMOVED.equals(action)) { |
| synchronized (mUserIds) { |
| mUserIds.remove(userId); |
| } |
| } else if (Intent.ACTION_PROFILE_AVAILABLE.equals(action) |
| || Intent.ACTION_PROFILE_ADDED.equals(action)) { |
| synchronized (mUserIds) { |
| if (!mUserIds.contains(userId)) { |
| mUserIds.add(userId); |
| } |
| } |
| synchronized (mUserIdToLabelMap) { |
| if (!mUserIdToLabelMap.containsKey(userId)) { |
| mUserIdToLabelMap.put(userId, getProfileLabel(userId)); |
| } |
| } |
| synchronized (mUserIdToBadgeMap) { |
| if (!mUserIdToBadgeMap.containsKey(userId)) { |
| mUserIdToBadgeMap.put(userId, getProfileBadge(userId)); |
| } |
| } |
| synchronized (mCanForwardToProfileIdMap) { |
| if (!mCanForwardToProfileIdMap.containsKey(userId)) { |
| mCanForwardToProfileIdMap.put( |
| userId, |
| isCrossProfileAllowedToUser( |
| mContext, |
| mCurrentStateIntent, |
| UserId.CURRENT_USER, |
| userId)); |
| } |
| } |
| } else { |
| Log.e(TAG, "Unexpected action received: " + action); |
| } |
| } |
| |
| @Override |
| public void setCurrentStateIntent(Intent intent) { |
| mCurrentStateIntent = intent; |
| } |
| |
| @Override |
| public boolean areHiddenInQuietModeProfilesPresent() { |
| if (!SdkLevel.isAtLeastV()) { |
| return false; |
| } |
| |
| for (UserId userId : getUserIds()) { |
| if (mUserManager |
| .getUserProperties(UserHandle.of(userId.getIdentifier())) |
| .getShowInQuietMode() |
| == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private List<UserId> getUserIdsInternal() { |
| final List<UserId> result = new ArrayList<>(); |
| |
| if (!mIsDeviceSupported) { |
| result.add(mCurrentUser); |
| return result; |
| } |
| |
| if (mUserManager == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return result; |
| } |
| |
| final List<UserHandle> userProfiles = mUserManager.getUserProfiles(); |
| |
| result.add(mCurrentUser); |
| boolean currentUserIsManaged = |
| mUserManager.isManagedProfile(mCurrentUser.getIdentifier()); |
| |
| for (UserHandle handle : userProfiles) { |
| if (SdkLevel.isAtLeastV()) { |
| if (!isProfileAllowed(handle)) { |
| continue; |
| } |
| } else { |
| // Only allow managed profiles + the parent user on lower than V. |
| if (currentUserIsManaged |
| && mUserManager.getProfileParent(mCurrentUser.getUserHandle()) |
| == handle) { |
| // Intentionally empty so that this profile gets added. |
| } else if (!mUserManager.isManagedProfile(handle.getIdentifier())) { |
| continue; |
| } |
| } |
| |
| // Ensure the system user doesn't get added twice. |
| if (result.contains(UserId.of(handle))) continue; |
| result.add(UserId.of(handle)); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Checks if a package is installed for a given user. |
| * |
| * @param userHandle The ID of the user. |
| * @return {@code true} if the package is installed for the user, {@code false} otherwise. |
| */ |
| @RequiresPermission( |
| anyOf = { |
| "android.permission.MANAGE_USERS", |
| "android.permission.INTERACT_ACROSS_USERS" |
| }) |
| private boolean isPackageInstalledForUser(UserHandle userHandle) { |
| String packageName = mContext.getPackageName(); |
| try { |
| Context userPackageContext = |
| mContext.createPackageContextAsUser( |
| mContext.getPackageName(), 0 /* flags */, userHandle); |
| return userPackageContext != null; |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.w(TAG, "Package " + packageName + " not found for user " + userHandle); |
| return false; |
| } |
| } |
| |
| /** |
| * Checks if quiet mode is enabled for a given user. |
| * |
| * @param userHandle The UserHandle of the profile to check. |
| * @return {@code true} if quiet mode is enabled, {@code false} otherwise. |
| */ |
| private boolean isQuietModeEnabledForUser(UserHandle userHandle) { |
| return UserId.of(userHandle.getIdentifier()).isQuietModeEnabled(mContext); |
| } |
| |
| /** |
| * Checks if a profile should be allowed, taking into account quiet mode and package |
| * installation. |
| * |
| * @param userHandle The UserHandle of the profile to check. |
| * @return {@code true} if the profile should be allowed, {@code false} otherwise. |
| */ |
| @SuppressLint("NewApi") |
| @RequiresPermission( |
| anyOf = { |
| "android.permission.MANAGE_USERS", |
| "android.permission.INTERACT_ACROSS_USERS" |
| }) |
| private boolean isProfileAllowed(UserHandle userHandle) { |
| final UserProperties userProperties = mUserManager.getUserProperties(userHandle); |
| |
| // 1. Check if the package is installed for the user |
| if (!isPackageInstalledForUser(userHandle)) { |
| Log.w( |
| TAG, |
| "Package " |
| + mContext.getPackageName() |
| + " is not installed for user " |
| + userHandle); |
| return false; |
| } |
| |
| // 2. Check user properties and quiet mode |
| if (userProperties.getShowInSharingSurfaces() |
| == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { |
| // Return true if profile is not in quiet mode or if it is in quiet mode |
| // then its user properties do not require it to be hidden |
| return !isQuietModeEnabledForUser(userHandle) |
| || userProperties.getShowInQuietMode() |
| != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; |
| } |
| |
| return false; |
| } |
| |
| private void getUserIdToLabelMapInternal() { |
| if (SdkLevel.isAtLeastV()) { |
| getUserIdToLabelMapInternalPostV(); |
| } else { |
| getUserIdToLabelMapInternalPreV(); |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private void getUserIdToLabelMapInternalPostV() { |
| if (mUserManager == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return; |
| } |
| List<UserId> userIds = getUserIds(); |
| for (UserId userId : userIds) { |
| synchronized (mUserIdToLabelMap) { |
| mUserIdToLabelMap.put(userId, getProfileLabel(userId)); |
| } |
| } |
| } |
| |
| private void getUserIdToLabelMapInternalPreV() { |
| if (mUserManager == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return; |
| } |
| List<UserId> userIds = getUserIds(); |
| for (UserId userId : userIds) { |
| if (mUserManager.isManagedProfile(userId.getIdentifier())) { |
| synchronized (mUserIdToLabelMap) { |
| mUserIdToLabelMap.put( |
| userId, getEnterpriseString(WORK_TAB, R.string.work_tab)); |
| } |
| } else { |
| synchronized (mUserIdToLabelMap) { |
| mUserIdToLabelMap.put( |
| userId, getEnterpriseString(PERSONAL_TAB, R.string.personal_tab)); |
| } |
| } |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private String getProfileLabel(UserId userId) { |
| if (userId.getIdentifier() == ActivityManager.getCurrentUser()) { |
| return getEnterpriseString(PERSONAL_TAB, R.string.personal_tab); |
| } |
| try { |
| Context userContext = |
| mContext.createContextAsUser( |
| UserHandle.of(userId.getIdentifier()), 0 /* flags */); |
| UserManager userManagerAsUser = userContext.getSystemService(UserManager.class); |
| if (userManagerAsUser == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return null; |
| } |
| return userManagerAsUser.getProfileLabel(); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception occurred while trying to get profile label:\n" + e); |
| return null; |
| } |
| } |
| |
| private String getEnterpriseString(String updatableStringId, int defaultStringId) { |
| if (SdkLevel.isAtLeastT()) { |
| return getUpdatableEnterpriseString(updatableStringId, defaultStringId); |
| } else { |
| return mContext.getString(defaultStringId); |
| } |
| } |
| |
| @RequiresApi(Build.VERSION_CODES.TIRAMISU) |
| private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { |
| DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); |
| if (Objects.equal(dpm, null)) { |
| Log.e(TAG, "can not get device policy manager"); |
| return mContext.getString(defaultStringId); |
| } |
| return dpm.getResources() |
| .getString(updatableStringId, () -> mContext.getString(defaultStringId)); |
| } |
| |
| private void getUserIdToBadgeMapInternal() { |
| if (SdkLevel.isAtLeastV()) { |
| getUserIdToBadgeMapInternalPostV(); |
| } else { |
| getUserIdToBadgeMapInternalPreV(); |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private void getUserIdToBadgeMapInternalPostV() { |
| if (mUserManager == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return; |
| } |
| List<UserId> userIds = getUserIds(); |
| for (UserId userId : userIds) { |
| synchronized (mUserIdToBadgeMap) { |
| mUserIdToBadgeMap.put(userId, getProfileBadge(userId)); |
| } |
| } |
| } |
| |
| private void getUserIdToBadgeMapInternalPreV() { |
| if (!SdkLevel.isAtLeastR()) return; |
| if (mUserManager == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return; |
| } |
| List<UserId> userIds = getUserIds(); |
| for (UserId userId : userIds) { |
| if (mUserManager.isManagedProfile(userId.getIdentifier())) { |
| synchronized (mUserIdToBadgeMap) { |
| mUserIdToBadgeMap.put( |
| userId, |
| SdkLevel.isAtLeastT() |
| ? getWorkProfileBadge() |
| : mContext.getDrawable(R.drawable.ic_briefcase)); |
| } |
| } |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private Drawable getProfileBadge(UserId userId) { |
| if (userId.getIdentifier() == ActivityManager.getCurrentUser()) { |
| return null; |
| } |
| try { |
| Context userContext = |
| mContext.createContextAsUser( |
| UserHandle.of(userId.getIdentifier()), 0 /* flags */); |
| UserManager userManagerAsUser = userContext.getSystemService(UserManager.class); |
| if (userManagerAsUser == null) { |
| Log.e(TAG, "cannot obtain user manager"); |
| return null; |
| } |
| return userManagerAsUser.getUserBadge(); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception occurred while trying to get profile badge:\n" + e); |
| return null; |
| } |
| } |
| |
| @RequiresApi(Build.VERSION_CODES.TIRAMISU) |
| private Drawable getWorkProfileBadge() { |
| DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); |
| Drawable drawable = |
| dpm.getResources() |
| .getDrawable( |
| WORK_PROFILE_ICON, |
| SOLID_COLORED, |
| () -> mContext.getDrawable(R.drawable.ic_briefcase)); |
| return drawable; |
| } |
| |
| /** |
| * Updates Cross Profile access for all UserProfiles in {@code getUserIds()} |
| * |
| * <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. |
| * |
| * <ol> |
| * <li>UserProperties attributes for CrossProfileDelegation are checked first. When the |
| * profile delegates to the parent profile, the parent's access is used. |
| * <li>{@link CrossProfileIntentForwardingActivity}s are resolved via the process owner's |
| * PackageManager, and are considered when evaluating cross profile to the target |
| * profile. |
| * </ol> |
| * |
| * <p>In the event none of the above checks succeeds, the profile is considered to be |
| * inaccessible to the current process user. |
| * |
| * @param intent The intent Photopicker is currently running under, for |
| * CrossProfileForwardActivity checking. |
| */ |
| private void getCanForwardToProfileIdMapInternal(Intent intent) { |
| |
| synchronized (mCanForwardToProfileIdMap) { |
| mCanForwardToProfileIdMap.clear(); |
| for (UserId userId : getUserIds()) { |
| mCanForwardToProfileIdMap.put( |
| userId, |
| isCrossProfileAllowedToUser( |
| mContext, intent, mCurrentUser, userId)); |
| } |
| } |
| } |
| |
| /** |
| * Determines if the provided UserIds support CrossProfile content sharing. |
| * |
| * <p>This method accepts a pair of user handles (from/to) and determines if CrossProfile |
| * access is permitted between those two profiles. |
| * |
| * <p>There are differences is on how the access is determined based on the platform SDK: |
| * |
| * <p>For Platform SDK < V: |
| * |
| * <p>A check for CrossProfileIntentForwarders in the origin (from) profile that target the |
| * destination (to) profile. If such a forwarder exists, then access is allowed, and denied |
| * otherwise. |
| * |
| * <p>For Platform SDK >= V: |
| * |
| * <p>The method now takes into account access delegation, which was first added in Android |
| * V. |
| * |
| * <p>For profiles that set the [CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT] |
| * property in its [UserProperties], its parent profile will be substituted in for its side |
| * of the check. |
| * |
| * <p>ex. For access checks between a Managed (from) and Private (to) profile, where: - |
| * Managed does not delegate to its parent - Private delegates to its parent |
| * |
| * <p>The following logic is performed: Managed -> parent(Private) |
| * |
| * <p>The same check in the other direction would yield: parent(Private) -> Managed |
| * |
| * <p>Note how the private profile is never actually used for either side of the check, |
| * since it is delegating its access check to the parent. And thus, if Managed can access |
| * the parent, it can also access the private. |
| * |
| * @param context Current context object, for switching user contexts. |
| * @param intent The current intent the Photopicker is running under. |
| * @param fromUser The Origin profile, where the user is coming from |
| * @param toUser The destination profile, where the user is attempting to go to. |
| * @return Whether CrossProfile content sharing is supported in this handle. |
| */ |
| private boolean isCrossProfileAllowedToUser( |
| Context context, Intent intent, UserId fromUser, UserId toUser) { |
| |
| // Early exit conditions, accessing self. |
| // NOTE: It is also possible to reach this state if this method is recursively checking |
| // from: parent(A) to:parent(B) where A and B are both children of the same parent. |
| if (fromUser.getIdentifier() == toUser.getIdentifier()) { |
| return true; |
| } |
| |
| // Decide if we should use actual from or parent(from) |
| UserHandle currentFromUser = |
| getProfileToCheckCrossProfileAccess(fromUser.getUserHandle()); |
| |
| // Decide if we should use actual to or parent(to) |
| UserHandle currentToUser = getProfileToCheckCrossProfileAccess(toUser.getUserHandle()); |
| |
| // When the from/to has changed from the original parameters, recursively restart the |
| // checks with the new from/to handles. |
| if (fromUser.getIdentifier() != currentFromUser.getIdentifier() |
| || toUser.getIdentifier() != currentToUser.getIdentifier()) { |
| return isCrossProfileAllowedToUser( |
| context, intent, UserId.of(currentFromUser), UserId.of(currentToUser)); |
| } |
| |
| PackageManager pm = context.getPackageManager(); |
| return doesCrossProfileIntentForwarderExist(intent, pm, fromUser, toUser); |
| } |
| |
| /** |
| * Determines if the target UserHandle delegates its content sharing to its parent. |
| * |
| * @param userHandle The target handle to check delegation for. |
| * @return TRUE if V+ and the handle delegates to parent. False otherwise. |
| */ |
| private boolean isCrossProfileStrategyDelegatedToParent(UserHandle userHandle) { |
| if (SdkLevel.isAtLeastV()) { |
| if (mUserManager == null) { |
| Log.e(TAG, "Cannot obtain user manager"); |
| return false; |
| } |
| UserProperties userProperties = mUserManager.getUserProperties(userHandle); |
| if (userProperties.getCrossProfileContentSharingStrategy() |
| == userProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Acquires the correct {@link UserHandle} which should be used for CrossProfile access |
| * checks. |
| * |
| * @param userHandle the origin handle. |
| * @return The UserHandle that should be used for cross profile access checks. In the event |
| * the origin handle delegates its access, this may not be the same handle as the origin |
| * handle. |
| */ |
| private UserHandle getProfileToCheckCrossProfileAccess(UserHandle userHandle) { |
| if (mUserManager == null) { |
| Log.e(TAG, "Cannot obtain user manager"); |
| return null; |
| } |
| return isCrossProfileStrategyDelegatedToParent(userHandle) |
| ? mUserManager.getProfileParent(userHandle) |
| : userHandle; |
| } |
| |
| /** |
| * Looks for a matching CrossProfileIntentForwardingActivity in the targetUserId for the |
| * given intent. |
| * |
| * @param intent The intent the forwarding activity needs to match. |
| * @param targetUserId The target user to check for. |
| * @return whether a CrossProfileIntentForwardingActivity could be found for the given |
| * intent, and user. |
| */ |
| private boolean doesCrossProfileIntentForwarderExist( |
| Intent intent, PackageManager pm, UserId fromUser, UserId targetUserId) { |
| |
| final Intent intentToCheck = (Intent) intent.clone(); |
| intentToCheck.setComponent(null); |
| intentToCheck.setPackage(null); |
| |
| for (ResolveInfo resolveInfo : |
| pm.queryIntentActivitiesAsUser( |
| intentToCheck, |
| PackageManager.MATCH_DEFAULT_ONLY, |
| fromUser.getUserHandle())) { |
| |
| 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 activityTargetUserId = (int) targetUserIdField.get(resolveInfo); |
| |
| if (activityTargetUserId == targetUserId.getIdentifier()) { |
| |
| // Found a match for this profile |
| return true; |
| } |
| |
| } 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); |
| return false; |
| } catch (Exception ex) { |
| Log.e(TAG, "Exception occurred during cross profile checks", ex); |
| } |
| } |
| } |
| |
| // No match found, so return false. |
| return false; |
| } |
| |
| @SuppressLint("NewApi") |
| private boolean isCrossProfileContentSharingStrategyDelegatedFromParent( |
| UserHandle userHandle) { |
| if (mUserManager == null) { |
| Log.e(TAG, "can not obtain user manager"); |
| return false; |
| } |
| UserProperties userProperties = mUserManager.getUserProperties(userHandle); |
| if (java.util.Objects.equals(userProperties, null)) { |
| Log.e(TAG, "can not obtain user properties"); |
| return false; |
| } |
| |
| return userProperties.getCrossProfileContentSharingStrategy() |
| == UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT; |
| } |
| |
| private static boolean isDeviceSupported(Context context) { |
| // The feature requires Android R DocumentsContract APIs and |
| // INTERACT_ACROSS_USERS_FULL permission. |
| return VersionUtils.isAtLeastR() |
| && context.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS) |
| == PackageManager.PERMISSION_GRANTED; |
| } |
| } |
| } |