blob: 2541a4ba5de88dcc39293386d7ab9c8a2e425a12 [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.compatibility.common.util.enterprise;
import static android.app.UiAutomation.FLAG_DONT_USE_ACCESSIBILITY;
import static org.junit.Assume.assumeTrue;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Scanner;
/**
* JUnit Rule which allows configuration of device state
*/
public final class DeviceState extends TestWatcher {
public enum UserType {
CURRENT_USER,
PRIMARY_USER,
SECONDARY_USER,
WORK_PROFILE
}
private static final String LOG_TAG = "DeviceState";
private static final Instrumentation sInstrumentation =
InstrumentationRegistry.getInstrumentation();
/**
* Copied from {@link android.content.pm.UserInfo}.
*/
private static final int FLAG_PRIMARY = 0x00000001;
/**
* Copied from {@link android.content.pm.UserInfo}.
*/
private static final int FLAG_MANAGED_PROFILE = 0x00000020;
/**
* Copied from {@link android.content.pm.UserInfo}.
*/
private static final int FLAG_FULL = 0x00000400;
private List<Integer> createdUserIds = new ArrayList<>();
private UiAutomation mUiAutomation;
private final int MAX_UI_AUTOMATION_RETRIES = 5;
@Nullable
public UserHandle getWorkProfile() {
return getWorkProfile(/* forUser= */ UserType.CURRENT_USER);
}
@Nullable
public UserHandle getWorkProfile(UserType forUser) {
assumeTrue("Due to API limitations, tests cannot manage work profiles for users other " +
"than the current one", forUser == UserType.CURRENT_USER);
UserManager userManager = sInstrumentation.getContext().getSystemService(UserManager.class);
for (UserHandle userHandle : userManager.getUserProfiles()) {
if ((getFlagsForUserID(userHandle.getIdentifier()) & FLAG_MANAGED_PROFILE) != 0) {
return userHandle;
}
}
return null;
}
public boolean isRunningOnWorkProfile() {
return getWorkProfile() != null
&& getWorkProfile().getIdentifier() == android.os.UserHandle.myUserId();
}
private Integer getFlagsForUserID(int userId) {
ArrayList<String[]> users = tokenizeListUsers();
for (String[] user : users) {
int foundUserId = Integer.parseInt(user[1]);
if (userId == foundUserId) {
return Integer.parseInt(user[3], 16);
}
}
return null;
}
public boolean isRunningOnPrimaryUser() {
return android.os.UserHandle.myUserId() == getPrimaryUserId();
}
public boolean isRunningOnSecondaryUser() {
return UserHandle.myUserId() != getPrimaryUserId()
&& (getFlagsForUserID(android.os.UserHandle.myUserId() & FLAG_FULL) != 0);
}
/**
* Get the first human user on the device.
*
* <p>Returns {@code null} if there is none present.
*/
@Nullable
public UserHandle getPrimaryUser() {
Integer primaryUserId = getPrimaryUserId();
if (primaryUserId == null) {
return null;
}
return UserHandle.of(primaryUserId);
}
/**
* Get the first human user on the device other than the primary user.
*
* <p>Returns {@code null} if there is none present.
*/
@Nullable
public UserHandle getSecondaryUser() {
Integer secondaryUserId = getSecondaryUserId();
if (secondaryUserId == null) {
return null;
}
return UserHandle.of(secondaryUserId);
}
/**
* Get the user ID of the first human user on the device.
*
* <p>Returns {@code null} if there is none present.
*/
@Nullable
private Integer getPrimaryUserId() {
// This would be cleaner if there was a test api which could find this information
ArrayList<String[]> users = tokenizeListUsers();
for (String[] user : users) {
int flag = Integer.parseInt(user[3], 16);
if ((flag & FLAG_PRIMARY) != 0) {
return Integer.parseInt(user[1]);
}
}
return null;
}
/**
* Get the user ID of a human user on the device other than the primary user.
*
* <p>Returns {@code null} if there is none present.
*/
@Nullable
private Integer getSecondaryUserId() {
// This would be cleaner if there was a test api which could find this information
ArrayList<String[]> users = tokenizeListUsers();
for (String[] user : users) {
int flag = Integer.parseInt(user[3], 16);
if (((flag & FLAG_PRIMARY) == 0) && ((flag & FLAG_FULL) != 0)) {
return Integer.parseInt(user[1]);
}
}
return null;
}
/**
* Tokenizes the output of 'pm list users'.
* The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
* Integer.toHexString(flag), "[running]"}; (the last one being optional)
* @return a list of arrays of strings, each element of the list representing the tokens
* for a user, or {@code null} if there was an error while tokenizing the adb command output.
*/
private ArrayList<String[]> tokenizeListUsers() {
String command = "pm list users";
String commandOutput = runCommandWithOutput(command);
// Extract the id of all existing users.
String[] lines = commandOutput.split("\\r?\\n");
if (!lines[0].equals("Users:")) {
throw new RuntimeException(
String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
}
ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
for (int i = 1; i < lines.length; i++) {
// Individual user is printed out like this:
// \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
String[] tokens = lines[i].split("\\{|\\}|:");
if (tokens.length != 4 && tokens.length != 5) {
throw new RuntimeException(
String.format(
"device output: '%s' \nline: '%s' was not in the expected "
+ "format for user info.",
commandOutput, lines[i]));
}
users.add(tokens);
}
return users;
}
public void ensureHasWorkProfile(boolean installTestApp, UserType forUser) {
requireFeature("android.software.managed_users");
assumeTrue("Due to API limitations, tests cannot manage work profiles for users other " +
"than the current one", forUser == UserType.CURRENT_USER);
if (getWorkProfile() == null) {
createWorkProfile(resolveUserTypeToUserId(forUser));
}
if (installTestApp) {
installInProfile(getWorkProfile().getIdentifier(),
sInstrumentation.getContext().getPackageName());
} else {
uninstallFromProfile(getWorkProfile().getIdentifier(),
sInstrumentation.getContext().getPackageName());
}
}
public void ensureHasSecondaryUser(boolean installTestApp) {
// TODO: What is the requirement?
if (getSecondaryUser() == null) {
createSecondaryUser();
}
if (installTestApp) {
installInProfile(getSecondaryUserId(), sInstrumentation.getContext().getPackageName());
} else {
uninstallFromProfile(getSecondaryUserId(),
sInstrumentation.getContext().getPackageName());
}
}
public void requireCanSupportAdditionalUser() {
int maxUsers = getMaxNumberOfUsersSupported();
int currentUsers = tokenizeListUsers().size();
assumeTrue("The device does not have space for an additional user (" + currentUsers +
" current users, " + maxUsers + " max users)", currentUsers + 1 <= maxUsers);
}
private int resolveUserTypeToUserId(UserType userType) {
switch (userType) {
case CURRENT_USER:
return android.os.UserHandle.myUserId();
case PRIMARY_USER:
return getPrimaryUserId();
case SECONDARY_USER:
return getSecondaryUserId();
case WORK_PROFILE:
return getWorkProfile().getIdentifier();
default:
throw new IllegalArgumentException("Unknown user type " + userType);
}
}
void teardown() {
for (Integer userId : createdUserIds) {
runCommandWithOutput("pm remove-user " + userId);
}
createdUserIds.clear();
}
private void createWorkProfile(int parentUserId) {
final String createUserOutput =
runCommandWithOutput(
"pm create-user --profileOf " + parentUserId + " --managed work");
final int profileId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
runCommandWithOutput("am start-user -w " + profileId);
createdUserIds.add(profileId);
}
private void createSecondaryUser() {
requireCanSupportAdditionalUser();
final String createUserOutput =
runCommandWithOutput("pm create-user secondary");
final int userId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
runCommandWithOutput("am start-user -w " + userId);
createdUserIds.add(userId);
}
private void installInProfile(int profileId, String packageName) {
runCommandWithOutput("pm install-existing --user " + profileId + " " + packageName);
}
private void uninstallFromProfile(int profileId, String packageName) {
runCommandWithOutput("pm uninstall --user " + profileId + " " + packageName);
}
private String runCommandWithOutput(String command) {
ParcelFileDescriptor p = runCommand(command);
InputStream inputStream = new FileInputStream(p.getFileDescriptor());
try (Scanner scanner = new Scanner(inputStream, UTF_8.name())) {
return scanner.useDelimiter("\\A").next();
} catch (NoSuchElementException e) {
return "";
}
}
private ParcelFileDescriptor runCommand(String command) {
return getAutomation()
.executeShellCommand(command);
}
private UiAutomation getAutomation() {
if (mUiAutomation != null) {
return mUiAutomation;
}
int retries = MAX_UI_AUTOMATION_RETRIES;
mUiAutomation = sInstrumentation.getUiAutomation(FLAG_DONT_USE_ACCESSIBILITY);
while (mUiAutomation == null && retries > 0) {
Log.e(LOG_TAG, "Failed to get UiAutomation");
retries--;
mUiAutomation = sInstrumentation.getUiAutomation(FLAG_DONT_USE_ACCESSIBILITY);
}
if (mUiAutomation == null) {
throw new AssertionError("Could not get UiAutomation");
}
return mUiAutomation;
}
private void requireFeature(String feature) {
assumeTrue("Device must have feature " + feature,
sInstrumentation.getContext().getPackageManager().hasSystemFeature(feature));
}
private int getMaxNumberOfUsersSupported() {
String command = "pm get-max-users";
String commandOutput = runCommandWithOutput(command);
try {
return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid command output", e);
}
}
}