blob: 3384385c38e01d17947e72a4f40dacd5d1cc9a26 [file] [log] [blame]
/*
* Copyright (C) 2020 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.harrier;
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.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.bedstead.harrier.annotations.FailureMode;
import com.android.bedstead.harrier.annotations.RequireFeatures;
import com.android.bedstead.harrier.annotations.RequireUserSupported;
import com.android.bedstead.harrier.annotations.meta.EnsureHasNoProfileAnnotation;
import com.android.bedstead.harrier.annotations.meta.EnsureHasNoUserAnnotation;
import com.android.bedstead.harrier.annotations.meta.EnsureHasProfileAnnotation;
import com.android.bedstead.harrier.annotations.meta.EnsureHasUserAnnotation;
import com.android.bedstead.harrier.annotations.meta.RequireRunOnUserAnnotation;
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.users.User;
import com.android.bedstead.nene.users.UserBuilder;
import com.android.bedstead.nene.users.UserReference;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* A Junit rule which exposes methods for efficiently changing and querying device state.
*
* <p>States set by the methods on this class will by default be cleaned up after the test.
*
*
* <p>Using this rule also enforces preconditions in annotations from the
* {@code com.android.comaptibility.common.util.enterprise.annotations} package.
*
* {@code assumeTrue} will be used, so tests which do not meet preconditions will be skipped.
*/
public final class DeviceState implements TestRule {
private final Context mContext = ApplicationProvider.getApplicationContext();
private static final TestApis sTestApis = new TestApis();
private static final String SKIP_TEST_TEARDOWN_KEY = "skip-test-teardown";
private static final String SKIP_CLASS_TEARDOWN_KEY = "skip-class-teardown";
private static final String SKIP_TESTS_REASON_KEY = "skip-tests-reason";
private boolean mSkipTestTeardown;
private boolean mSkipClassTeardown;
private boolean mSkipTests;
private String mSkipTestsReason;
private static final String TV_PROFILE_TYPE_NAME = "com.android.tv.profile";
public DeviceState() {
Bundle arguments = InstrumentationRegistry.getArguments();
mSkipTestTeardown = Boolean.parseBoolean(
arguments.getString(SKIP_TEST_TEARDOWN_KEY, "false"));
mSkipClassTeardown = Boolean.parseBoolean(
arguments.getString(SKIP_CLASS_TEARDOWN_KEY, "false"));
mSkipTestsReason = arguments.getString(SKIP_TESTS_REASON_KEY, "");
mSkipTests = !mSkipTestsReason.isEmpty();
}
void setSkipTestTeardown(boolean skipTestTeardown) {
mSkipTestTeardown = skipTestTeardown;
}
@Override public Statement apply(final Statement base,
final Description description) {
if (description.isTest()) {
return applyTest(base, description);
} else if (description.isSuite()) {
return applySuite(base, description);
}
throw new IllegalStateException("Unknown description type: " + description);
}
private Statement applyTest(final Statement base, final Description description) {
return new Statement() {
@Override public void evaluate() throws Throwable {
Log.d(LOG_TAG, "Preparing state for test " + description.getMethodName());
assumeFalse(mSkipTestsReason, mSkipTests);
for (Annotation annotation : description.getAnnotations()) {
Class<? extends Annotation> annotationType = annotation.annotationType();
EnsureHasNoProfileAnnotation ensureHasNoProfileAnnotation =
annotationType.getAnnotation(EnsureHasNoProfileAnnotation.class);
if (ensureHasNoProfileAnnotation != null) {
UserType userType = (UserType) annotation.annotationType()
.getMethod("forUser").invoke(annotation);
ensureHasNoProfile(ensureHasNoProfileAnnotation.value(), userType);
}
EnsureHasProfileAnnotation ensureHasProfileAnnotation =
annotationType.getAnnotation(EnsureHasProfileAnnotation.class);
if (ensureHasProfileAnnotation != null) {
UserType forUser = (UserType) annotation.annotationType()
.getMethod("forUser").invoke(annotation);
boolean installTestApp = (boolean) annotation.annotationType()
.getMethod("installTestApp").invoke(annotation);
ensureHasProfile(
ensureHasProfileAnnotation.value(), installTestApp, forUser);
}
EnsureHasNoUserAnnotation ensureHasNoUserAnnotation =
annotationType.getAnnotation(EnsureHasNoUserAnnotation.class);
if (ensureHasNoUserAnnotation != null) {
ensureHasNoUser(ensureHasNoUserAnnotation.value());
}
EnsureHasUserAnnotation ensureHasUserAnnotation =
annotationType.getAnnotation(EnsureHasUserAnnotation.class);
if (ensureHasUserAnnotation != null) {
boolean installTestApp = (boolean) annotation.getClass()
.getMethod("installTestApp").invoke(annotation);
ensureHasUser(ensureHasUserAnnotation.value(), installTestApp);
}
RequireRunOnUserAnnotation requireRunOnUserAnnotation =
annotationType.getAnnotation(RequireRunOnUserAnnotation.class);
if (requireRunOnUserAnnotation != null) {
requireRunOnUser(requireRunOnUserAnnotation.value());
}
}
RequireFeatures requireFeaturesAnnotation =
description.getAnnotation(RequireFeatures.class);
if (requireFeaturesAnnotation != null) {
for (String feature: requireFeaturesAnnotation.value()) {
requireFeature(feature, requireFeaturesAnnotation.failureMode());
}
}
RequireUserSupported requireUserSupportedAnnotation =
description.getAnnotation(RequireUserSupported.class);
if (requireUserSupportedAnnotation != null) {
for (String userType: requireUserSupportedAnnotation.value()) {
requireUserSupported(
userType, requireUserSupportedAnnotation.failureMode());
}
}
Log.d(LOG_TAG,
"Finished preparing state for test " + description.getMethodName());
try {
base.evaluate();
} finally {
Log.d(LOG_TAG,
"Tearing down state for test " + description.getMethodName());
teardownNonShareableState();
if (!mSkipTestTeardown) {
teardownShareableState();
}
Log.d(LOG_TAG,
"Finished tearing down state for test " + description.getMethodName());
}
}};
}
private Statement applySuite(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
if (!mSkipClassTeardown) {
teardownShareableState();
}
}
};
}
private void requireRunOnUser(String userType) {
assumeTrue("This test only runs on users of type " + userType,
isRunningOnUser(userType));
}
private void requireFeature(String feature, FailureMode failureMode) {
checkFailOrSkip("Device must have feature " + feature,
sTestApis.packages().features().contains(feature), failureMode);
}
private com.android.bedstead.nene.users.UserType requireUserSupported(
String userType, FailureMode failureMode) {
com.android.bedstead.nene.users.UserType resolvedUserType =
sTestApis.users().supportedType(userType);
checkFailOrSkip(
"Device must support user type " + userType
+ " only supports: " + sTestApis.users().supportedTypes(),
resolvedUserType != null, failureMode);
return resolvedUserType;
}
private void checkFailOrSkip(String message, boolean value, FailureMode failureMode) {
if (failureMode.equals(FailureMode.FAIL)) {
assertWithMessage(message).that(value).isTrue();
} else if (failureMode.equals(FailureMode.SKIP)) {
assumeTrue(message, value);
} else {
throw new IllegalStateException("Unknown failure mode: " + failureMode);
}
}
public enum UserType {
CURRENT_USER,
PRIMARY_USER,
SECONDARY_USER,
WORK_PROFILE,
TV_PROFILE,
}
private static final String LOG_TAG = "DeviceState";
private static final Context sContext = sTestApis.context().instrumentedContext();
private final Map<com.android.bedstead.nene.users.UserType, UserReference> mUsers =
new HashMap<>();
private final Map<com.android.bedstead.nene.users.UserType, Map<UserReference, UserReference>>
mProfiles = new HashMap<>();
private final List<UserReference> mCreatedUsers = new ArrayList<>();
private final List<UserBuilder> mRemovedUsers = new ArrayList<>();
private final List<BlockingBroadcastReceiver> mRegisteredBroadcastReceivers = new ArrayList<>();
/**
* Get the {@link UserReference} of the work profile for the current user
*
* <p>This should only be used to get work profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed work profile
*/
public UserReference workProfile() {
return workProfile(/* forUser= */ UserType.CURRENT_USER);
}
/**
* Get the {@link UserReference} of the work profile.
*
* <p>This should only be used to get work profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed work profile for the given user
*/
public UserReference workProfile(UserType forUser) {
return workProfile(resolveUserTypeToUser(forUser));
}
/**
* Get the {@link UserReference} of the work profile.
*
* <p>This should only be used to get work profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed work profile for the given user
*/
public UserReference workProfile(UserReference forUser) {
return profile(MANAGED_PROFILE_TYPE_NAME, forUser);
}
private UserReference profile(String profileType, UserReference forUser) {
com.android.bedstead.nene.users.UserType resolvedUserType =
sTestApis.users().supportedType(profileType);
if (resolvedUserType == null) {
throw new IllegalStateException("Can not have a profile of type " + profileType
+ " as they are not supported on this device");
}
return profile(resolvedUserType, forUser);
}
private UserReference profile(
com.android.bedstead.nene.users.UserType userType, UserReference forUser) {
if (userType == null || forUser == null) {
throw new NullPointerException();
}
if (!mProfiles.containsKey(userType) || !mProfiles.get(userType).containsKey(forUser)) {
throw new IllegalStateException(
"No harrier-managed profile of type " + userType + ". This method should only"
+ " be used when Harrier has been used to create the profile.");
}
return mProfiles.get(userType).get(forUser);
}
private boolean isRunningOnUser(String userType) {
return sTestApis.users().instrumented()
.resolve().type().name().equals(userType);
}
/**
* Get the {@link UserReference} of the tv profile for the current user
*
* <p>This should only be used to get tv profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed tv profile
*/
public UserReference tvProfile() {
return tvProfile(/* forUser= */ UserType.CURRENT_USER);
}
/**
* Get the {@link UserReference} of the tv profile.
*
* <p>This should only be used to get tv profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed tv profile
*/
public UserReference tvProfile(UserType forUser) {
return tvProfile(resolveUserTypeToUser(forUser));
}
/**
* Get the {@link UserReference} of the tv profile.
*
* <p>This should only be used to get tv profiles managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed tv profile
*/
public UserReference tvProfile(UserReference forUser) {
return profile(TV_PROFILE_TYPE_NAME, forUser);
}
/**
* Get the user ID of the first human user on the device.
*
* <p>Returns {@code null} if there is none present.
*/
@Nullable
public UserReference primaryUser() {
return sTestApis.users().all()
.stream().filter(User::isPrimary).findFirst().orElse(null);
}
/**
* Get a secondary user.
*
* <p>This should only be used to get secondary users managed by Harrier (using either the
* annotations or calls to the {@link DeviceState} class.
*
* @throws IllegalStateException if there is no harrier-managed secondary user
*/
@Nullable
public UserReference secondaryUser() {
return user(SECONDARY_USER_TYPE_NAME);
}
private UserReference user(String userType) {
com.android.bedstead.nene.users.UserType resolvedUserType =
sTestApis.users().supportedType(userType);
if (resolvedUserType == null) {
throw new IllegalStateException("Can not have a user of type " + userType
+ " as they are not supported on this device");
}
return user(resolvedUserType);
}
private UserReference user(com.android.bedstead.nene.users.UserType userType) {
if (userType == null) {
throw new NullPointerException();
}
if (!mUsers.containsKey(userType)) {
throw new IllegalStateException(
"No harrier-managed secondary user. This method should only be used when "
+ "Harrier has been used to create the secondary user.");
}
return mUsers.get(userType);
}
private UserReference ensureHasProfile(
String profileType, boolean installTestApp, UserType forUser) {
requireFeature("android.software.managed_users", FailureMode.SKIP);
com.android.bedstead.nene.users.UserType resolvedUserType =
requireUserSupported(profileType, FailureMode.SKIP);
UserReference forUserReference = resolveUserTypeToUser(forUser);
UserReference profile =
sTestApis.users().findProfileOfType(resolvedUserType, forUserReference);
if (profile == null) {
profile = createProfile(resolvedUserType, forUserReference);
}
profile.start();
if (installTestApp) {
sTestApis.packages().find(sContext.getPackageName()).install(profile);
} else {
sTestApis.packages().find(sContext.getPackageName()).uninstall(profile);
}
if (!mProfiles.containsKey(resolvedUserType)) {
mProfiles.put(resolvedUserType, new HashMap<>());
}
mProfiles.get(resolvedUserType).put(forUserReference, profile);
return profile;
}
private void ensureHasNoProfile(String profileType, UserType forUser) {
requireFeature("android.software.managed_users", FailureMode.SKIP);
UserReference forUserReference = resolveUserTypeToUser(forUser);
com.android.bedstead.nene.users.UserType resolvedProfileType =
sTestApis.users().supportedType(profileType);
if (resolvedProfileType == null) {
// These profile types don't exist so there can't be any
return;
}
UserReference profile =
sTestApis.users().findProfileOfType(
resolvedProfileType,
forUserReference);
if (profile != null) {
removeAndRecordUser(profile.resolve());
}
}
private void ensureHasUser(String userType, boolean installTestApp) {
com.android.bedstead.nene.users.UserType resolvedUserType =
requireUserSupported(userType, FailureMode.SKIP);
Collection<UserReference> users = sTestApis.users().findUsersOfType(resolvedUserType);
UserReference user = users.isEmpty() ? createUser(resolvedUserType)
: users.iterator().next();
user.start();
if (installTestApp) {
sTestApis.packages().find(sContext.getPackageName()).install(user);
} else {
sTestApis.packages().find(sContext.getPackageName()).uninstall(user);
}
mUsers.put(resolvedUserType, user);
}
/**
* Ensure that there is no user of the given type.
*/
private void ensureHasNoUser(String userType) {
com.android.bedstead.nene.users.UserType resolvedUserType =
sTestApis.users().supportedType(userType);
if (resolvedUserType == null) {
// These user types don't exist so there can't be any
return;
}
for (UserReference secondaryUser : sTestApis.users().findUsersOfType(resolvedUserType)) {
removeAndRecordUser(secondaryUser.resolve());
}
}
private void removeAndRecordUser(User user) {
if (user == null) {
return; // Nothing to remove
}
mRemovedUsers.add(sTestApis.users().createUser()
.name(user.name())
.type(user.type())
.parent(user.parent()));
user.remove();
}
public void requireCanSupportAdditionalUser() {
int maxUsers = getMaxNumberOfUsersSupported();
int currentUsers = sTestApis.users().all().size();
assumeTrue("The device does not have space for an additional user (" + currentUsers +
" current users, " + maxUsers + " max users)", currentUsers + 1 <= maxUsers);
}
/**
* Create and register a {@link BlockingBroadcastReceiver} which will be unregistered after the
* test has run.
*/
public BlockingBroadcastReceiver registerBroadcastReceiver(String action) {
return registerBroadcastReceiver(action, /* checker= */ null);
}
/**
* Create and register a {@link BlockingBroadcastReceiver} which will be unregistered after the
* test has run.
*/
public BlockingBroadcastReceiver registerBroadcastReceiver(
String action, Function<Intent, Boolean> checker) {
BlockingBroadcastReceiver broadcastReceiver =
new BlockingBroadcastReceiver(mContext, action, checker);
broadcastReceiver.register();
mRegisteredBroadcastReceivers.add(broadcastReceiver);
return broadcastReceiver;
}
private UserReference resolveUserTypeToUser(UserType userType) {
switch (userType) {
case CURRENT_USER:
return sTestApis.users().instrumented();
case PRIMARY_USER:
return primaryUser();
case SECONDARY_USER:
return secondaryUser();
case WORK_PROFILE:
return workProfile();
case TV_PROFILE:
return tvProfile();
default:
throw new IllegalArgumentException("Unknown user type " + userType);
}
}
private void teardownNonShareableState() {
mProfiles.clear();
mUsers.clear();
for (BlockingBroadcastReceiver broadcastReceiver : mRegisteredBroadcastReceivers) {
broadcastReceiver.unregisterQuietly();
}
mRegisteredBroadcastReceivers.clear();
}
private void teardownShareableState() {
for (UserReference user : mCreatedUsers) {
user.remove();
}
mCreatedUsers.clear();
for (UserBuilder userBuilder : mRemovedUsers) {
userBuilder.create();
}
mRemovedUsers.clear();
}
private UserReference createProfile(
com.android.bedstead.nene.users.UserType profileType, UserReference parent) {
requireCanSupportAdditionalUser();
try {
UserReference user = sTestApis.users().createUser()
.parent(parent)
.type(profileType)
.createAndStart();
mCreatedUsers.add(user);
return user;
} catch (NeneException e) {
throw new IllegalStateException("Error creating profile of type " + profileType, e);
}
}
private UserReference createUser(com.android.bedstead.nene.users.UserType userType) {
requireCanSupportAdditionalUser();
try {
UserReference user = sTestApis.users().createUser()
.type(userType)
.createAndStart();
mCreatedUsers.add(user);
return user;
} catch (NeneException e) {
throw new IllegalStateException("Error creating user of type " + userType, e);
}
}
private int getMaxNumberOfUsersSupported() {
try {
return ShellCommand.builder("pm get-max-users")
.validate((output) -> output.startsWith("Maximum supported users:"))
.executeAndParseOutput(
(output) -> Integer.parseInt(output.split(": ", 2)[1].trim()));
} catch (AdbException e) {
throw new IllegalStateException("Invalid command output", e);
}
}
}