blob: 3223c3ca2ce20eeac58a2acc0da3684158b62df7 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.R;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.IUserManager;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Robolectric implementation of {@link android.os.UserManager}.
*/
@Implements(value = UserManager.class, minSdk = JELLY_BEAN_MR1)
public class ShadowUserManager {
/**
* The default user ID user for secondary user testing, when the ID is not otherwise specified.
*/
public static final int DEFAULT_SECONDARY_USER_ID = 10;
public static final int FLAG_PRIMARY = UserInfo.FLAG_PRIMARY;
public static final int FLAG_ADMIN = UserInfo.FLAG_ADMIN;
public static final int FLAG_GUEST = UserInfo.FLAG_GUEST;
public static final int FLAG_RESTRICTED = UserInfo.FLAG_RESTRICTED;
public static final int FLAG_MANAGED_PROFILE = UserInfo.FLAG_MANAGED_PROFILE;
private static Map<Integer, Integer> userPidMap = new HashMap<>();
@RealObject private UserManager realObject;
private boolean userUnlocked = true;
private boolean managedProfile = false;
private boolean isSystemUser = true;
private Map<Integer, Bundle> userRestrictions = new HashMap<>();
private BiMap<UserHandle, Long> userProfiles = HashBiMap.create();
private Map<String, Bundle> applicationRestrictions = new HashMap<>();
private long nextUserSerial = 0;
private Map<Integer, UserState> userState = new HashMap<>();
private Map<Integer, UserInfo> userInfoMap = new HashMap<>();
private Map<Integer, List<UserInfo>> profiles = new HashMap<>();
private Map<Integer, Integer> profileToParent = new HashMap<>();
private Context context;
private boolean enforcePermissions;
private boolean canSwitchUser = false;
@Implementation
protected void __constructor__(Context context, IUserManager service) {
this.context = context;
addUser(UserHandle.USER_SYSTEM, "system_user", UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN);
}
/**
* Compared to real Android, there is no check that the package name matches the application
* package name and the method returns instantly.
*
* @see #setApplicationRestrictions(String, Bundle)
*/
@Implementation(minSdk = JELLY_BEAN_MR2)
protected Bundle getApplicationRestrictions(String packageName) {
Bundle bundle = applicationRestrictions.get(packageName);
return bundle != null ? bundle : new Bundle();
}
/**
* Sets the value returned by {@link UserManager#getApplicationRestrictions(String)}.
*/
public void setApplicationRestrictions(String packageName, Bundle restrictions) {
applicationRestrictions.put(packageName, restrictions);
}
/**
* Adds a profile associated for the user that the calling process is running on.
*
* The user is assigned an arbitrary unique serial number.
*
* @return the user's serial number
*/
public long addUserProfile(UserHandle userHandle) {
long serialNumber = nextUserSerial++;
userProfiles.put(userHandle, serialNumber);
return serialNumber;
}
@Implementation(minSdk = LOLLIPOP)
protected List<UserHandle> getUserProfiles() {
return ImmutableList.copyOf(userProfiles.keySet());
}
/**
* If any profiles have been added using {@link #addProfile}, return those profiles.
*
* Otherwise follow real android behaviour.
*/
@Implementation(minSdk = LOLLIPOP)
protected List<UserInfo> getProfiles(int userHandle) {
if (profiles.containsKey(userHandle)) {
return ImmutableList.copyOf(profiles.get(userHandle));
}
if (profileToParent.containsKey(userHandle)
&& profiles.containsKey(profileToParent.get(userHandle))) {
return ImmutableList.copyOf(profiles.get(profileToParent.get(userHandle)));
}
return directlyOn(
realObject, UserManager.class, "getProfiles", ClassParameter.from(int.class, userHandle));
}
/** Add a profile to be returned by {@link #getProfiles(int)}.**/
public void addProfile(
int userHandle, int profileUserHandle, String profileName, int profileFlags) {
UserInfo userInfo = new UserInfo(profileUserHandle, profileName, profileFlags);
profiles.putIfAbsent(userHandle, new ArrayList<>());
profiles.get(userHandle).add(userInfo);
userInfoMap.put(profileUserHandle, userInfo);
profileToParent.put(profileUserHandle, userHandle);
}
/**
* If this profile has been added using {@link #addProfile}, return its parent.
*/
@Implementation(minSdk = LOLLIPOP)
protected UserInfo getProfileParent(int userHandle) {
if (!profileToParent.containsKey(userHandle)) {
return null;
}
return userInfoMap.get(profileToParent.get(userHandle));
}
@Implementation(minSdk = N)
protected boolean isUserUnlocked() {
return userUnlocked;
}
/**
* Setter for {@link UserManager#isUserUnlocked()}
*/
public void setUserUnlocked(boolean userUnlocked) {
this.userUnlocked = userUnlocked;
}
/**
* If permissions are enforced (see {@link #enforcePermissionChecks(boolean)}) and the application
* doesn't have the {@link android.Manifest.permission#MANAGE_USERS} permission, throws a
* {@link SecurityManager} exception.
*
* @return `false` by default, or the value specified via {@link #setManagedProfile(boolean)}
* @see #enforcePermissionChecks(boolean)
* @see #setManagedProfile(boolean)
*/
@Implementation(minSdk = LOLLIPOP)
protected boolean isManagedProfile() {
if (enforcePermissions && !hasManageUsersPermission()) {
throw new SecurityException(
"You need MANAGE_USERS permission to: check if specified user a " +
"managed profile outside your profile group");
}
return managedProfile;
}
/**
* If permissions are enforced (see {@link #enforcePermissionChecks(boolean)}) and the application
* doesn't have the {@link android.Manifest.permission#MANAGE_USERS} permission, throws a {@link
* SecurityManager} exception.
*
* @return true if the profile added has FLAG_MANAGED_PROFILE
* @see #enforcePermissionChecks(boolean)
* @see #addProfile(int, int, String, int)
* @see #addUser(int, String, int)
*/
@Implementation(minSdk = N)
protected boolean isManagedProfile(int userHandle) {
if (enforcePermissions && !hasManageUsersPermission()) {
throw new SecurityException(
"You need MANAGE_USERS permission to: check if specified user a "
+ "managed profile outside your profile group");
}
UserInfo info = getUserInfo(userHandle);
return info != null && ((info.flags & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE);
}
// BEGIN-INTERNAL
@Implementation(minSdk = R)
protected boolean isProfile() {
return isManagedProfile();
}
/**
* Compared to real Android, userId is not used, instead
* managedProfile determines if user has badge.
*
* @param userId ignored, uses managedProfile field
* @return true if managedProfile field is true
*/
@Implementation(minSdk = R)
protected boolean hasBadge(int userId) {
return isProfile();
}
// END-INTERNAL
public void enforcePermissionChecks(boolean enforcePermissions) {
this.enforcePermissions = enforcePermissions;
}
/**
* Setter for {@link UserManager#isManagedProfile()}.
*/
public void setManagedProfile(boolean managedProfile) {
this.managedProfile = managedProfile;
}
@Implementation(minSdk = LOLLIPOP)
protected boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) {
Bundle bundle = userRestrictions.get(userHandle.getIdentifier());
return bundle != null && bundle.getBoolean(restrictionKey);
}
// BEGIN-INTERNAL
@Implementation(minSdk = R)
protected boolean hasUserRestrictionForUser(String restrictionKey, UserHandle userHandle) {
return hasUserRestriction(restrictionKey, userHandle);
}
// END-INTERNAL
public void setUserRestriction(UserHandle userHandle, String restrictionKey, boolean value) {
Bundle bundle = getUserRestrictionsForUser(userHandle);
bundle.putBoolean(restrictionKey, value);
}
/**
* Removes all user restrictions set of a user identified by {@code userHandle}.
*/
public void clearUserRestrictions(UserHandle userHandle) {
userRestrictions.remove(userHandle.getIdentifier());
}
@Implementation(minSdk = JELLY_BEAN_MR2)
protected Bundle getUserRestrictions(UserHandle userHandle) {
return new Bundle(getUserRestrictionsForUser(userHandle));
}
private Bundle getUserRestrictionsForUser(UserHandle userHandle) {
Bundle bundle = userRestrictions.get(userHandle.getIdentifier());
if (bundle == null) {
bundle = new Bundle();
userRestrictions.put(userHandle.getIdentifier(), bundle);
}
return bundle;
}
/**
* @see #addUserProfile(UserHandle)
*/
@Implementation
protected long getSerialNumberForUser(UserHandle userHandle) {
Long result = userProfiles.get(userHandle);
return result == null ? -1L : result;
}
/**
* @deprecated prefer {@link #addUserProfile(UserHandle)} to ensure consistency of profiles known
* to the {@link UserManager}. Furthermore, calling this method for the current user, i.e: {@link
* Process#myUserHandle()} is no longer necessary as this user is always known to UserManager and
* has a preassigned serial number.
*/
@Deprecated
public void setSerialNumberForUser(UserHandle userHandle, long serialNumber) {
userProfiles.put(userHandle, serialNumber);
}
/**
* @see #addUserProfile(UserHandle)
*/
@Implementation
protected UserHandle getUserForSerialNumber(long serialNumber) {
return userProfiles.inverse().get(serialNumber);
}
/**
* @see #addProfile(int, int, String, int)
* @see #addUser(int, String, int)
*/
@Implementation
protected int getUserSerialNumber(@UserIdInt int userHandle) {
Long result = userProfiles.get(UserHandle.of(userHandle));
return result != null ? result.intValue() : -1;
}
private boolean hasManageUsersPermission() {
return context.getPackageManager().checkPermission(permission.MANAGE_USERS, context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
}
private void checkPermissions() {
// TODO Ensure permisions
// throw new SecurityException("You need INTERACT_ACROSS_USERS or MANAGE_USERS
// permission "
// + "to: check " + name);throw new SecurityException();
}
/**
* @return `false` by default, or the value specified via {@link #setIsDemoUser(boolean)}
*/
@Implementation(minSdk = N_MR1)
protected boolean isDemoUser() {
return getUserInfo(UserHandle.myUserId()).isDemo();
}
/**
* Sets that the current user is a demo user; controls the return value of {@link
* UserManager#isDemoUser()}.
*
* @deprecated Use {@link ShadowUserManager#addUser(int, String, int)} to create a demo user
* instead of changing default user flags.
*/
@Deprecated
public void setIsDemoUser(boolean isDemoUser) {
UserInfo userInfo = getUserInfo(UserHandle.myUserId());
if (isDemoUser) {
userInfo.flags |= UserInfo.FLAG_DEMO;
} else {
userInfo.flags &= ~UserInfo.FLAG_DEMO;
}
}
/**
* @return {@code false} by default, or the value specified via {@link #setIsAdminUser(boolean)}
*/
@Implementation(minSdk = N_MR1)
public boolean isAdminUser() {
return getUserInfo(UserHandle.myUserId()).isAdmin();
}
/**
* Sets that the current user is an admin user; controls the return value of
* {@link UserManager#isAdminUser}.
*/
public void setIsAdminUser(boolean isAdminUser) {
UserInfo userInfo = getUserInfo(UserHandle.myUserId());
if (isAdminUser) {
userInfo.flags |= UserInfo.FLAG_ADMIN;
} else {
userInfo.flags &= ~UserInfo.FLAG_ADMIN;
}
}
/**
* @return 'true' by default, or the value specified via {@link #setIsSystemUser(boolean)}
*/
@Implementation(minSdk = M)
protected boolean isSystemUser() {
if (isSystemUser == false) {
return false;
} else {
return directlyOn(realObject, UserManager.class, "isSystemUser");
}
}
/**
* Sets that the current user is the system user; controls the return value of {@link
* UserManager#isSystemUser()}.
*
* @deprecated Use {@link ShadowUserManager#addUser(int, String, int)} to create a system user
* instead of changing default user flags.
*/
@Deprecated
public void setIsSystemUser(boolean isSystemUser) {
this.isSystemUser = isSystemUser;
}
/**
* Sets that the current user is the primary user; controls the return value of {@link
* UserManager#isPrimaryUser()}.
*
* @deprecated Use {@link ShadowUserManager#addUser(int, String, int)} to create a primary user
* instead of changing default user flags.
*/
@Deprecated
public void setIsPrimaryUser(boolean isPrimaryUser) {
UserInfo userInfo = getUserInfo(UserHandle.myUserId());
if (isPrimaryUser) {
userInfo.flags |= UserInfo.FLAG_PRIMARY;
} else {
userInfo.flags &= ~UserInfo.FLAG_PRIMARY;
}
}
/**
* @return 'false' by default, or the value specified via {@link #setIsLinkedUser(boolean)}
*/
@Implementation(minSdk = JELLY_BEAN_MR2)
protected boolean isLinkedUser() {
return getUserInfo(UserHandle.myUserId()).isRestricted();
}
/**
* Sets that the current user is the linked user; controls the return value of {@link
* UserManager#isLinkedUser()}.
*
* @deprecated Use {@link ShadowUserManager#addUser(int, String, int)} to create a linked user
* instead of changing default user flags.
*/
@Deprecated
public void setIsLinkedUser(boolean isLinkedUser) {
UserInfo userInfo = getUserInfo(UserHandle.myUserId());
if (isLinkedUser) {
userInfo.flags |= UserInfo.FLAG_RESTRICTED;
} else {
userInfo.flags &= ~UserInfo.FLAG_RESTRICTED;
}
}
/**
* Sets that the current user is the guest user; controls the return value of {@link
* UserManager#isGuestUser()}.
*
* @deprecated Use {@link ShadowUserManager#addUser(int, String, int)} to create a guest user
* instead of changing default user flags.
*/
@Deprecated
public void setIsGuestUser(boolean isGuestUser) {
UserInfo userInfo = getUserInfo(UserHandle.myUserId());
if (isGuestUser) {
userInfo.flags |= UserInfo.FLAG_GUEST;
} else {
userInfo.flags &= ~UserInfo.FLAG_GUEST;
}
}
/**
* @see #setUserState(UserHandle, UserState)
*/
@Implementation
protected boolean isUserRunning(UserHandle handle) {
checkPermissions();
UserState state = userState.get(handle.getIdentifier());
if (state == UserState.STATE_RUNNING_LOCKED
|| state == UserState.STATE_RUNNING_UNLOCKED
|| state == UserState.STATE_RUNNING_UNLOCKING) {
return true;
} else {
return false;
}
}
/**
* @see #setUserState(UserHandle, UserState)
*/
@Implementation
protected boolean isUserRunningOrStopping(UserHandle handle) {
checkPermissions();
UserState state = userState.get(handle.getIdentifier());
if (state == UserState.STATE_RUNNING_LOCKED
|| state == UserState.STATE_RUNNING_UNLOCKED
|| state == UserState.STATE_RUNNING_UNLOCKING
|| state == UserState.STATE_STOPPING) {
return true;
} else {
return false;
}
}
/**
* Describes the current state of the user. State can be set using
* {@link #setUserState(UserHandle, UserState)}.
*/
public enum UserState {
// User is first coming up.
STATE_BOOTING,
// User is in the locked state.
STATE_RUNNING_LOCKED,
// User is in the unlocking state.
STATE_RUNNING_UNLOCKING,
// User is in the running state.
STATE_RUNNING_UNLOCKED,
// User is in the initial process of being stopped.
STATE_STOPPING,
// User is in the final phase of stopping, sending Intent.ACTION_SHUTDOWN.
STATE_SHUTDOWN
}
/**
* Sets the current state for a given user, see {@link UserManager#isUserRunning(UserHandle)}
* and {@link UserManager#isUserRunningOrStopping(UserHandle)}
*/
public void setUserState(UserHandle handle, UserState state) {
userState.put(handle.getIdentifier(), state);
}
@Implementation
protected List<UserInfo> getUsers() {
return new ArrayList<UserInfo>(userInfoMap.values());
}
@Implementation
protected UserInfo getUserInfo(int userHandle) {
return userInfoMap.get(userHandle);
}
/**
* Returns {@code true} by default, or the value specified via {@link #setCanSwitchUser(boolean)}.
*/
@Implementation(minSdk = N)
protected boolean canSwitchUsers() {
return canSwitchUser;
}
/**
* Sets whether switching users is allowed or not; controls the return value of {@link
* UserManager#canSwitchUser()}
*/
public void setCanSwitchUser(boolean canSwitchUser) {
this.canSwitchUser = canSwitchUser;
}
@Implementation(minSdk = JELLY_BEAN_MR1)
protected boolean removeUser(int userHandle) {
userInfoMap.remove(userHandle);
return true;
}
// BEGIN-INTERNAL
@Implementation(minSdk = R)
protected UserInfo createProfileForUserEvenWhenDisallowed(String name,
@NonNull String userType, @UserInfo.UserInfoFlag int flags, @UserIdInt int userId,
String[] disallowedPackages) throws UserManager.UserOperationException {
List<UserInfo> userIdProfiles = profiles.computeIfAbsent(userId, ignored -> new ArrayList<>());
int profileUserId = userIdProfiles.isEmpty() ? 10 : findMaxProfileId(userIdProfiles) + 1;
UserInfo profileUserInfo = new UserInfo(profileUserId, name, flags);
userIdProfiles.add(profileUserInfo);
profileToParent.put(profileUserId, userId);
addUserProfile(UserHandle.of(profileUserId));
return profileUserInfo;
}
/** Assumes the given list of profile infos is non-empty. */
private int findMaxProfileId(List<UserInfo> userIdProfiles) {
return Collections.max(
userIdProfiles.stream()
.map(userInfo -> userInfo.id)
.collect(Collectors.toList()));
}
// END-INTERNAL
/**
* Switches the current user to {@code userHandle}.
*
* @param userId the integer handle of the user, where 0 is the primary user.
*/
public void switchUser(int userId) {
if (!userInfoMap.containsKey(userId)) {
throw new UnsupportedOperationException("Must add user before switching to it");
}
ShadowProcess.setUid(userPidMap.get(userId));
}
/**
* Creates a user with the specified name, userId and flags.
*
* @param id the unique id of user
* @param name name of the user
* @param flags 16 bits for user type. See {@link UserInfo#flags}
*/
public void addUser(int id, String name, int flags) {
UserHandle userHandle =
id == UserHandle.USER_SYSTEM ? Process.myUserHandle() : new UserHandle(id);
addUserProfile(userHandle);
setSerialNumberForUser(userHandle, (long) id);
profiles.putIfAbsent(id, new ArrayList<>());
userInfoMap.put(id, new UserInfo(id, name, flags));
userPidMap.put(
id,
id == UserHandle.USER_SYSTEM
? Process.myUid()
: id * UserHandle.PER_USER_RANGE + ShadowProcess.getRandomApplicationUid());
}
@Resetter
public static void reset() {
if (userPidMap != null && userPidMap.isEmpty() == false) {
ShadowProcess.setUid(userPidMap.get(UserHandle.USER_SYSTEM));
userPidMap.clear();
userPidMap.put(UserHandle.USER_SYSTEM, Process.myUid());
}
}
}