blob: cf0891a894837b3642774d33b1c5bb030f2a68ac [file] [log] [blame]
/*
* Copyright (C) 2021 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.bedstead.nene.users;
import static android.Manifest.permission.CREATE_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.Process.myUserHandle;
import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME;
import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME;
import static com.android.bedstead.nene.users.UserType.SYSTEM_USER_TYPE_NAME;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.bedstead.nene.exceptions.AdbParseException;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.permissions.PermissionContext;
import com.android.bedstead.nene.permissions.Permissions;
import com.android.bedstead.nene.utils.Poll;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.bedstead.nene.utils.Versions;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class Users {
static final int SYSTEM_USER_ID = 0;
private static final Duration WAIT_FOR_USER_TIMEOUT = Duration.ofMinutes(4);
private Map<Integer, AdbUser> mCachedUsers = null;
private Map<String, UserType> mCachedUserTypes = null;
private Set<UserType> mCachedUserTypeValues = null;
private final AdbUserParser mParser;
private static final UserManager sUserManager =
TestApis.context().instrumentedContext().getSystemService(UserManager.class);
private Map<Integer, UserReference> mUsers = new ConcurrentHashMap<>();
public static final Users sInstance = new Users();
private Users() {
mParser = AdbUserParser.get(SDK_INT);
}
/** Get all {@link UserReference}s on the device. */
public Collection<UserReference> all() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
fillCache();
return mCachedUsers.keySet().stream().map(UserReference::new)
.collect(Collectors.toSet());
}
return users().map(
ui -> find(ui.id)
).collect(Collectors.toSet());
}
/**
* Gets a {@link UserReference} for the initial user for the device.
*
* <p>This will be the {@link #system()} user on most systems.</p>
*/
public UserReference initial() {
if (!isHeadlessSystemUserMode()) {
return system();
}
if (TestApis.packages().features().contains("android.hardware.type.automotive")) {
try {
return ShellCommand.builder("cmd car_service get-initial-user")
.executeAndParseOutput(i -> find(Integer.parseInt(i.trim())));
} catch (AdbException e) {
throw new NeneException("Error finding initial user on Auto", e);
}
}
List<UserReference> users = new ArrayList<>(all());
users.sort(Comparator.comparingInt(UserReference::id));
for (UserReference user : users) {
if (user.parent() == null) {
return user;
}
}
throw new NeneException("No initial user available");
}
/** Get a {@link UserReference} for the user currently switched to. */
public UserReference current() {
if (Versions.meetsMinimumSdkVersionRequirement(S)) {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
return find(ActivityManager.getCurrentUser());
}
}
try {
return find((int) ShellCommand.builder("am get-current-user")
.executeAndParseOutput(i -> Integer.parseInt(i.trim())));
} catch (AdbException e) {
throw new NeneException("Error getting current user", e);
}
}
/** Get a {@link UserReference} for the user running the current test process. */
public UserReference instrumented() {
return find(myUserHandle());
}
/** Get a {@link UserReference} for the system user. */
public UserReference system() {
return find(0);
}
/** Get a {@link UserReference} by {@code id}. */
public UserReference find(int id) {
if (!mUsers.containsKey(id)) {
mUsers.put(id, new UserReference(id));
}
return mUsers.get(id);
}
/** Get a {@link UserReference} by {@code userHandle}. */
public UserReference find(UserHandle userHandle) {
return find(userHandle.getIdentifier());
}
/** Get all supported {@link UserType}s. */
public Set<UserType> supportedTypes() {
// TODO(b/203557600): Stop using adb
ensureSupportedTypesCacheFilled();
return mCachedUserTypeValues;
}
/** Get a {@link UserType} with the given {@code typeName}, or {@code null} */
@Nullable
public UserType supportedType(String typeName) {
ensureSupportedTypesCacheFilled();
return mCachedUserTypes.get(typeName);
}
/**
* Find all users which have the given {@link UserType}.
*/
public Set<UserReference> findUsersOfType(UserType userType) {
if (userType == null) {
throw new NullPointerException();
}
if (userType.baseType().contains(UserType.BaseType.PROFILE)) {
throw new NeneException("Cannot use findUsersOfType with profile type " + userType);
}
return all().stream()
.filter(u -> u.type().equals(userType))
.collect(Collectors.toSet());
}
/**
* Find a single user which has the given {@link UserType}.
*
* <p>If there are no users of the given type, {@code Null} will be returned.
*
* <p>If there is more than one user of the given type, {@link NeneException} will be thrown.
*/
@Nullable
public UserReference findUserOfType(UserType userType) {
Set<UserReference> users = findUsersOfType(userType);
if (users.isEmpty()) {
return null;
} else if (users.size() > 1) {
throw new NeneException("findUserOfType called but there is more than 1 user of type "
+ userType + ". Found: " + users);
}
return users.iterator().next();
}
/**
* Find all users which have the given {@link UserType} and the given parent.
*/
public Set<UserReference> findProfilesOfType(UserType userType, UserReference parent) {
if (userType == null || parent == null) {
throw new NullPointerException();
}
if (!userType.baseType().contains(UserType.BaseType.PROFILE)) {
throw new NeneException("Cannot use findProfilesOfType with non-profile type "
+ userType);
}
return all().stream()
.filter(u -> parent.equals(u.parent())
&& u.type().equals(userType))
.collect(Collectors.toSet());
}
/**
* Find all users which have the given {@link UserType} and the given parent.
*
* <p>If there are no users of the given type and parent, {@code Null} will be returned.
*
* <p>If there is more than one user of the given type and parent, {@link NeneException} will
* be thrown.
*/
@Nullable
public UserReference findProfileOfType(UserType userType, UserReference parent) {
Set<UserReference> profiles = findProfilesOfType(userType, parent);
if (profiles.isEmpty()) {
return null;
} else if (profiles.size() > 1) {
throw new NeneException("findProfileOfType called but there is more than 1 user of "
+ "type " + userType + " with parent " + parent + ". Found: " + profiles);
}
return profiles.iterator().next();
}
private void ensureSupportedTypesCacheFilled() {
if (mCachedUserTypes != null) {
// SupportedTypes don't change so don't need to be refreshed
return;
}
if (SDK_INT < Build.VERSION_CODES.R) {
mCachedUserTypes = new HashMap<>();
mCachedUserTypes.put(MANAGED_PROFILE_TYPE_NAME, managedProfileUserType());
mCachedUserTypes.put(SYSTEM_USER_TYPE_NAME, systemUserType());
mCachedUserTypes.put(SECONDARY_USER_TYPE_NAME, secondaryUserType());
mCachedUserTypeValues = new HashSet<>();
mCachedUserTypeValues.addAll(mCachedUserTypes.values());
return;
}
fillCache();
}
private UserType managedProfileUserType() {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = MANAGED_PROFILE_TYPE_NAME;
managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.PROFILE);
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = 1;
return new UserType(managedProfileMutableUserType);
}
private UserType systemUserType() {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = SYSTEM_USER_TYPE_NAME;
managedProfileMutableUserType.mBaseType =
Set.of(UserType.BaseType.FULL, UserType.BaseType.SYSTEM);
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = -1;
return new UserType(managedProfileMutableUserType);
}
private UserType secondaryUserType() {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = SECONDARY_USER_TYPE_NAME;
managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = -1;
return new UserType(managedProfileMutableUserType);
}
/**
* Create a new user.
*/
@CheckResult
public UserBuilder createUser() {
return new UserBuilder();
}
/**
* Get a {@link UserReference} to a user who does not exist.
*/
public UserReference nonExisting() {
Set<Integer> userIds;
if (Versions.meetsMinimumSdkVersionRequirement(S)) {
userIds = users().map(ui -> ui.id).collect(Collectors.toSet());
} else {
fillCache();
userIds = mCachedUsers.keySet();
}
int id = 0;
while (userIds.contains(id)) {
id++;
}
return find(id);
}
private void fillCache() {
try {
// TODO: Replace use of adb on supported versions of Android
String userDumpsysOutput = ShellCommand.builder("dumpsys user").execute();
AdbUserParser.ParseResult result = mParser.parse(userDumpsysOutput);
mCachedUsers = result.mUsers;
if (result.mUserTypes != null) {
mCachedUserTypes = result.mUserTypes;
} else {
ensureSupportedTypesCacheFilled();
}
Iterator<Map.Entry<Integer, AdbUser>> iterator = mCachedUsers.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, AdbUser> entry = iterator.next();
if (entry.getValue().isRemoving()) {
// We don't expose users who are currently being removed
iterator.remove();
continue;
}
AdbUser.MutableUser mutableUser = entry.getValue().mMutableUser;
if (SDK_INT < Build.VERSION_CODES.R) {
if (entry.getValue().id() == SYSTEM_USER_ID) {
mutableUser.mType = supportedType(SYSTEM_USER_TYPE_NAME);
mutableUser.mIsPrimary = true;
} else if (entry.getValue().hasFlag(AdbUser.FLAG_MANAGED_PROFILE)) {
mutableUser.mType =
supportedType(MANAGED_PROFILE_TYPE_NAME);
mutableUser.mIsPrimary = false;
} else {
mutableUser.mType =
supportedType(SECONDARY_USER_TYPE_NAME);
mutableUser.mIsPrimary = false;
}
}
if (SDK_INT < S) {
if (mutableUser.mType.baseType()
.contains(UserType.BaseType.PROFILE)) {
// We assume that all profiles before S were on the System User
mutableUser.mParent = find(SYSTEM_USER_ID);
}
}
}
mCachedUserTypeValues = new HashSet<>();
mCachedUserTypeValues.addAll(mCachedUserTypes.values());
} catch (AdbException | AdbParseException e) {
throw new RuntimeException("Error filling cache", e);
}
}
/**
* Block until the user with the given {@code userReference} to not exist or to be in the
* correct state.
*
* <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown.
*/
@Nullable
UserReference waitForUserToNotExistOrMatch(
UserReference userReference, Function<UserReference, Boolean> userChecker) {
return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ false);
}
@Nullable
private UserReference waitForUserToMatch(
UserReference userReference, Function<UserReference, Boolean> userChecker,
boolean waitForExist) {
// TODO(scottjonathan): This is pretty heavy because we resolve everything when we know we
// are throwing away everything except one user. Optimise
try {
return Poll.forValue("user", () -> userReference)
.toMeet((user) -> {
if (user == null) {
return !waitForExist;
}
return userChecker.apply(user);
}).timeout(WAIT_FOR_USER_TIMEOUT)
.errorOnFail("Expected user to meet requirement")
.await();
} catch (AssertionError e) {
if (!userReference.exists()) {
throw new NeneException(
"Timed out waiting for user state for user "
+ userReference + ". User does not exist.", e);
}
throw new NeneException(
"Timed out waiting for user state, current state " + userReference, e
);
}
}
/** See {@link UserManager#isHeadlessSystemUserMode()}. */
@SuppressWarnings("NewApi")
public boolean isHeadlessSystemUserMode() {
if (Versions.meetsMinimumSdkVersionRequirement(S)) {
return UserManager.isHeadlessSystemUserMode();
}
return false;
}
/**
* Set the stopBgUsersOnSwitch property.
*
* <p>This affects if background users will be swapped when switched away from on some devices.
*/
public void setStopBgUsersOnSwitch(int value) {
if (!Versions.meetsMinimumSdkVersionRequirement(S_V2)) {
return;
}
Context context = TestApis.context().instrumentedContext();
try (PermissionContext p = TestApis.permissions()
.withPermission(INTERACT_ACROSS_USERS)) {
context.getSystemService(ActivityManager.class).setStopUserOnSwitch(value);
}
}
@Nullable
AdbUser fetchUser(int id) {
fillCache();
return mCachedUsers.get(id);
}
static Stream<UserInfo> users() {
if (Permissions.sIgnorePermissions.get()) {
return sUserManager.getUsers(
/* excludePartial= */ false,
/* excludeDying= */ true,
/* excludePreCreated= */ false).stream();
}
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
return sUserManager.getUsers(
/* excludePartial= */ false,
/* excludeDying= */ true,
/* excludePreCreated= */ false).stream();
}
}
}