blob: eccc6b00c018110e679d426e40db94c0e9968f71 [file] [log] [blame]
/*
* 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;
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
*
* 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 or
* {@link Intent}.ACTION_PROFILE_AVAILABLE
*/
void onProfileActionStatusChange(String action, UserId userId);
/**
* Sets the intent that triggered the launch of the DocsUI
*/
void setCurrentStateIntent(Intent intent);
/**
* 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("mCanFrowardToProfileIdMap")
private final Map<UserId, Boolean> 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<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 (mCanFrowardToProfileIdMap) {
if (mCanFrowardToProfileIdMap.isEmpty()) {
getCanForwardToProfileIdMapInternal(intent);
}
return mCanFrowardToProfileIdMap;
}
}
@Override
@SuppressLint("NewApi")
public void onProfileActionStatusChange(String action, UserId userId) {
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)) {
synchronized (mUserIds) {
mUserIds.remove(userId);
}
} else if (Intent.ACTION_PROFILE_AVAILABLE.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;
}
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");
result.add(mCurrentUser);
return result;
}
final List<UserHandle> 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<UserHandle> userProfiles, List<UserId> 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<UserHandle> userProfiles, List<UserId> 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<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;
}
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<UserId> parentOrDelegatedFromParent = new ArrayList<>();
List<UserId> canForwardToProfileIds = new ArrayList<>();
List<UserId> noDelegation = new ArrayList<>();
List<UserId> 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<UserId> 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;
}
}
}