blob: 7f52a422f9045ff40e1809fa7f9d800b249f451a [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.os.Build.VERSION.SDK_INT;
import static android.os.Process.myUserHandle;
import android.os.Build;
import android.os.UserHandle;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
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.utils.ShellCommandUtils;
import com.android.compatibility.common.util.PollingCheck;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public final class Users {
private static final long WAIT_FOR_USER_TIMEOUT_MS = 1000 * 60;
private Map<Integer, User> mCachedUsers = null;
private Map<String, UserType> mCachedUserTypes = null;
private final AdbUserParser parser = AdbUserParser.get(this, SDK_INT);
/** Get all {@link User}s on the device. */
public Collection<User> all() {
fillCache();
return mCachedUsers.values();
}
/** 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) {
return new UnresolvedUser(this, id);
}
/** Get a {@link UserReference} by {@code userHandle}. */
public UserReference find(UserHandle userHandle) {
return new UnresolvedUser(this, userHandle.getIdentifier());
}
@Nullable
User fetchUser(int id) {
// TODO(scottjonathan): fillCache probably does more than we need here -
// can we make it more efficient?
fillCache();
return mCachedUsers.get(id);
}
/** Get all supported {@link UserType}s. */
@RequiresApi(Build.VERSION_CODES.R)
@Nullable
public Collection<UserType> supportedTypes() {
if (SDK_INT < Build.VERSION_CODES.R) {
return null;
}
if (mCachedUserTypes == null) {
// supportedTypes cannot change so we don't need to refill the cache
fillCache();
}
return mCachedUserTypes.values();
}
/** Get a {@link UserType} with the given {@code typeName}, or {@code null} */
@RequiresApi(Build.VERSION_CODES.R)
@Nullable
public UserType supportedType(String typeName) {
if (SDK_INT < Build.VERSION_CODES.R) {
return null;
}
if (mCachedUserTypes == null) {
// supportedTypes cannot change so we don't need to refill the cache
fillCache();
}
return mCachedUserTypes.get(typeName);
}
/**
* Create a new user.
*/
public UserBuilder createUser() {
return new UserBuilder(this);
}
private void fillCache() {
try {
// TODO: Replace use of adb on supported versions of Android
String userDumpsysOutput = ShellCommandUtils.executeCommand("dumpsys user");
AdbUserParser.ParseResult result = parser.parse(userDumpsysOutput);
mCachedUsers = result.mUsers;
mCachedUserTypes = result.mUserTypes;
// We don't expose users who are currently being removed
mCachedUsers.entrySet().removeIf(
integerUserEntry -> integerUserEntry.getValue().isRemoving());
} catch (AdbException | AdbParseException e) {
throw new RuntimeException("Error filling cache", e);
}
}
/**
* Block until the user with the given {@code userReference} exists and is in the correct state.
*
* <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown.
*/
User waitForUserToMatch(UserReference userReference, Function<User, Boolean> userChecker) {
return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ true);
}
/**
* 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
User waitForUserToNotExistOrMatch(
UserReference userReference, Function<User, Boolean> userChecker) {
return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ false);
}
@Nullable
private User waitForUserToMatch(
UserReference userReference, Function<User, 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 {
AtomicReference<User> returnUser = new AtomicReference<>();
PollingCheck.waitFor(WAIT_FOR_USER_TIMEOUT_MS, () -> {
User user = userReference.resolve();
returnUser.set(user);
if (user == null) {
return !waitForExist;
}
return userChecker.apply(user);
});
return returnUser.get();
} catch (AssertionError e) {
User user = userReference.resolve();
if (user == null) {
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 " + user, e
);
}
}
}