/* * 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 static com.android.documentsui.base.SharedMinimal.DEBUG; 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.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.VisibleForTesting; import com.android.documentsui.base.Features; import com.android.documentsui.base.UserId; import com.android.documentsui.util.CrossProfileUtils; import com.android.documentsui.util.VersionUtils; import com.android.modules.utils.build.SdkLevel; import com.google.common.base.Objects; 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 getUserIds(); /** * Returns mapping between the {@link UserId} and the label for the profile */ Map getUserIdToLabelMap(); /** * Returns mapping between the {@link UserId} and the drawable badge for the profile * * returns {@code null} for non-profile userId */ Map 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 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 mUserIds = new ArrayList<>(); /** * Mapping between the {@link UserId} to the corresponding profile label */ @GuardedBy("mUserIdToLabelMap") private final Map mUserIdToLabelMap = new HashMap<>(); /** * Mapping between the {@link UserId} to the corresponding profile badge */ @GuardedBy("mUserIdToBadgeMap") private final Map 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("mCanFrowardToProfileIdMap") private final Map mCanFrowardToProfileIdMap = 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 (mCanFrowardToProfileIdMap) { mCanFrowardToProfileIdMap.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 getUserIds() { synchronized (mUserIds) { if (mUserIds.isEmpty()) { mUserIds.addAll(getUserIdsInternal()); } return mUserIds; } } @Override public Map getUserIdToLabelMap() { synchronized (mUserIdToLabelMap) { if (mUserIdToLabelMap.isEmpty()) { getUserIdToLabelMapInternal(); } return mUserIdToLabelMap; } } @Override public Map getUserIdToBadgeMap() { synchronized (mUserIdToBadgeMap) { if (mUserIdToBadgeMap.isEmpty()) { getUserIdToBadgeMapInternal(); } return mUserIdToBadgeMap; } } @Override public Map getCanForwardToProfileIdMap(Intent intent) { synchronized (mCanFrowardToProfileIdMap) { if (mCanFrowardToProfileIdMap.isEmpty()) { getCanForwardToProfileIdMapInternal(intent); } return mCanFrowardToProfileIdMap; } } @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 (mCanFrowardToProfileIdMap) { if (!mCanFrowardToProfileIdMap.containsKey(userId)) { if (userId.getIdentifier() == ActivityManager.getCurrentUser() || isCrossProfileContentSharingStrategyDelegatedFromParent( UserHandle.of(userId.getIdentifier())) || CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser, mContext.getPackageManager(), mCurrentStateIntent, mContext, mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) { mCanFrowardToProfileIdMap.put(userId, true); } else { mCanFrowardToProfileIdMap.put(userId, false); } } } } 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 getUserIdsInternal() { final List result = new ArrayList<>(); if (!mIsDeviceSupported) { result.add(mCurrentUser); return result; } if (mUserManager == null) { Log.e(TAG, "cannot obtain user manager"); result.add(mCurrentUser); return result; } final List userProfiles = mUserManager.getUserProfiles(); if (userProfiles.size() < 2) { result.add(mCurrentUser); return result; } if (SdkLevel.isAtLeastV()) { getUserIdsInternalPostV(userProfiles, result); } else { getUserIdsInternalPreV(userProfiles, result); } return result; } @SuppressLint("NewApi") private void getUserIdsInternalPostV(List userProfiles, List result) { for (UserHandle userHandle : userProfiles) { if (userHandle.getIdentifier() == ActivityManager.getCurrentUser()) { result.add(UserId.of(userHandle)); } else { // Out of all the profiles returned by user manager the profiles that are // returned should satisfy both the following conditions: // 1. It has user property SHOW_IN_SHARING_SURFACES_SEPARATE // 2. Quite mode is not enabled, if it is enabled then the profile's user // property is not SHOW_IN_QUIET_MODE_HIDDEN if (isProfileAllowed(userHandle)) { result.add(UserId.of(userHandle)); } } } if (result.isEmpty()) { result.add(mCurrentUser); } } @SuppressLint("NewApi") private boolean isProfileAllowed(UserHandle userHandle) { final UserProperties userProperties = mUserManager.getUserProperties(userHandle); if (userProperties.getShowInSharingSurfaces() == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { return !UserId.of(userHandle).isQuietModeEnabled(mContext) || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; } return false; } private void getUserIdsInternalPreV(List userProfiles, List result) { result.add(mCurrentUser); UserId systemUser = null; UserId managedUser = null; for (UserHandle userHandle : userProfiles) { if (userHandle.isSystem()) { systemUser = UserId.of(userHandle); } else if (mUserManager.isManagedProfile(userHandle.getIdentifier())) { managedUser = UserId.of(userHandle); } } if (mCurrentUser.isSystem() && managedUser != null) { result.add(managedUser); } else if (mCurrentUser.isManagedProfile(mUserManager) && systemUser != null) { result.add(0, systemUser); } else { if (DEBUG) { Log.w(TAG, "The current user " + UserId.CURRENT_USER + " is neither system nor managed user. has system user: " + (systemUser != null)); } } } 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 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 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 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 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; } private void getCanForwardToProfileIdMapInternal(Intent intent) { // Versions less than V will not have the user properties required to determine whether // cross profile check is delegated from parent or not if (!SdkLevel.isAtLeastV()) { getCanForwardToProfileIdMapPreV(intent); return; } if (mUserManager == null) { Log.e(TAG, "can not get user manager"); return; } List parentOrDelegatedFromParent = new ArrayList<>(); List canForwardToProfileIds = new ArrayList<>(); List noDelegation = new ArrayList<>(); List userIds = getUserIds(); for (UserId userId : userIds) { final UserHandle userHandle = UserHandle.of(userId.getIdentifier()); // Parent (personal) profile and all the child profiles that delegate cross profile // content sharing check to parent can share among each other if (userId.getIdentifier() == ActivityManager.getCurrentUser() || isCrossProfileContentSharingStrategyDelegatedFromParent(userHandle)) { parentOrDelegatedFromParent.add(userId); } else { noDelegation.add(userId); } } if (noDelegation.size() > 1) { Log.e(TAG, "There cannot be more than one profile delegating cross profile " + "content sharing check from self."); } /* * Cross profile resolve info need to be checked in the following 2 cases: * 1. current user is either parent or delegates check to parent and the target user * does not delegate to parent * 2. current user does not delegate check to the parent and the target user is the * parent profile */ UserId needToCheck = null; if (parentOrDelegatedFromParent.contains(mCurrentUser) && !noDelegation.isEmpty()) { needToCheck = noDelegation.get(0); } else if (mCurrentUser.getIdentifier() != ActivityManager.getCurrentUser()) { final UserHandle parentProfile = mUserManager.getProfileParent( UserHandle.of(mCurrentUser.getIdentifier())); needToCheck = UserId.of(parentProfile); } if (needToCheck != null && CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser, mContext.getPackageManager(), intent, mContext, mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) { if (parentOrDelegatedFromParent.contains(needToCheck)) { canForwardToProfileIds.addAll(parentOrDelegatedFromParent); } else { canForwardToProfileIds.add(needToCheck); } } if (parentOrDelegatedFromParent.contains(mCurrentUser)) { canForwardToProfileIds.addAll(parentOrDelegatedFromParent); } for (UserId userId : userIds) { synchronized (mCanFrowardToProfileIdMap) { if (userId.equals(mCurrentUser)) { mCanFrowardToProfileIdMap.put(userId, true); continue; } mCanFrowardToProfileIdMap.put(userId, canForwardToProfileIds.contains(userId)); } } } @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 void getCanForwardToProfileIdMapPreV(Intent intent) { // There only two profiles pre V List userIds = getUserIds(); for (UserId userId : userIds) { synchronized (mCanFrowardToProfileIdMap) { if (mCurrentUser.equals(userId)) { mCanFrowardToProfileIdMap.put(userId, true); } else { mCanFrowardToProfileIdMap.put(userId, CrossProfileUtils.getCrossProfileResolveInfo( mCurrentUser, mContext.getPackageManager(), intent, mContext, mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null); } } } } 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; } } }