blob: aa9343f7bf0c1dd0c301644512663cb0a39a22f0 [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_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static com.android.bedstead.nene.users.Users.users;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.permissions.PermissionContext;
import com.android.bedstead.nene.utils.Poll;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.bedstead.nene.utils.ShellCommandUtils;
import com.android.bedstead.nene.utils.Versions;
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* A representation of a User on device which may or may not exist.
*/
public class UserReference implements AutoCloseable {
private static final Set<AdbUser.UserState> RUNNING_STATES = new HashSet<>(
Arrays.asList(AdbUser.UserState.RUNNING_LOCKED,
AdbUser.UserState.RUNNING_UNLOCKED,
AdbUser.UserState.RUNNING_UNLOCKING)
);
private static final String LOG_TAG = "UserReference";
private final int mId;
private final UserManager mUserManager;
private Long mSerialNo;
private String mName;
private UserType mUserType;
private Boolean mIsPrimary;
private boolean mParentCached = false;
private UserReference mParent;
UserReference(int id) {
mId = id;
mUserManager = TestApis.context().androidContextAsUser(this)
.getSystemService(UserManager.class);
}
public final int id() {
return mId;
}
/**
* Get a {@link UserHandle} for the {@link #id()}.
*/
public final UserHandle userHandle() {
return UserHandle.of(mId);
}
/**
* Remove the user from the device.
*
* <p>If the user does not exist, or the removal fails for any other reason, a
* {@link NeneException} will be thrown.
*/
public final void remove() {
try {
// Expected success string is "Success: removed user"
ShellCommand.builder("pm remove-user")
.addOperand(mId)
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Poll.forValue("User exists", this::exists)
.toBeEqualTo(false)
// TODO(b/203630556): Reduce timeout once we have a faster way of removing users
.timeout(Duration.ofMinutes(1))
.errorOnFail()
.await();
} catch (AdbException e) {
throw new NeneException("Could not remove user " + this, e);
}
}
/**
* Start the user.
*
* <p>After calling this command, the user will be running unlocked.
*
* <p>If the user does not exist, or the start fails for any other reason, a
* {@link NeneException} will be thrown.
*/
//TODO(scottjonathan): Deal with users who won't unlock
public UserReference start() {
try {
// Expected success string is "Success: user started"
ShellCommand.builder("am start-user")
.addOperand(mId)
.addOperand("-w")
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Poll.forValue("User running unlocked", () -> isRunning() && isUnlocked())
.toBeEqualTo(true)
.errorOnFail()
.timeout(Duration.ofMinutes(1))
.await();
} catch (AdbException e) {
throw new NeneException("Could not start user " + this, e);
}
return this;
}
/**
* Stop the user.
*
* <p>After calling this command, the user will be not running.
*/
public UserReference stop() {
try {
// Expects no output on success or failure - stderr output on failure
ShellCommand.builder("am stop-user")
.addOperand("-f") // Force stop
.addOperand(mId)
.allowEmptyOutput(true)
.validate(String::isEmpty)
.execute();
Poll.forValue("User running", this::isRunning)
.toBeEqualTo(false)
// TODO(b/203630556): Replace stopping with something faster
.timeout(Duration.ofMinutes(10))
.errorOnFail()
.await();
} catch (AdbException e) {
throw new NeneException("Could not stop user " + this, e);
}
return this;
}
/**
* Make the user the foreground user.
*
* <p>If the user is a profile, then this will make the parent the foreground user. It will
* still return the {@link UserReference} of the profile in that case.
*/
public UserReference switchTo() {
UserReference parent = parent();
if (parent != null) {
parent.switchTo();
return this;
}
if (TestApis.users().current().equals(this)) {
// Already switched to
return this;
}
// This is created outside of the try because we don't want to wait for the broadcast
// on versions less than R
BlockingBroadcastReceiver broadcastReceiver =
new BlockingBroadcastReceiver(TestApis.context().instrumentedContext(),
Intent.ACTION_USER_FOREGROUND,
(intent) ->((UserHandle)
intent.getParcelableExtra(Intent.EXTRA_USER))
.getIdentifier() == mId);
try {
if (Versions.meetsMinimumSdkVersionRequirement(R)) {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
broadcastReceiver.registerForAllUsers();
}
}
// Expects no output on success or failure
ShellCommand.builder("am switch-user")
.addOperand(mId)
.allowEmptyOutput(true)
.validate(String::isEmpty)
.execute();
if (Versions.meetsMinimumSdkVersionRequirement(R)) {
broadcastReceiver.awaitForBroadcast();
} else {
Thread.sleep(20000);
}
} catch (AdbException e) {
throw new NeneException("Could not switch to user", e);
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Interrupted while switching user", e);
} finally {
broadcastReceiver.unregisterQuietly();
}
return this;
}
/** Get the serial number of the user. */
public long serialNo() {
if (mSerialNo == null) {
mSerialNo = TestApis.context().instrumentedContext().getSystemService(UserManager.class)
.getSerialNumberForUser(userHandle());
if (mSerialNo == -1) {
mSerialNo = null;
throw new NeneException("User does not exist " + this);
}
}
return mSerialNo;
}
/** Get the name of the user. */
public String name() {
if (mName == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mName = adbUser().name();
} else {
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
mName = TestApis.context().androidContextAsUser(this)
.getSystemService(UserManager.class)
.getUserName();
}
if (mName.equals("")) {
if (!exists()) {
mName = null;
throw new NeneException("User does not exist " + this);
}
}
}
}
return mName;
}
/** Is the user running? */
public boolean isRunning() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
AdbUser adbUser = adbUserOrNull();
if (adbUser == null) {
return false;
}
return RUNNING_STATES.contains(adbUser().state());
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
return mUserManager.isUserRunning(userHandle());
}
}
/** Is the user unlocked? */
public boolean isUnlocked() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
AdbUser adbUser = adbUserOrNull();
if (adbUser == null) {
return false;
}
return adbUser.state().equals(AdbUser.UserState.RUNNING_UNLOCKED);
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
return mUserManager.isUserUnlocked(userHandle());
}
}
/**
* Get the user type.
*/
public UserType type() {
if (mUserType == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mUserType = adbUser().type();
} else {
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
String userTypeName = mUserManager.getUserType();
if (userTypeName.equals("")) {
throw new NeneException("User does not exist " + this);
}
mUserType = TestApis.users().supportedType(userTypeName);
}
}
}
return mUserType;
}
/**
* Return {@code true} if this is the primary user.
*/
public Boolean isPrimary() {
if (mIsPrimary == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mIsPrimary = adbUser().isPrimary();
} else {
mIsPrimary = userInfo().isPrimary();
}
}
return mIsPrimary;
}
/**
* Return the parent of this profile.
*
* <p>Returns {@code null} if this user is not a profile.
*/
@Nullable
public UserReference parent() {
if (!mParentCached) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mParent = adbUser().parent();
} else {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
UserHandle parentHandle = mUserManager.getProfileParent(userHandle());
if (parentHandle == null) {
if (!exists()) {
throw new NeneException("User does not exist " + this);
}
mParent = null;
} else {
mParent = TestApis.users().find(parentHandle);
}
}
}
mParentCached = true;
}
return mParent;
}
/**
* Return {@code true} if a user with this ID exists.
*/
public boolean exists() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
return TestApis.users().all().stream().anyMatch(u -> u.equals(this));
}
return users().anyMatch(ui -> ui.id == id());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UserReference)) {
return false;
}
UserReference other = (UserReference) obj;
return other.id() == id();
}
@Override
public int hashCode() {
return id();
}
/** See {@link #remove}. */
@Override
public void close() {
remove();
}
private AdbUser adbUserOrNull() {
return TestApis.users().fetchUser(mId);
}
private AdbUser adbUser() {
AdbUser user = adbUserOrNull();
if (user == null) {
throw new NeneException("User does not exist " + this);
}
return user;
}
private UserInfo userInfo() {
return users().filter(ui -> ui.id == id()).findFirst()
.orElseThrow(() -> new NeneException("User does not exist " + this));
}
@Override
public String toString() {
return "User{id=" + id() + "}";
}
}