blob: 7cccc64fe2103526ea1af6429aad28bc034f4e2e [file] [log] [blame]
/*
* Copyright (C) 2010 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.tradefed.device;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.InstallReceiver;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.TimeoutException;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.device.IDeviceSelection.BaseDeviceType;
import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.AaptParser;
import com.android.tradefed.util.Bugreport;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.KeyguardControllerState;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.ZipUtil2;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
/**
* Implementation of a {@link ITestDevice} for a full stack android device
*/
public class TestDevice extends NativeDevice {
/** number of attempts made to clear dialogs */
private static final int NUM_CLEAR_ATTEMPTS = 5;
/** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
static final String DISMISS_DIALOG_CMD = "input keyevent 23";
/** Commands that can be used to dismiss the keyguard. */
public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
/**
* Alternative command to dismiss the keyguard by requesting the Window Manager service to do
* it. Api 23 and after.
*/
static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard";
/** Timeout to wait for input dispatch to become ready **/
private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
/** command to test input dispatch readiness **/
private static final String TEST_INPUT_CMD = "dumpsys input";
private static final long AM_COMMAND_TIMEOUT = 10 * 1000;
private static final long CHECK_NEW_USER = 1000;
static final String LIST_PACKAGES_CMD = "pm list packages -f";
private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode -f";
private static final Pattern APEXES_WITH_PATH_REGEX =
Pattern.compile("package:(.*)=(.*) versionCode:(.*)");
static final String GET_MODULEINFOS_CMD = "pm get-moduleinfo --all";
private static final Pattern MODULEINFO_REGEX =
Pattern.compile("ModuleInfo\\{(.*)\\} packageName: (.*)");
/**
* Regexp to match on old versions of platform (before R), where {@code -f} flag for the {@code
* pm list packages apex-only} command wasn't supported.
*/
private static final Pattern APEXES_WITHOUT_PATH_REGEX =
Pattern.compile("package:(.*) versionCode:(.*)");
private static final int FLAG_PRIMARY = 1; // From the UserInfo class
private static final int FLAG_MAIN = 0x00004000; // From the UserInfo class
private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"};
/** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\w+)(\\}.*)";
/** Pattern to find the display ids of "dumpsys SurfaceFlinger" */
private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)";
private static final int API_LEVEL_GET_CURRENT_USER = 24;
/** Timeout to wait for a screenshot before giving up to avoid hanging forever */
private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min
/** adb shell am dumpheap <service pid> <dump file path> */
private static final String DUMPHEAP_CMD = "am dumpheap %s %s";
/** Time given to a file to be dumped on device side */
private static final long DUMPHEAP_TIME = 5000L;
/** Timeout in minutes for the package installation */
static final long INSTALL_TIMEOUT_MINUTES = 4;
/** Max timeout to output for package installation */
static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3;
private boolean mWasWifiHelperInstalled = false;
private static final String APEX_SUFFIX = ".apex";
private static final String APEX_ARG = "--apex";
/** Contains a set of Microdroid instances running in this TestDevice, and their resources. */
private Map<Process, MicrodroidTracker> mStartedMicrodroids = new HashMap<>();
private static final String TEST_ROOT = "/data/local/tmp/virt/";
private static final String VIRT_APEX = "/apex/com.android.virt/";
private static final String INSTANCE_IMG = "instance.img";
// This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s).
// Then there is time to run the actual task. Set the maximum timeout value big enough.
private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
private static final long MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES = 5;
private static final String EARLY_REBOOT = "Too early to call shutdown() or reboot()";
/**
* Allow pauses of up to 2 minutes while receiving bugreport.
*
* <p>Note that dumpsys may pause up to a minute while waiting for unresponsive components. It
* still should bail after that minute, if it will ever terminate on its own.
*/
private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000;
private static final String BUGREPORT_CMD = "bugreport";
private static final String BUGREPORTZ_CMD = "bugreportz";
private static final Pattern BUGREPORTZ_RESPONSE_PATTERN = Pattern.compile("(OK:)(.*)");
/** Track microdroid and its resources */
private class MicrodroidTracker {
ExecutorService executor;
}
/**
* @param device
* @param stateMonitor
* @param allocationMonitor
*/
public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
IDeviceMonitor allocationMonitor) {
super(device, stateMonitor, allocationMonitor);
}
@Override
public boolean isAppEnumerationSupported() throws DeviceNotAvailableException {
if (!checkApiLevelAgainstNextRelease(30)) {
return false;
}
return hasFeature("android.software.app_enumeration");
}
/**
* Core implementation of package installation, with retries around
* {@link IDevice#installPackage(String, boolean, String...)}
* @param packageFile
* @param reinstall
* @param extraArgs
* @return the response from the installation
* @throws DeviceNotAvailableException
*/
private String internalInstallPackage(
final File packageFile, final boolean reinstall, final List<String> extraArgs)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
List<String> args = new ArrayList<>(extraArgs);
if (packageFile.getName().endsWith(APEX_SUFFIX)) {
args.add(APEX_ARG);
}
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction =
new DeviceAction() {
@Override
public boolean run() throws InstallException {
try {
InstallReceiver receiver = createInstallReceiver();
getIDevice()
.installPackage(
packageFile.getAbsolutePath(),
reinstall,
receiver,
INSTALL_TIMEOUT_MINUTES,
INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
TimeUnit.MINUTES,
args.toArray(new String[] {}));
response[0] = handleInstallReceiver(receiver, packageFile);
} catch (InstallException e) {
response[0] = handleInstallationError(e);
}
return response[0] == null;
}
};
CLog.v(
"Installing package file %s with args %s on %s",
packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber());
performDeviceAction(
String.format("install %s", packageFile.getAbsolutePath()),
installAction,
MAX_RETRY_ATTEMPTS);
List<File> packageFiles = new ArrayList<>();
packageFiles.add(packageFile);
allowLegacyStorageForApps(packageFiles);
return response[0];
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_TIME,
System.currentTimeMillis() - startTime);
}
}
/**
* Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File,
* boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing.
*/
@VisibleForTesting
InstallReceiver createInstallReceiver() {
return new InstallReceiver();
}
/** {@inheritDoc} */
@Override
public InputStreamSource getBugreport() {
if (getApiLevelSafe() < 24) {
InputStreamSource bugreport = getBugreportInternal();
if (bugreport == null) {
// Safe call so we don't return null but an empty resource.
return new ByteArrayInputStreamSource("".getBytes());
}
return bugreport;
}
CLog.d("Api level above 24, using bugreportz instead.");
File mainEntry = null;
File bugreportzFile = null;
long startTime = System.currentTimeMillis();
try {
bugreportzFile = getBugreportzInternal();
if (bugreportzFile == null) {
// return empty buffer
return new ByteArrayInputStreamSource("".getBytes());
}
try (ZipFile zip = new ZipFile(bugreportzFile)) {
// We get the main_entry.txt that contains the bugreport name.
mainEntry = ZipUtil2.extractFileFromZip(zip, "main_entry.txt");
String bugreportName = FileUtil.readStringFromFile(mainEntry).trim();
CLog.d("bugreport name: '%s'", bugreportName);
File bugreport = ZipUtil2.extractFileFromZip(zip, bugreportName);
return new FileInputStreamSource(bugreport, true);
}
} catch (IOException e) {
CLog.e("Error while unzipping bugreportz");
CLog.e(e);
return new ByteArrayInputStreamSource("corrupted bugreport.".getBytes());
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
FileUtil.deleteFile(bugreportzFile);
FileUtil.deleteFile(mainEntry);
}
}
/** {@inheritDoc} */
@Override
public boolean logBugreport(String dataName, ITestLogger listener) {
InputStreamSource bugreport = null;
LogDataType type = null;
try {
bugreport = getBugreportz();
type = LogDataType.BUGREPORTZ;
// Limit fallback to older devices
if (!TestDeviceState.RECOVERY.equals(getDeviceState())) {
if (bugreport == null && getApiLevelSafe() < 24) {
CLog.d("Bugreportz failed, attempting bugreport collection instead.");
bugreport = getBugreportInternal();
type = LogDataType.BUGREPORT;
}
}
// log what we managed to capture.
if (bugreport != null && bugreport.size() > 0L) {
listener.testLog(dataName, type, bugreport);
return true;
}
} finally {
StreamUtil.cancel(bugreport);
}
CLog.d(
"logBugreport() was not successful in collecting and logging the bugreport "
+ "for device %s",
getSerialNumber());
return false;
}
/** {@inheritDoc} */
@Override
public Bugreport takeBugreport() {
File bugreportFile = null;
int apiLevel = getApiLevelSafe();
if (apiLevel == UNKNOWN_API_LEVEL) {
return null;
}
long startTime = System.currentTimeMillis();
try {
if (apiLevel >= 24) {
CLog.d("Api level above 24, using bugreportz.");
bugreportFile = getBugreportzInternal();
if (bugreportFile != null) {
return new Bugreport(bugreportFile, true);
}
return null;
}
// fall back to regular bugreport
InputStreamSource bugreport = getBugreportInternal();
if (bugreport == null) {
CLog.e("Error when collecting the bugreport.");
return null;
}
try {
bugreportFile = FileUtil.createTempFile("bugreport", ".txt");
FileUtil.writeToFile(bugreport.createInputStream(), bugreportFile);
return new Bugreport(bugreportFile, false);
} catch (IOException e) {
CLog.e("Error when writing the bugreport file");
CLog.e(e);
}
return null;
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
}
}
/** {@inheritDoc} */
@Override
public InputStreamSource getBugreportz() {
if (getApiLevelSafe() < 24) {
return null;
}
CLog.d("Start getBugreportz()");
long startTime = System.currentTimeMillis();
try {
File bugreportZip = getBugreportzInternal();
if (bugreportZip != null) {
return new FileInputStreamSource(bugreportZip, true);
}
return null;
} finally {
CLog.d("Done with getBugreportz()");
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
}
}
/** Internal Helper method to get the bugreportz zip file as a {@link File}. */
@VisibleForTesting
protected File getBugreportzInternal() {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
// Does not rely on {@link ITestDevice#executeAdbCommand(String...)} because it does not
// provide a timeout.
try {
executeShellCommand(
BUGREPORTZ_CMD,
receiver,
getOptions().getBugreportzTimeout(),
TimeUnit.MILLISECONDS,
0 /* don't retry */);
String output = receiver.getOutput().trim();
Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output);
if (!match.find()) {
CLog.e("Something went went wrong during bugreportz collection: '%s'", output);
return null;
} else {
String remoteFilePath = match.group(2);
if (Strings.isNullOrEmpty(remoteFilePath)) {
CLog.e("Invalid bugreportz path found from output: %s", output);
return null;
}
File zipFile = null;
try {
if (!doesFileExist(remoteFilePath)) {
CLog.e("Did not find bugreportz at: '%s'", remoteFilePath);
return null;
}
// Create a placeholder to replace the file
zipFile = FileUtil.createTempFile("bugreportz", ".zip");
// pull
pullFile(remoteFilePath, zipFile);
String bugreportDir =
remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/'));
if (!bugreportDir.isEmpty()) {
// clean bugreport files directory on device
deleteFile(String.format("%s/*", bugreportDir));
}
return zipFile;
} catch (IOException e) {
CLog.e("Failed to create the temporary file.");
return null;
}
}
} catch (DeviceNotAvailableException e) {
CLog.e("Device %s became unresponsive while retrieving bugreportz", getSerialNumber());
CLog.e(e);
}
return null;
}
protected InputStreamSource getBugreportInternal() {
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
try {
executeShellCommand(
BUGREPORT_CMD,
receiver,
BUGREPORT_TIMEOUT,
TimeUnit.MILLISECONDS,
0 /* don't retry */);
} catch (DeviceNotAvailableException e) {
// Log, but don't throw, so the caller can get the bugreport contents even
// if the device goes away
CLog.e("Device %s became unresponsive while retrieving bugreport", getSerialNumber());
return null;
}
return new ByteArrayInputStreamSource(receiver.getOutput());
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(final File packageFile, final boolean reinstall,
final String... extraArgs) throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
// grant all permissions by default if feature is supported
if (runtimePermissionSupported) {
args.add("-g");
}
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
String... extraArgs) throws DeviceNotAvailableException {
ensureRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
args.add("-g");
}
return internalInstallPackage(packageFile, reinstall, args);
}
public String installPackage(final File packageFile, final File certFile,
final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction =
new DeviceAction() {
@Override
public boolean run()
throws InstallException, SyncException, IOException,
TimeoutException, AdbCommandRejectedException {
// TODO: create a getIDevice().installPackage(File, File...) method when
// the
// dist cert functionality is ready to be open sourced
String remotePackagePath =
getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
String remoteCertPath =
getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
// trick installRemotePackage into issuing a 'pm install <apk> <cert>'
// command, by adding apk path to extraArgs, and using cert as the
// 'apk file'.
String[] newExtraArgs = new String[extraArgs.length + 1];
System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
newExtraArgs[newExtraArgs.length - 1] =
String.format("\"%s\"", remotePackagePath);
try {
InstallReceiver receiver = createInstallReceiver();
getIDevice()
.installRemotePackage(
remoteCertPath,
reinstall,
receiver,
INSTALL_TIMEOUT_MINUTES,
INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
TimeUnit.MINUTES,
newExtraArgs);
response[0] = handleInstallReceiver(receiver, packageFile);
} catch (InstallException e) {
response[0] = handleInstallationError(e);
} finally {
getIDevice().removeRemotePackage(remotePackagePath);
getIDevice().removeRemotePackage(remoteCertPath);
}
return true;
}
};
performDeviceAction(
String.format("install %s", packageFile.getAbsolutePath()),
installAction,
MAX_RETRY_ATTEMPTS);
List<File> packageFiles = new ArrayList<>();
packageFiles.add(packageFile);
allowLegacyStorageForApps(packageFiles);
return response[0];
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_TIME,
System.currentTimeMillis() - startTime);
}
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall, int userId,
String... extraArgs) throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
// grant all permissions by default if feature is supported
if (runtimePermissionSupported) {
args.add("-g");
}
args.add("--user");
args.add(Integer.toString(userId));
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall,
boolean grantPermissions, int userId, String... extraArgs)
throws DeviceNotAvailableException {
ensureRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
args.add("-g");
}
args.add("--user");
args.add(Integer.toString(userId));
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
return uninstallPackage(packageName, /* extraArgs= */ null);
}
private String uninstallPackage(String packageName, @Nullable String extraArgs)
throws DeviceNotAvailableException {
final String finalExtraArgs = (extraArgs == null) ? "" : extraArgs;
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction uninstallAction =
() -> {
CLog.d("Uninstalling %s with extra args %s", packageName, finalExtraArgs);
String result = getIDevice().uninstallApp(packageName, finalExtraArgs);
response[0] = result;
return result == null;
};
performDeviceAction(
String.format("uninstall %s with extra args %s", packageName, finalExtraArgs),
uninstallAction,
MAX_RETRY_ATTEMPTS);
return response[0];
}
/** {@inheritDoc} */
@Override
public String uninstallPackageForUser(final String packageName, int userId)
throws DeviceNotAvailableException {
return uninstallPackage(packageName, "--user " + userId);
}
/**
* Core implementation for installing application with split apk files {@link
* IDevice#installPackages(List, boolean, List)} See
* "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to
* several files.
*
* @param packageFiles the local apk files
* @param reinstall <code>true</code> if a reinstall should be performed
* @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available
* options.
* @return the response from the installation <code>null</code> if installation succeeds.
* @throws DeviceNotAvailableException
*/
private String internalInstallPackages(
final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction =
new DeviceAction() {
@Override
public boolean run() throws InstallException {
try {
getIDevice()
.installPackages(
packageFiles,
reinstall,
extraArgs,
INSTALL_TIMEOUT_MINUTES,
TimeUnit.MINUTES);
response[0] = null;
return true;
} catch (InstallException e) {
response[0] = handleInstallationError(e);
return false;
}
}
};
performDeviceAction(
String.format("install %s", packageFiles.toString()),
installAction,
MAX_RETRY_ATTEMPTS);
allowLegacyStorageForApps(packageFiles);
return response[0];
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PACKAGE_INSTALL_TIME,
System.currentTimeMillis() - startTime);
}
}
/**
* Allows Legacy External Storage access for apps that request for it.
*
* <p>Apps that request for legacy external storage access are granted the access by setting
* MANAGE_EXTERNAL_STORAGE App Op. This gives the app File manager privileges, File managers
* have legacy external storage access.
*
* @param appFiles List of Files. Apk Files of the apps that are installed.
*/
private void allowLegacyStorageForApps(List<File> appFiles) throws DeviceNotAvailableException {
for (File appFile : appFiles) {
AaptParser aaptParser = createParser(appFile);
if (aaptParser != null
&& aaptParser.getTargetSdkVersion() > 29
&& aaptParser.isRequestingLegacyStorage()) {
if (!aaptParser.isUsingPermissionManageExternalStorage()) {
CLog.w(
"App is requesting legacy storage and targets R or above, but didn't"
+ " request the MANAGE_EXTERNAL_STORAGE permission so the"
+ " associated app op cannot be automatically granted and the"
+ " app won't have legacy external storage access: "
+ aaptParser.getPackageName());
continue;
}
// Set the MANAGE_EXTERNAL_STORAGE App Op to MODE_ALLOWED (Code = 0)
// for all users.
ArrayList<Integer> userIds = listUsers();
for (int userId : userIds) {
CommandResult setFileManagerAppOpResult =
executeShellV2Command(
"appops set --user "
+ userId
+ " --uid "
+ aaptParser.getPackageName()
+ " MANAGE_EXTERNAL_STORAGE 0");
if (!CommandStatus.SUCCESS.equals(setFileManagerAppOpResult.getStatus())) {
CLog.e(
"Failed to set MANAGE_EXTERNAL_STORAGE App Op to"
+ " allow legacy external storage for: %s ; stderr: %s",
aaptParser.getPackageName(), setFileManagerAppOpResult.getStderr());
}
}
}
}
CommandResult persistFileManagerAppOpResult =
executeShellV2Command("appops write-settings");
if (!CommandStatus.SUCCESS.equals(persistFileManagerAppOpResult.getStatus())) {
CLog.e(
"Failed to persist MANAGE_EXTERNAL_STORAGE App Op over `adb reboot`: %s",
persistFileManagerAppOpResult.getStderr());
}
}
@VisibleForTesting
protected AaptParser createParser(File appFile) {
return AaptParser.parse(appFile);
}
/** {@inheritDoc} */
@Override
public String installPackages(
final List<File> packageFiles, final boolean reinstall, final String... extraArgs)
throws DeviceNotAvailableException {
// Grant all permissions by default if feature is supported
return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs);
}
/** {@inheritDoc} */
@Override
public String installPackages(
List<File> packageFiles,
boolean reinstall,
boolean grantPermissions,
String... extraArgs)
throws DeviceNotAvailableException {
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
ensureRuntimePermissionSupported();
args.add("-g");
}
return internalInstallPackages(packageFiles, reinstall, args);
}
/** {@inheritDoc} */
@Override
public String installPackagesForUser(
List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)
throws DeviceNotAvailableException {
// Grant all permissions by default if feature is supported
return installPackagesForUser(
packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs);
}
/** {@inheritDoc} */
@Override
public String installPackagesForUser(
List<File> packageFiles,
boolean reinstall,
boolean grantPermissions,
int userId,
String... extraArgs)
throws DeviceNotAvailableException {
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
ensureRuntimePermissionSupported();
args.add("-g");
}
args.add("--user");
args.add(Integer.toString(userId));
return internalInstallPackages(packageFiles, reinstall, args);
}
/**
* Core implementation for split apk remote installation {@link IDevice#installPackage(String,
* boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on
* how to split apk to several files.
*
* @param remoteApkPaths the remote apk file paths
* @param reinstall <code>true</code> if a reinstall should be performed
* @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available
* options.
* @return the response from the installation <code>null</code> if installation succeeds.
* @throws DeviceNotAvailableException
*/
private String internalInstallRemotePackages(
final List<String> remoteApkPaths,
final boolean reinstall,
final List<String> extraArgs)
throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction =
new DeviceAction() {
@Override
public boolean run() throws InstallException {
try {
getIDevice()
.installRemotePackages(
remoteApkPaths,
reinstall,
extraArgs,
INSTALL_TIMEOUT_MINUTES,
TimeUnit.MINUTES);
response[0] = null;
return true;
} catch (InstallException e) {
response[0] = handleInstallationError(e);
return false;
}
}
};
performDeviceAction(
String.format("install %s", remoteApkPaths.toString()),
installAction,
MAX_RETRY_ATTEMPTS);
return response[0];
}
/** {@inheritDoc} */
@Override
public String installRemotePackages(
final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)
throws DeviceNotAvailableException {
// Grant all permissions by default if feature is supported
return installRemotePackages(
remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs);
}
/** {@inheritDoc} */
@Override
public String installRemotePackages(
List<String> remoteApkPaths,
boolean reinstall,
boolean grantPermissions,
String... extraArgs)
throws DeviceNotAvailableException {
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
ensureRuntimePermissionSupported();
args.add("-g");
}
return internalInstallRemotePackages(remoteApkPaths, reinstall, args);
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
return getScreenshot("PNG");
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
return getScreenshot(format, true);
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot(String format, boolean rescale)
throws DeviceNotAvailableException {
if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
format = "PNG";
}
ScreenshotAction action = new ScreenshotAction();
if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
byte[] imageData =
compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale);
if (imageData != null) {
return new ByteArrayInputStreamSource(imageData);
}
}
// Return an error in the buffer
return new ByteArrayInputStreamSource(
"Error: device reported null for screenshot.".getBytes());
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot(long displayId) throws DeviceNotAvailableException {
final String tmpDevicePath = String.format("/data/local/tmp/display_%s.png", displayId);
CommandResult result =
executeShellV2Command(
String.format("screencap -p -d %s %s", displayId, tmpDevicePath));
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
// Return an error in the buffer
CLog.e("Error: device reported error for screenshot:");
CLog.e("stdout: %s\nstderr: %s", result.getStdout(), result.getStderr());
return null;
}
try {
File tmpScreenshot = pullFile(tmpDevicePath);
if (tmpScreenshot == null) {
return null;
}
return new FileInputStreamSource(tmpScreenshot, true);
} finally {
deleteFile(tmpDevicePath);
}
}
private class ScreenshotAction implements DeviceAction {
RawImage mRawScreenshot;
/**
* {@inheritDoc}
*/
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
mRawScreenshot =
getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS);
return mRawScreenshot != null;
}
}
/**
* Helper to compress a rawImage obtained from the screen.
*
* @param rawImage {@link RawImage} to compress.
* @param format resulting format of compressed image. PNG and JPEG are supported.
* @param rescale if rescaling should be done to further reduce size of compressed image.
* @return compressed image.
*/
@VisibleForTesting
byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) {
BufferedImage image = rawImageToBufferedImage(rawImage, format);
// Rescale to reduce size if needed
// Screenshot default format is 1080 x 1920, 8-bit/color RGBA
// By cutting in half we can easily keep good quality and smaller size
if (rescale) {
image = rescaleImage(image);
}
return getImageData(image, format);
}
/**
* Converts {@link RawImage} to {@link BufferedImage} in specified format.
*
* @param rawImage {@link RawImage} to convert.
* @param format resulting format of image. PNG and JPEG are supported.
* @return converted image.
*/
@VisibleForTesting
BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) {
BufferedImage image = null;
if ("JPEG".equalsIgnoreCase(format)) {
//JPEG does not support ARGB without a special encoder
image =
new BufferedImage(
rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR);
}
else {
image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB);
}
// borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
int index = 0;
int IndexInc = rawImage.bpp >> 3;
for (int y = 0 ; y < rawImage.height ; y++) {
for (int x = 0 ; x < rawImage.width ; x++) {
int value = rawImage.getARGB(index);
index += IndexInc;
image.setRGB(x, y, value);
}
}
return image;
}
/**
* Rescales image cutting it in half.
*
* @param image source {@link BufferedImage}.
* @return resulting scaled image.
*/
@VisibleForTesting
BufferedImage rescaleImage(BufferedImage image) {
int shortEdge = Math.min(image.getHeight(), image.getWidth());
if (shortEdge > 720) {
Image resized =
image.getScaledInstance(
image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH);
image =
new BufferedImage(
image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE);
image.getGraphics().drawImage(resized, 0, 0, null);
}
return image;
}
/**
* Gets byte array representation of {@link BufferedImage}.
*
* @param image source {@link BufferedImage}.
* @param format resulting format of image. PNG and JPEG are supported.
* @return byte array representation of the image.
*/
@VisibleForTesting
byte[] getImageData(BufferedImage image, String format) {
// store compressed image in memory, and let callers write to persistent storage
// use initial buffer size of 128K
byte[] imageData = null;
ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
try {
if (ImageIO.write(image, format, imageOut)) {
imageData = imageOut.toByteArray();
} else {
CLog.e("Failed to compress screenshot to png");
}
} catch (IOException e) {
CLog.e("Failed to compress screenshot to png");
CLog.e(e);
}
StreamUtil.close(imageOut);
return imageData;
}
/**
* {@inheritDoc}
*/
@Override
public boolean clearErrorDialogs() throws DeviceNotAvailableException {
// attempt to clear error dialogs multiple times
for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
int numErrorDialogs = getErrorDialogCount();
if (numErrorDialogs == 0) {
return true;
}
doClearDialogs(numErrorDialogs);
}
if (getErrorDialogCount() > 0) {
// at this point, all attempts to clear error dialogs completely have failed
// it might be the case that the process keeps showing new dialogs immediately after
// clearing. There's really no workaround, but to dump an error
CLog.e("error dialogs still exist on %s.", getSerialNumber());
return false;
}
return true;
}
/**
* Detects the number of crash or ANR dialogs currently displayed.
* <p/>
* Parses output of 'dump activity processes'
*
* @return count of dialogs displayed
* @throws DeviceNotAvailableException
*/
private int getErrorDialogCount() throws DeviceNotAvailableException {
int errorDialogCount = 0;
Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
String systemStatusOutput =
executeShellCommand(
"dumpsys activity processes | grep -e .*crashing=true.*AppErrorDialog.* -e"
+ " .*notResponding=true.*AppNotRespondingDialog.*");
Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
while (crashMatcher.find()) {
errorDialogCount++;
}
Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
while (anrMatcher.find()) {
errorDialogCount++;
}
return errorDialogCount;
}
private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
for (int i=0; i < numDialogs; i++) {
// send DPAD_CENTER
executeShellCommand(DISMISS_DIALOG_CMD);
}
}
/**
* {@inheritDoc}
*/
@Override
public void disableKeyguard() throws DeviceNotAvailableException {
long start = System.currentTimeMillis();
while (true) {
Boolean ready = isDeviceInputReady();
if (ready == null) {
// unsupported API level, bail
break;
}
if (ready) {
// input dispatch is ready, bail
break;
}
long timeSpent = System.currentTimeMillis() - start;
if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
// break & proceed anyway
break;
} else {
getRunUtil().sleep(1000);
}
}
if (getApiLevel() >= 23) {
CLog.i(
"Attempting to disable keyguard on %s using %s",
getSerialNumber(), DISMISS_KEYGUARD_WM_CMD);
String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD);
CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output);
} else {
CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD,
DISMISS_KEYGUARD_CMD);
executeShellCommand(DISMISS_KEYGUARD_CMD);
}
// TODO: check that keyguard was actually dismissed.
}
/** {@inheritDoc} */
@Override
public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
String output =
executeShellCommand("dumpsys activity activities | grep -A3 KeyguardController:");
CLog.d("Output from KeyguardController: %s", output);
KeyguardControllerState state =
KeyguardControllerState.create(Arrays.asList(output.trim().split("\n")));
return state;
}
/**
* Tests the device to see if input dispatcher is ready
*
* @return <code>null</code> if not supported by platform, or the actual readiness state
* @throws DeviceNotAvailableException
*/
Boolean isDeviceInputReady() throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(TEST_INPUT_CMD, receiver);
String output = receiver.getOutput();
Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
if (!m.find()) {
// output does not contain the line at all, implying unsupported API level, bail
return null;
}
return "1".equals(m.group(1));
}
/**
* {@inheritDoc}
*/
@Override
protected void prePostBootSetup() throws DeviceNotAvailableException {
if (mOptions.isDisableKeyguard()) {
disableKeyguard();
}
}
/**
* Performs an reboot via framework power manager
*
* <p>Must have root access, device must be API Level 18 or above
*
* @param rebootMode a mode of this reboot.
* @param reason for this reboot.
* @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
* unsupported
* @throws DeviceNotAvailableException
*/
private boolean doAdbFrameworkReboot(RebootMode rebootMode, @Nullable final String reason)
throws DeviceNotAvailableException {
// use framework reboot when:
// 1. device API level >= 18
// 2. has adb root
// 3. framework is running
if (!isEnableAdbRoot()) {
CLog.i("framework reboot is not supported; when enable root is disabled");
return false;
}
boolean isRoot = enableAdbRoot();
if (getApiLevel() >= 18 && isRoot) {
try {
// check framework running
String output = executeShellCommand("pm path android");
if (output == null || !output.contains("package:")) {
CLog.v("framework reboot: can't detect framework running");
return false;
}
notifyRebootStarted();
String command = "svc power reboot " + rebootMode.formatRebootCommand(reason);
CommandResult result = executeShellV2Command(command);
if (result.getStdout().contains(EARLY_REBOOT)
|| result.getStderr().contains(EARLY_REBOOT)) {
CLog.e(
"Reboot was called too early: stdout: %s.\nstderr: %s.",
result.getStdout(), result.getStderr());
// notify of this reboot end, since reboot will be retried again at later stage.
notifyRebootEnded();
return false;
}
} catch (DeviceUnresponsiveException due) {
CLog.v("framework reboot: device unresponsive to shell command, using fallback");
return false;
}
postAdbReboot();
return true;
} else {
CLog.v("framework reboot: not supported");
return false;
}
}
/**
* Perform a adb reboot.
*
* @param rebootMode a mode of this reboot.
* @param reason for this reboot.
* @throws DeviceNotAvailableException
*/
@Override
protected void doAdbReboot(RebootMode rebootMode, @Nullable final String reason)
throws DeviceNotAvailableException {
getConnection().notifyAdbRebootCalled();
if (!TestDeviceState.ONLINE.equals(getDeviceState())
|| !doAdbFrameworkReboot(rebootMode, reason)) {
super.doAdbReboot(rebootMode, reason);
}
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
return getInstalledPackageNames(null, null);
}
// TODO: convert this to use DumpPkgAction
private Set<String> getInstalledPackageNames(String packageNameSearched, String userId)
throws DeviceNotAvailableException {
Set<String> packages= new HashSet<String>();
String command = LIST_PACKAGES_CMD;
if (userId != null) {
command += String.format(" --user %s", userId);
}
if (packageNameSearched != null) {
command += (" | grep " + packageNameSearched);
}
String output = executeShellCommand(command);
if (output != null) {
Matcher m = PACKAGE_REGEX.matcher(output);
while (m.find()) {
String packageName = m.group(2);
if (packageNameSearched != null && packageName.equals(packageNameSearched)) {
packages.add(packageName);
} else if (packageNameSearched == null) {
packages.add(packageName);
}
}
}
return packages;
}
/** {@inheritDoc} */
@Override
public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException {
return getInstalledPackageNames(packageName, null).contains(packageName);
}
/** {@inheritDoc} */
@Override
public boolean isPackageInstalled(String packageName, String userId)
throws DeviceNotAvailableException {
return getInstalledPackageNames(packageName, userId).contains(packageName);
}
/** {@inheritDoc} */
@Override
public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException {
String output = executeShellCommand(LIST_APEXES_CMD);
// Optimistically parse expecting platform to return paths. If it doesn't, empty set will
// be returned.
Set<ApexInfo> ret = parseApexesFromOutput(output, true /* withPath */);
if (ret.isEmpty()) {
ret = parseApexesFromOutput(output, false /* withPath */);
}
return ret;
}
/** {@inheritDoc} */
@Override
public Set<String> getMainlineModuleInfo() throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease(GET_MODULEINFOS_CMD, 29);
Set<String> ret = new HashSet<>();
String output = executeShellCommand(GET_MODULEINFOS_CMD);
if (output != null) {
Matcher m = MODULEINFO_REGEX.matcher(output);
while (m.find()) {
String packageName = m.group(2);
ret.add(packageName);
}
}
return ret;
}
private Set<ApexInfo> parseApexesFromOutput(final String output, boolean withPath) {
Set<ApexInfo> ret = new HashSet<>();
Matcher matcher =
withPath
? APEXES_WITH_PATH_REGEX.matcher(output)
: APEXES_WITHOUT_PATH_REGEX.matcher(output);
while (matcher.find()) {
if (withPath) {
String sourceDir = matcher.group(1);
String name = matcher.group(2);
long version = Long.valueOf(matcher.group(3));
ret.add(new ApexInfo(name, version, sourceDir));
} else {
String name = matcher.group(1);
long version = Long.valueOf(matcher.group(2));
ret.add(new ApexInfo(name, version));
}
}
return ret;
}
/**
* A {@link com.android.tradefed.device.NativeDevice.DeviceAction}
* for retrieving package system service info, and do retries on
* failures.
*/
private class DumpPkgAction implements DeviceAction {
Map<String, PackageInfo> mPkgInfoMap;
DumpPkgAction() {
}
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
getIDevice().executeShellCommand("dumpsys package p", receiver);
mPkgInfoMap = receiver.getPackages();
if (mPkgInfoMap.size() == 0) {
// Package parsing can fail if package manager is currently down. throw exception
// to retry
CLog.w("no packages found from dumpsys package p.");
throw new IOException();
}
return true;
}
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
DumpPkgAction action = new DumpPkgAction();
performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
Set<String> pkgs = new HashSet<String>();
for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
pkgs.add(pkgInfo.getPackageName());
}
}
return pkgs;
}
/**
* {@inheritDoc}
*/
@Override
public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
DumpPkgAction action = new DumpPkgAction();
performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
return action.mPkgInfoMap.get(packageName);
}
/** {@inheritDoc} */
@Override
public List<PackageInfo> getAppPackageInfos() throws DeviceNotAvailableException {
DumpPkgAction action = new DumpPkgAction();
performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
return new ArrayList<>(action.mPkgInfoMap.values());
}
/** {@inheritDoc} */
@Override
public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
int currentUser = 0;
if (deviceFilePath.startsWith(SD_CARD)) {
if (getApiLevel() > 23) {
// Don't trigger the current logic if unsupported
currentUser = getCurrentUser();
}
}
return doesFileExist(deviceFilePath, currentUser);
}
/** {@inheritDoc} */
@Override
public boolean doesFileExist(String deviceFilePath, int userId)
throws DeviceNotAvailableException {
if (deviceFilePath.startsWith(SD_CARD)) {
deviceFilePath =
deviceFilePath.replaceFirst(
SD_CARD, String.format("/storage/emulated/%s/", userId));
}
return super.doesFileExist(deviceFilePath, userId);
}
/**
* {@inheritDoc}
*/
@Override
public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
ArrayList<String[]> users = tokenizeListUsers();
ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
for (String[] user : users) {
userIds.add(Integer.parseInt(user[1]));
}
return userIds;
}
/** {@inheritDoc} */
@Override
public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
ArrayList<String[]> lines = tokenizeListUsers();
Map<Integer, UserInfo> result = new HashMap<Integer, UserInfo>(lines.size());
for (String[] tokens : lines) {
if (getApiLevel() < 33) {
UserInfo userInfo =
new UserInfo(
/* userId= */ Integer.parseInt(tokens[1]),
/* userName= */ tokens[2],
/* flag= */ Integer.parseInt(tokens[3], 16),
/* isRunning= */ tokens.length >= 5
? tokens[4].contains("running")
: false);
result.put(userInfo.userId(), userInfo);
} else {
UserInfo userInfo =
new UserInfo(
/* userId= */ Integer.parseInt(tokens[1]),
/* userName= */ tokens[2],
/* flag= */ Integer.parseInt(tokens[3], 16),
/* isRunning= */ tokens.length >= 5
? tokens[4].contains("running")
: false,
/* userType= */ tokens[5]);
result.put(userInfo.userId(), userInfo);
}
}
return result;
}
/**
* Tokenizes the output of 'pm list users' pre-T and 'cmd user list -v' post-T.
*
* <p>Pre-T: The returned tokens for each user have the form: {"\tUserInfo",
* Integer.toString(id), name, Integer.toHexString(flag), "[running]"}; (the last one being
* optional)
*
* <p>Post-T: The returned tokens for each user have the form: {"\tUserInfo", Integer
* .toString(id), name, Integer.toHexString(flag), "[running]", type}; (the last two 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() throws DeviceNotAvailableException {
if (getApiLevel() < 33) { // Android-T
return tokenizeListUsersPreT();
} else {
return tokenizeListUserPostT();
}
}
private ArrayList<String[]> tokenizeListUserPostT() throws DeviceNotAvailableException {
String command = "cmd user list -v";
String commandOutput = executeShellCommand(command);
// Extract the id of all existing users.
List<String> lines =
Arrays.stream(commandOutput.split("\\r?\\n"))
.filter(line -> line != null && line.trim().length() != 0)
.collect(Collectors.toList());
if (!lines.get(0).contains("users:")) {
if (commandOutput.contains("cmd: Can't find service: package")) {
throw new DeviceNotAvailableException(
String.format(
"'%s' in not a valid output for 'user list -v'", commandOutput),
getSerialNumber());
}
throw new DeviceRuntimeException(
String.format("'%s' in not a valid output for 'user list -v'", commandOutput),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
ArrayList<String[]> users = new ArrayList<String[]>(lines.size() - 1);
String pattern = ".id=(.*), name=(.*), type=(.*), flags=(.*)";
Pattern r = Pattern.compile(pattern);
for (int i = 1; i < lines.size(); i++) {
// Individual user is printed out like this:
// idx: id=$id, name=$name, type=$type, flags=AAA|BBB|XXX (running) (current) (visible)
Matcher m = r.matcher(lines.get(i));
if (m.find()) {
String id = m.group(1);
String name = m.group(2);
// example: full.SYSTEM, profile.XXX
String type = m.group(3);
// AAA|BBB|XXX (running) (current) (visible)
String flags_and_status = m.group(4);
String flags = "";
String status = "";
if (flags_and_status != null) {
// Split flags and convert to hex
// output: [AAA, BBB, XXX (running) (current) (visible)]
String[] flagsArr = flags_and_status.split("\\|");
// XXX (running) (current) (visible)
String last_flag_and_status =
flagsArr.length > 0 ? flagsArr[flagsArr.length - 1] : "";
String[] arr = last_flag_and_status.split("\\s", 2);
if (arr.length > 0) {
flags = Integer.toHexString(convertToHex(flagsArr, arr[0]));
}
if (arr.length > 1) {
status = arr[1] != null ? arr[1] : "";
}
}
// Maintain same sequence as per-Q output, add type at the end.
users.add(new String[] {"", id, name, flags, status, type});
}
}
return users;
}
private ArrayList<String[]> tokenizeListUsersPreT() throws DeviceNotAvailableException {
String command = "pm list users";
String commandOutput = executeShellCommand(command);
// Extract the id of all existing users.
String[] lines = commandOutput.split("\\r?\\n");
if (!lines[0].equals("Users:")) {
throw new DeviceRuntimeException(
String.format("'%s' in not a valid output for 'pm list users'", commandOutput),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
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 DeviceRuntimeException(
String.format(
"device output: '%s' \nline: '%s' was not in the expected "
+ "format for user info.",
commandOutput, lines[i]),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
users.add(tokens);
}
return users;
}
private int convertToHex(String[] arr, String str) {
int res = 0;
for (int i = 0; i < arr.length - 1; i++) {
res |= getHexaDecimalValue(arr[i]);
}
res |= getHexaDecimalValue(str);
return res;
}
private int getHexaDecimalValue(String flag) {
switch (flag) {
case "PRIMARY":
return 0x00000001;
case "ADMIN":
return 0x00000002;
case "GUEST":
return 0x00000004;
case "RESTRICTED":
return 0x00000008;
case "INITIALIZED":
return 0x00000010;
case "MANAGED_PROFILE":
return 0x00000020;
case "DISABLED":
return 0x00000040;
case "QUIET_MODE":
return 0x00000080;
case "EPHEMERAL":
return 0x00000100;
case "DEMO":
return 0x00000200;
case "FULL":
return 0x00000400;
case "SYSTEM":
return 0x00000800;
case "PROFILE":
return 0x00001000;
case "EPHEMERAL_ON_CREATE":
return 0x00002000;
case "MAIN":
return 0x00004000;
case "FOR_TESTING":
return 0x00008000;
default:
CLog.e("Flag %s not found.", flag);
return 0;
}
}
/**
* {@inheritDoc}
*/
@Override
public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
String command = "pm get-max-users";
String commandOutput = executeShellCommand(command);
try {
return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", commandOutput);
}
return 0;
}
/** {@inheritDoc} */
@Override
public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("get-max-running-users", 28);
String command = "pm get-max-running-users";
String commandOutput = executeShellCommand(command);
try {
return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", commandOutput);
}
return 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMultiUserSupported() throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("get-max-running-users", 28);
final int apiLevel = getApiLevel();
if (apiLevel > 33) {
String command = "pm supports-multiple-users";
String commandOutput = executeShellCommand(command).trim();
try {
String parsedOutput =
commandOutput.substring(commandOutput.lastIndexOf(" ")).trim();
Boolean retValue = Boolean.valueOf(parsedOutput);
return retValue;
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", commandOutput);
return false;
}
}
return getMaxNumberOfUsersSupported() > 1;
}
@Override
public boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
checkApiLevelAgainst("isHeadlessSystemUserMode", 29);
return checkApiLevelAgainstNextRelease(34)
? executeShellV2CommandThatReturnsBooleanSafe(
"cmd user is-headless-system-user-mode")
: getBooleanProperty("ro.fw.mu.headless_system_user", false);
}
/** {@inheritDoc} */
@Override
public boolean canSwitchToHeadlessSystemUser() throws DeviceNotAvailableException {
checkApiLevelAgainst("canSwitchToHeadlessSystemUser", 34);
return executeShellV2CommandThatReturnsBooleanSafe(
"cmd user can-switch-to-headless-system-user");
}
/** {@inheritDoc} */
@Override
public boolean isMainUserPermanentAdmin() throws DeviceNotAvailableException {
checkApiLevelAgainst("isMainUserPermanentAdmin", 34);
return executeShellV2CommandThatReturnsBooleanSafe("cmd user is-main-user-permanent-admin");
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
return createUser(name, false, false);
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name, boolean guest, boolean ephemeral)
throws DeviceNotAvailableException, IllegalStateException {
return createUser(name, guest, ephemeral, /* forTesting= */ false);
}
/** {@inheritDoc} */
@Override
public int createUser(String name, boolean guest, boolean ephemeral, boolean forTesting)
throws DeviceNotAvailableException, IllegalStateException {
String command =
"pm create-user "
+ (guest ? "--guest " : "")
+ (ephemeral ? "--ephemeral " : "")
+ (forTesting && getApiLevel() >= 34 ? "--for-testing " : "")
+ name;
final String output = executeShellCommand(command);
if (output.startsWith("Success")) {
try {
resetContentProviderSetup();
return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", output);
}
}
throw new IllegalStateException(String.format("Failed to create user: %s", output));
}
/** {@inheritDoc} */
@Override
public int createUserNoThrow(String name) throws DeviceNotAvailableException {
try {
return createUser(name);
} catch (IllegalStateException e) {
CLog.e("Error creating user: " + e.toString());
return -1;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeUser(int userId) throws DeviceNotAvailableException {
final String output = executeShellCommand(String.format("pm remove-user %s", userId));
if (output.startsWith("Error")) {
CLog.e("Failed to remove user %d on device %s: %s", userId, getSerialNumber(), output);
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean startUser(int userId) throws DeviceNotAvailableException {
return startUser(userId, false);
}
/** {@inheritDoc} */
@Override
public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException {
if (waitFlag) {
checkApiLevelAgainstNextRelease("start-user -w", 29);
}
String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId;
CLog.d("Starting user with command: %s", cmd);
final String output = executeShellCommand(cmd);
if (output.startsWith("Error")) {
CLog.e("Failed to start user: %s", output);
return false;
}
if (waitFlag) {
String state = executeShellCommand("am get-started-user-state " + userId);
if (!state.contains("RUNNING_UNLOCKED")) {
CLog.w("User %s is not RUNNING_UNLOCKED after start-user -w. (%s).", userId, state);
return false;
}
}
return true;
}
@Override
public boolean startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag)
throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("startVisibleBackgroundUser", 34);
String cmd =
String.format(
"am start-user%s --display %d %d",
(waitFlag ? " -w" : ""), displayId, userId);
CommandResult res = executeShellV2Command(cmd);
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
throw new DeviceRuntimeException(
"Command '" + cmd + "' failed: " + res,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
return res.getStdout().trim().startsWith("Success");
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId) throws DeviceNotAvailableException {
// No error or status code is returned.
return stopUser(userId, false, false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
throws DeviceNotAvailableException {
final int apiLevel = getApiLevel();
if (waitFlag && apiLevel < 23) {
throw new IllegalArgumentException("stop-user -w requires API level >= 23");
}
if (forceFlag && apiLevel < 24) {
throw new IllegalArgumentException("stop-user -f requires API level >= 24");
}
StringBuilder cmd = new StringBuilder("am stop-user ");
if (waitFlag) {
cmd.append("-w ");
}
if (forceFlag) {
cmd.append("-f ");
}
cmd.append(userId);
CLog.d("stopping user with command: %s", cmd.toString());
final String output = executeShellCommand(cmd.toString());
if (output.contains("Error: Can't stop system user")) {
CLog.e("Cannot stop System user.");
return false;
}
if (output.contains("Can't stop current user")) {
CLog.e("Cannot stop current user.");
return false;
}
if (isUserRunning(userId)) {
CLog.w("User Id: %s is still running after the stop-user command.", userId);
return false;
}
return true;
}
@Override
public boolean isVisibleBackgroundUsersSupported() throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("isHeadlessSystemUserMode", 34);
return executeShellV2CommandThatReturnsBoolean(
"cmd user is-visible-background-users-supported");
}
@Override
public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported()
throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("isVisibleBackgroundUsersOnDefaultDisplaySupported", 34);
return executeShellV2CommandThatReturnsBoolean(
"cmd user is-visible-background-users-on-default-display-supported");
}
/**
* {@inheritDoc}
*/
@Override
public Integer getPrimaryUserId() throws DeviceNotAvailableException {
return getUserIdByFlag(FLAG_PRIMARY);
}
/** {@inheritDoc} */
@Override
public Integer getMainUserId() throws DeviceNotAvailableException {
return getUserIdByFlag(FLAG_MAIN);
}
private Integer getUserIdByFlag(int requiredFlag) throws DeviceNotAvailableException {
ArrayList<String[]> users = tokenizeListUsers();
for (String[] user : users) {
int flag = Integer.parseInt(user[3], 16);
if ((flag & requiredFlag) != 0) {
return Integer.parseInt(user[1]);
}
}
return null;
}
/** {@inheritDoc} */
@Override
public int getCurrentUser() throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER);
final String output = executeShellCommand("am get-current-user");
try {
int userId = Integer.parseInt(output.trim());
if (userId >= 0) {
return userId;
}
CLog.e("Invalid user id '%s' was returned for get-current-user", userId);
} catch (NumberFormatException e) {
CLog.e("Invalid string was returned for get-current-user: %s.", output);
}
return INVALID_USER_ID;
}
@Override
public boolean isUserVisible(int userId) throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("isUserVisible", 34);
return executeShellV2CommandThatReturnsBoolean("cmd user is-user-visible %d", userId);
}
@Override
public boolean isUserVisibleOnDisplay(int userId, int displayId)
throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("isUserVisibleOnDisplay", 34);
return executeShellV2CommandThatReturnsBoolean(
"cmd user is-user-visible --display %d %d", displayId, userId);
}
private Matcher findUserInfo(String pmListUsersOutput) {
Pattern pattern = Pattern.compile(USER_PATTERN);
Matcher matcher = pattern.matcher(pmListUsersOutput);
return matcher;
}
/**
* {@inheritDoc}
*/
@Override
public int getUserFlags(int userId) throws DeviceNotAvailableException {
checkApiLevelAgainst("getUserFlags", 22);
final String commandOutput = executeShellCommand("pm list users");
Matcher matcher = findUserInfo(commandOutput);
while(matcher.find()) {
if (Integer.parseInt(matcher.group(2)) == userId) {
return Integer.parseInt(matcher.group(6), 16);
}
}
CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput);
return INVALID_USER_ID;
}
/** {@inheritDoc} */
@Override
public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
if (userId == UserInfo.USER_SYSTEM) {
return false;
}
int flags = getUserFlags(userId);
if (flags == INVALID_USER_ID) {
return false;
}
return (flags & UserInfo.FLAGS_NOT_SECONDARY) == 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
checkApiLevelAgainst("isUserIdRunning", 22);
final String commandOutput = executeShellCommand("pm list users");
Matcher matcher = findUserInfo(commandOutput);
while(matcher.find()) {
if (Integer.parseInt(matcher.group(2)) == userId) {
if (matcher.group(7).contains("running")) {
return true;
}
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
checkApiLevelAgainst("getUserSerialNumber", 22);
final String commandOutput = executeShellCommand("dumpsys user");
// example: UserInfo{0:Test:13} serialNo=0
String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)";
Pattern pattern = Pattern.compile(userSerialPatter);
Matcher matcher = pattern.matcher(commandOutput);
while(matcher.find()) {
if (Integer.parseInt(matcher.group(2)) == userId) {
return Integer.parseInt(matcher.group(5));
}
}
CLog.w("Could not find user serial number for userId: %d, in output: %s",
userId, commandOutput);
return INVALID_USER_ID;
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId) throws DeviceNotAvailableException {
return switchUser(userId, AM_COMMAND_TIMEOUT);
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER);
if (userId == getCurrentUser()) {
CLog.w("Already running as user id: %s. Nothing to be done.", userId);
return true;
}
String switchCommand =
checkApiLevelAgainstNextRelease(30)
? String.format("am switch-user -w %d", userId)
: String.format("am switch-user %d", userId);
resetContentProviderSetup();
long initialTime = getHostCurrentTime();
String output = executeShellCommand(switchCommand);
boolean success = userId == getCurrentUser();
while (!success && (getHostCurrentTime() - initialTime <= timeout)) {
// retry
RunUtil.getDefault().sleep(getCheckNewUserSleep());
output = executeShellCommand(String.format(switchCommand));
success = userId == getCurrentUser();
}
CLog.d("switchUser took %d ms", getHostCurrentTime() - initialTime);
if (success) {
prePostBootSetup();
return true;
} else {
CLog.e("User did not switch in the given %d timeout: %s", timeout, output);
return false;
}
}
/**
* Exposed for testing.
*/
protected long getCheckNewUserSleep() {
return CHECK_NEW_USER;
}
/**
* Exposed for testing
*/
protected long getHostCurrentTime() {
return System.currentTimeMillis();
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFeature(String feature) throws DeviceNotAvailableException {
// Add support for directly checking a feature and match the pm output.
if (!feature.startsWith("feature:")) {
feature = "feature:" + feature;
}
final String versionedFeature = feature + "=";
String commandOutput = executeShellCommand("pm list features");
for (String line: commandOutput.split("\\s+")) {
// Each line in the output of the command has the format
// "feature:{FEATURE_VALUE}[={FEATURE_VERSION}]".
if (line.equals(feature)) {
return true;
}
if (line.startsWith(versionedFeature)) {
return true;
}
}
CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
return false;
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(String namespace, String key) throws DeviceNotAvailableException {
return getSettingInternal("", namespace.trim(), key.trim());
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(int userId, String namespace, String key)
throws DeviceNotAvailableException {
return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim());
}
/**
* Internal Helper to get setting with or without a userId provided.
*/
private String getSettingInternal(String userFlag, String namespace, String key)
throws DeviceNotAvailableException {
namespace = namespace.toLowerCase();
if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
String cmd = String.format("settings %s get %s %s", userFlag, namespace, key);
String output = executeShellCommand(cmd);
if ("null".equals(output)) {
CLog.w("settings returned null for command: %s. "
+ "please check if the namespace:key exists", cmd);
return null;
}
return output.trim();
}
CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
return null;
}
/** {@inheritDoc} */
@Override
public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
return getAllSettingsInternal(namespace.trim());
}
/** Internal helper to get all settings */
private Map<String, String> getAllSettingsInternal(String namespace)
throws DeviceNotAvailableException {
namespace = namespace.toLowerCase();
if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
Map<String, String> map = new HashMap<>();
String cmd = String.format("settings list %s", namespace);
String output = executeShellCommand(cmd);
for (String line : output.split("\\n")) {
// Setting's value could be empty
String[] pair = line.trim().split("=", -1);
if (pair.length > 1) {
map.putIfAbsent(pair[0], pair[1]);
} else {
CLog.e("Unable to get setting from string: %s", line);
}
}
return map;
}
CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(String namespace, String key, String value)
throws DeviceNotAvailableException {
setSettingInternal("", namespace.trim(), key.trim(), value.trim());
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(int userId, String namespace, String key, String value)
throws DeviceNotAvailableException {
setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(),
value.trim());
}
/**
* Internal helper to set a setting with or without a userId provided.
*/
private void setSettingInternal(String userFlag, String namespace, String key, String value)
throws DeviceNotAvailableException {
checkApiLevelAgainst("Changing settings", 22);
if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) {
executeShellCommand(String.format("settings %s put %s %s %s",
userFlag, namespace, key, value));
} else {
throw new IllegalArgumentException("Namespace must be one of system, secure, global."
+ " You provided: " + namespace);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getAndroidId(int userId) throws DeviceNotAvailableException {
if (isAdbRoot()) {
String cmd = String.format(
"sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db "
+ "'select value from main where name = \"android_id\"'", userId);
String output = executeShellCommand(cmd).trim();
if (!output.contains("unable to open database")) {
return output;
}
CLog.w("Couldn't find android-id, output: %s", output);
} else {
CLog.w("adb root is required.");
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
ArrayList<Integer> userIds = listUsers();
if (userIds == null) {
return null;
}
Map<Integer, String> androidIds = new HashMap<Integer, String>();
for (Integer id : userIds) {
String androidId = getAndroidId(id);
androidIds.put(id, androidId);
}
return androidIds;
}
/**
* {@inheritDoc}
*/
@Override
IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
return createWifiHelper(true);
}
/**
* Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper
* setup or not.
*/
@VisibleForTesting
IWifiHelper createWifiHelper(boolean doSetup) throws DeviceNotAvailableException {
if (doSetup) {
mWasWifiHelperInstalled = true;
// Ensure device is ready before attempting wifi setup
waitForDeviceAvailable();
}
return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup);
}
/** {@inheritDoc} */
@Override
public void postInvocationTearDown(Throwable exception) {
super.postInvocationTearDown(exception);
// If wifi was installed and it's a real device, attempt to clean it.
if (mWasWifiHelperInstalled) {
mWasWifiHelperInstalled = false;
if (getIDevice() instanceof StubDevice) {
return;
}
if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
return;
}
if (exception instanceof DeviceNotAvailableException) {
CLog.e("Skip WifiHelper teardown due to DeviceNotAvailableException.");
return;
}
try {
// Uninstall the wifi utility if it was installed.
IWifiHelper wifi = createWifiHelper(false);
wifi.cleanUp();
} catch (DeviceNotAvailableException e) {
CLog.e("Device became unavailable while uninstalling wifi util.");
CLog.e(e);
}
}
}
/** {@inheritDoc} */
@Override
public boolean setDeviceOwner(String componentName, int userId)
throws DeviceNotAvailableException {
final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
final String commandOutput = executeShellCommand(command);
return commandOutput.startsWith("Success:");
}
/** {@inheritDoc} */
@Override
public boolean removeAdmin(String componentName, int userId)
throws DeviceNotAvailableException {
final String command =
"dpm remove-active-admin --user " + userId + " '" + componentName + "'";
final String commandOutput = executeShellCommand(command);
return commandOutput.startsWith("Success:");
}
/** {@inheritDoc} */
@Override
public void removeOwners() throws DeviceNotAvailableException {
String command = "dumpsys device_policy";
String commandOutput = executeShellCommand(command);
String[] lines = commandOutput.split("\\r?\\n");
for (int i = 0; i < lines.length; ++i) {
String line = lines[i].trim();
if (line.contains("Profile Owner")) {
// Line is "Profile owner (User <id>):
String[] tokens = line.split("\\(|\\)| ");
int userId = Integer.parseInt(tokens[4]);
i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
line = lines[i].trim();
// Line is admin=ComponentInfo{<component>}
tokens = line.split("\\{|\\}");
String componentName = tokens[1];
CLog.d("Cleaning up profile owner " + userId + " " + componentName);
removeAdmin(componentName, userId);
} else if (line.contains("Device Owner:")) {
i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
line = lines[i].trim();
// Line is admin=ComponentInfo{<component>}
String[] tokens = line.split("\\{|\\}");
String componentName = tokens[1];
// Skip to user id line.
i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i);
line = lines[i].trim();
// Line is User ID: <N>
tokens = line.split(":");
int userId = Integer.parseInt(tokens[1].trim());
CLog.d("Cleaning up device owner " + userId + " " + componentName);
removeAdmin(componentName, userId);
}
}
}
/**
* Search forward from the current index to find a string matching the given regex.
*
* @param regex The regex to match each line against.
* @param lines An array of strings to be searched.
* @param currentIndex the index to start searching from.
* @return The index of a string beginning with the regex.
* @throws IllegalStateException if the line cannot be found.
*/
private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) {
while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) {
currentIndex++;
}
if (currentIndex >= lines.length) {
throw new IllegalStateException(
"The output of 'dumpsys device_policy' was not as expected. Owners have not "
+ "been removed. This will leave the device in an unstable state and "
+ "will lead to further test failures.");
}
return currentIndex;
}
/**
* Helper for Api level checking of features in the new release before we incremented the api
* number.
*/
private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)
throws DeviceNotAvailableException {
if (checkApiLevelAgainstNextRelease(strictMinLevel)) {
return;
}
throw new IllegalArgumentException(
String.format(
"%s not supported on %s. Must be API %d.",
feature, getSerialNumber(), strictMinLevel));
}
@Override
public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) {
throw new IllegalArgumentException("devicePath or process cannot be null or empty.");
}
String pid = getProcessPid(process);
if (pid == null) {
return null;
}
File dump = dumpAndPullHeap(pid, devicePath);
// Clean the device.
deleteFile(devicePath);
return dump;
}
/** Dump the heap file and pull it from the device. */
private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException {
executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath));
// Allow a little bit of time for the file to populate on device side.
int attempt = 0;
// TODO: add an API to check device file size
while (!doesFileExist(devicePath) && attempt < 3) {
getRunUtil().sleep(DUMPHEAP_TIME);
attempt++;
}
File dumpFile = pullFile(devicePath);
return dumpFile;
}
/** {@inheritDoc} */
@Override
public Set<Long> listDisplayIds() throws DeviceNotAvailableException {
Set<Long> displays = new HashSet<>();
CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'");
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
CLog.e("Something went wrong while listing displays: %s", res.getStderr());
return displays;
}
String output = res.getStdout();
Pattern p = Pattern.compile(DISPLAY_ID_PATTERN);
for (String line : output.split("\n")) {
Matcher m = p.matcher(line);
if (m.matches()) {
displays.add(Long.parseLong(m.group("id")));
}
}
// If the device is older and did not report any displays
// then add the default.
// Note: this assumption breaks down if the device also has multiple displays
if (displays.isEmpty()) {
// Zero is the default display
displays.add(0L);
}
return displays;
}
@Override
public Set<Integer> listDisplayIdsForStartingVisibleBackgroundUsers()
throws DeviceNotAvailableException {
checkApiLevelAgainstNextRelease("getDisplayIdsForStartingVisibleBackgroundUsers", 34);
String cmd = "cmd activity list-displays-for-starting-users";
CommandResult res = executeShellV2Command(cmd);
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
throw new DeviceRuntimeException(
"Command '" + cmd + "' failed: " + res,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
String output = res.getStdout().trim();
if (output.equalsIgnoreCase("none")) {
return Collections.emptySet();
}
// TODO: reuse some helper to parse the list
if (!output.startsWith("[") || !output.endsWith("]")) {
throw new DeviceRuntimeException(
"Invalid output for command '" + cmd + "': " + output,
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
String contents = output.substring(1, output.length() - 1);
try {
String[] ids = contents.split(",");
return Arrays.asList(ids).stream()
.map(id -> Integer.parseInt(id.trim()))
.collect(Collectors.toSet());
} catch (Exception e) {
throw new DeviceRuntimeException(
"Invalid output for command '" + cmd + "': " + output,
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
}
@Override
public Set<DeviceFoldableState> getFoldableStates() throws DeviceNotAvailableException {
if (getIDevice() instanceof StubDevice) {
return new HashSet<>();
}
try (CloseableTraceScope foldable = new CloseableTraceScope("getFoldableStates")) {
CommandResult result = executeShellV2Command("cmd device_state print-states");
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
// Can't throw an exception since it would fail on non-supported version
return new HashSet<>();
}
Set<DeviceFoldableState> foldableStates = new LinkedHashSet<>();
Pattern deviceStatePattern =
Pattern.compile(
"DeviceState\\{identifier=(\\d+), name='(\\S+)'"
+ "(?:, app_accessible=)?(\\S+)?"
+ "(?:, cancel_when_requester_not_on_top=)?(\\S+)?"
+ "\\}\\S*");
for (String line : result.getStdout().split("\n")) {
Matcher m = deviceStatePattern.matcher(line.trim());
if (m.matches()) {
// Move onto the next state if the device state is not accessible by apps
if (m.groupCount() > 2
&& m.group(3) != null
&& !Boolean.parseBoolean(m.group(3))) {
continue;
}
// Move onto the next state if the device state is canceled when the requesting
// app
// is not on top.
if (m.groupCount() > 3
&& m.group(4) != null
&& Boolean.parseBoolean(m.group(4))) {
continue;
}
foldableStates.add(
new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2)));
}
}
return foldableStates;
}
}
@Override
public DeviceFoldableState getCurrentFoldableState() throws DeviceNotAvailableException {
if (getIDevice() instanceof StubDevice) {
return null;
}
CommandResult result = executeShellV2Command("cmd device_state state");
Pattern deviceStatePattern =
Pattern.compile(
"Committed state: DeviceState\\{identifier=(\\d+), name='(\\S+)'"
+ "(?:, app_accessible=)?(\\S+)?"
+ "(?:, cancel_when_requester_not_on_top=)?(\\S+)?"
+ "\\}\\S*");
for (String line : result.getStdout().split("\n")) {
Matcher m = deviceStatePattern.matcher(line.trim());
if (m.matches()) {
return new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2));
}
}
return null;
}
/**
* Checks the preconditions to run a microdroid.
*
* @param protectedVm true if microdroid is intended to run on protected VM.
* @return returns true if the preconditions are satisfied, false otherwise.
*/
public boolean supportsMicrodroid(boolean protectedVm) throws Exception {
CommandResult result = executeShellV2Command("getprop ro.product.cpu.abi");
if (result.getStatus() != CommandStatus.SUCCESS) {
return false;
}
String abi = result.getStdout().trim();
if (abi.isEmpty() || (!abi.startsWith("arm64") && !abi.startsWith("x86_64"))) {
CLog.d("Unsupported ABI: " + abi);
return false;
}
if (protectedVm) {
// check if device supports protected virtual machines.
boolean pVMSupported =
getBooleanProperty("ro.boot.hypervisor.protected_vm.supported", false);
if (!pVMSupported) {
CLog.i("Device does not support protected virtual machines.");
return false;
}
} else {
// check if device supports non protected virtual machines.
boolean nonProtectedVMSupported =
getBooleanProperty("ro.boot.hypervisor.vm.supported", false);
if (!nonProtectedVMSupported) {
CLog.i("Device does not support non protected virtual machines.");
return false;
}
}
if (!doesFileExist("/apex/com.android.virt")) {
CLog.i(
"com.android.virt APEX was not pre-installed. Command Failed: 'ls"
+ " /apex/com.android.virt/bin/crosvm'");
return false;
}
return true;
}
/**
* Checks the preconditions to run a microdroid.
*
* @return returns true if the preconditions are satisfied, false otherwise.
*/
public boolean supportsMicrodroid() throws Exception {
// Micrdroid can run on protected and non-protected VMs
return supportsMicrodroid(false) || supportsMicrodroid(true);
}
/**
* Forwards contents of a file to log. To be used when testing microdroid, to forward console
* and log outputs to the host device's log.
*/
private void forwardFileToLog(String logPath, String tag) {
try (CloseableTraceScope ignored = new CloseableTraceScope("forward_to_log:" + tag)) {
String logwrapperCmd =
"logwrapper "
+ "sh "
+ "-c "
+ "\"$'tail -f -n +0 "
+ logPath
+ " | sed \\'s/^/"
+ tag
+ ": /g\\''\""; // add tags in front of lines
getRunUtil().allowInterrupt(true);
// Manually execute the adb action to avoid any kind of recovery
// since it hard to interrupt the forwarding
final String[] fullCmd = buildAdbShellCommand(logwrapperCmd, false);
AdbShellAction adbActionV2 =
new AdbShellAction(
fullCmd,
null,
null,
null,
TimeUnit.MINUTES.toMillis(MICRODROID_MAX_LIFETIME_MINUTES));
adbActionV2.run();
} catch (Exception e) {
// Consume
}
}
/**
* Starts a Microdroid TestDevice.
*
* @param builder A {@link MicrodroidBuilder} with required properties to start a microdroid.
* @return returns a ITestDevice for the microdroid, can return null.
*/
private ITestDevice startMicrodroid(MicrodroidBuilder builder)
throws DeviceNotAvailableException {
IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
if (!mStartedMicrodroids.isEmpty())
throw new IllegalStateException(
String.format(
"Microdroid with cid '%s' already exists in device. Cannot create"
+ " another one.",
mStartedMicrodroids.values().iterator().next()));
String microdroidSerial;
int vmAdbPort = -1;
try {
ServerSocket microdroidServerSocket = new ServerSocket(0);
vmAdbPort = microdroidServerSocket.getLocalPort();
microdroidServerSocket.close();
} catch (IOException e) {
throw new DeviceRuntimeException(
"Unable to get an unused port for Microdroid.",
e,
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
microdroidSerial = "localhost:" + vmAdbPort;
// disconnect from microdroid
getRunUtil().runTimedCmd(10000, deviceManager.getAdbPath(), "disconnect", microdroidSerial);
// remove any leftover files under test root
executeShellV2Command("rm -rf " + TEST_ROOT + "*");
CommandResult result = executeShellV2Command("mkdir -p " + TEST_ROOT);
if (result.getStatus() != CommandStatus.SUCCESS) {
throw new DeviceRuntimeException(
"mkdir -p " + TEST_ROOT + " has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
for (File localFile : builder.mBootFiles.keySet()) {
String remoteFileName = builder.mBootFiles.get(localFile);
pushFile(localFile, TEST_ROOT + remoteFileName);
}
// Push the apk file to the test directory
if (builder.mApkFile != null) {
pushFile(builder.mApkFile, TEST_ROOT + builder.mApkFile.getName());
builder.mApkPath = TEST_ROOT + builder.mApkFile.getName();
} else if (builder.mApkPath == null) {
// if both apkFile and apkPath is null, we can not start a microdroid device
throw new IllegalArgumentException(
"apkFile and apkPath is both null. Can not start microdroid.");
}
// This file is not what we provide. It will be created by the vm tool.
final String outApkIdsigPath =
TEST_ROOT
+ (builder.mApkFile != null ? builder.mApkFile.getName() : "NULL")
+ ".idsig";
final String instanceImg = TEST_ROOT + INSTANCE_IMG;
final String consolePath = TEST_ROOT + "console.txt";
final String logPath = TEST_ROOT + "log.txt";
final String debugFlag =
Strings.isNullOrEmpty(builder.mDebugLevel) ? "" : "--debug " + builder.mDebugLevel;
final String cpuFlag = builder.mNumCpus == null ? "" : "--cpus " + builder.mNumCpus;
final String cpuAffinityFlag =
Strings.isNullOrEmpty(builder.mCpuAffinity)
? ""
: "--cpu-affinity " + builder.mCpuAffinity;
final String cpuTopologyFlag =
Strings.isNullOrEmpty(builder.mCpuTopology)
? ""
: "--cpu-topology " + builder.mCpuTopology;
List<String> args =
new ArrayList<>(
Arrays.asList(
deviceManager.getAdbPath(),
"-s",
getSerialNumber(),
"shell",
VIRT_APEX + "bin/vm",
"run-app",
"--console " + consolePath,
"--log " + logPath,
"--mem " + builder.mMemoryMib,
debugFlag,
cpuFlag,
cpuAffinityFlag,
cpuTopologyFlag,
builder.mApkPath,
outApkIdsigPath,
instanceImg,
"--config-path",
builder.mConfigPath));
if (builder.mProtectedVm) {
args.add("--protected");
}
for (String path : builder.mExtraIdsigPaths) {
args.add("--extra-idsig");
args.add(path);
}
// Run the VM
String cid;
Process process;
try {
PipedInputStream pipe = new PipedInputStream();
process = getRunUtil().runCmdInBackground(args, new PipedOutputStream(pipe));
BufferedReader stdout = new BufferedReader(new InputStreamReader(pipe));
// Retrieve the CID from the vm tool output
Pattern pattern = Pattern.compile("with CID (\\d+)");
while ((cid = stdout.readLine()) != null) {
Matcher matcher = pattern.matcher(cid);
if (matcher.find()) {
cid = matcher.group(1);
break;
}
}
if (cid == null) {
throw new DeviceRuntimeException(
"Failed to find the CID of the VM",
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
} catch (IOException ex) {
throw new DeviceRuntimeException(
"IOException trying to start a VM",
ex,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
// Redirect log.txt to logd using logwrapper
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(
() -> {
forwardFileToLog(consolePath, "MicrodroidConsole");
});
executor.execute(
() -> {
forwardFileToLog(logPath, "MicrodroidLog");
});
DeviceSelectionOptions microSelection = new DeviceSelectionOptions();
microSelection.setSerial(microdroidSerial);
microSelection.setBaseDeviceTypeRequested(BaseDeviceType.NATIVE_DEVICE);
NativeDevice microdroid = (NativeDevice) deviceManager.allocateDevice(microSelection);
if (microdroid == null) {
process.destroy();
try {
process.waitFor();
executor.shutdownNow();
executor.awaitTermination(2L, TimeUnit.MINUTES);
} catch (InterruptedException ex) {
}
throw new DeviceRuntimeException(
"Unable to force allocate the microdroid device",
InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR);
}
// microdroid can be slow to become unavailable after root. (b/259208275)
microdroid.getOptions().setAdbRootUnavailableTimeout(4 * 1000);
builder.mTestDeviceOptions.put("enable-device-connection", "true");
builder.mTestDeviceOptions.put(
TestDeviceOptions.INSTANCE_TYPE_OPTION, getOptions().getInstanceType().toString());
microdroid.setTestDeviceOptions(builder.mTestDeviceOptions);
((IManagedTestDevice) microdroid).setIDevice(new RemoteAvdIDevice(microdroidSerial));
adbConnectToMicrodroid(cid, microdroidSerial, vmAdbPort, builder.mAdbConnectTimeoutMs);
microdroid.setMicrodroidProcess(process);
try {
// TODO: Pass the build info
microdroid.initializeConnection(null, null);
} catch (DeviceNotAvailableException | TargetSetupError e) {
CLog.e(e);
}
MicrodroidTracker tracker = new MicrodroidTracker();
tracker.executor = executor;
mStartedMicrodroids.put(process, tracker);
return microdroid;
}
/**
* Establish an adb connection to microdroid by letting Android forward the connection to
* microdroid. Wait until the connection is established and microdroid is booted.
*/
private void adbConnectToMicrodroid(
String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs) {
MicrodroidHelper microdroidHelper = new MicrodroidHelper();
IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
long start = System.currentTimeMillis();
long timeoutMillis = adbConnectTimeoutMs;
long elapsed = 0;
final String serial = getSerialNumber();
final String from = "tcp:" + vmAdbPort;
final String to = "vsock:" + cid + ":5555";
getRunUtil()
.runTimedCmd(10000, deviceManager.getAdbPath(), "-s", serial, "forward", from, to);
boolean disconnected = true;
while (disconnected) {
elapsed = System.currentTimeMillis() - start;
timeoutMillis -= elapsed;
start = System.currentTimeMillis();
CommandResult result =
getRunUtil()
.runTimedCmd(
timeoutMillis,
deviceManager.getAdbPath(),
"connect",
microdroidSerial);
if (result.getStatus() != CommandStatus.SUCCESS) {
throw new DeviceRuntimeException(
deviceManager.getAdbPath()
+ " connect "
+ microdroidSerial
+ " has failed: "
+ result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
disconnected =
result.getStdout().trim().equals("failed to connect to " + microdroidSerial);
if (disconnected) {
// adb demands us to disconnect if the prior connection was a failure.
// b/194375443: this somtimes fails, thus 'try*'.
getRunUtil()
.runTimedCmd(
10000, deviceManager.getAdbPath(), "disconnect", microdroidSerial);
}
}
elapsed = System.currentTimeMillis() - start;
timeoutMillis -= elapsed;
getRunUtil()
.runTimedCmd(
timeoutMillis,
deviceManager.getAdbPath(),
"-s",
microdroidSerial,
"wait-for-device");
boolean dataAvailable = false;
while (!dataAvailable && timeoutMillis >= 0) {
elapsed = System.currentTimeMillis() - start;
timeoutMillis -= elapsed;
start = System.currentTimeMillis();
final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi";
dataAvailable =
microdroidHelper.runOnMicrodroid(microdroidSerial, checkCmd).equals("1");
}
// Check if it actually booted by reading a sysprop.
if (!microdroidHelper
.runOnMicrodroid(microdroidSerial, "getprop", "ro.hardware")
.equals("microdroid")) {
throw new DeviceRuntimeException(
String.format("Device '%s' was not booted.", microdroidSerial),
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
}
/**
* Shuts down the microdroid device, if one exist.
*
* @throws DeviceNotAvailableException
*/
public void shutdownMicrodroid(@Nonnull ITestDevice microdroidDevice)
throws DeviceNotAvailableException {
Process process = ((NativeDevice) microdroidDevice).getMicrodroidProcess();
if (process == null) {
throw new IllegalArgumentException("Process is null. TestDevice is not a Microdroid. ");
}
if (!mStartedMicrodroids.containsKey(process)) {
throw new IllegalArgumentException(
"Microdroid device was not started in this TestDevice.");
}
process.destroy();
try {
process.waitFor();
} catch (InterruptedException ex) {
}
// disconnect from microdroid
getRunUtil()
.runTimedCmd(
10000,
GlobalConfiguration.getDeviceManagerInstance().getAdbPath(),
"disconnect",
microdroidDevice.getSerialNumber());
GlobalConfiguration.getDeviceManagerInstance()
.freeDevice(microdroidDevice, FreeDeviceState.AVAILABLE);
MicrodroidTracker tracker = mStartedMicrodroids.remove(process);
getRunUtil().allowInterrupt(true);
try {
tracker.executor.shutdownNow();
tracker.executor.awaitTermination(1L, TimeUnit.MINUTES);
} catch (InterruptedException e) {
CLog.e(e);
}
}
// TODO (b/274941025): remove when shell commands using this method are merged in AOSP
private boolean executeShellV2CommandThatReturnsBooleanSafe(
String cmdFormat, Object... cmdArgs) {
try {
return executeShellV2CommandThatReturnsBoolean(cmdFormat, cmdArgs);
} catch (Exception e) {
CLog.e(e);
return false;
}
}
private boolean executeShellV2CommandThatReturnsBoolean(String cmdFormat, Object... cmdArgs)
throws DeviceNotAvailableException {
String cmd = String.format(cmdFormat, cmdArgs);
CommandResult res = executeShellV2Command(cmd);
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
throw new DeviceRuntimeException(
"Command '" + cmd + "' failed: " + res,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
String output = res.getStdout();
switch (output.trim().toLowerCase()) {
case "true":
return true;
case "false":
return false;
default:
throw new DeviceRuntimeException(
"Non-boolean result for '" + cmd + "': " + output,
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
}
/** A builder used to create a Microdroid TestDevice. */
public static class MicrodroidBuilder {
private File mApkFile;
private String mApkPath;
private String mConfigPath;
private String mDebugLevel;
private int mMemoryMib;
private Integer mNumCpus;
private String mCpuAffinity;
private String mCpuTopology;
private List<String> mExtraIdsigPaths;
private boolean mProtectedVm;
private Map<String, String> mTestDeviceOptions;
private Map<File, String> mBootFiles;
private long mAdbConnectTimeoutMs;
/** Creates a builder for the given APK/apkPath and the payload config file in APK. */
private MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath) {
mApkFile = apkFile;
mApkPath = apkPath;
mConfigPath = configPath;
mDebugLevel = null;
mMemoryMib = 0;
mNumCpus = null;
mCpuAffinity = null;
mExtraIdsigPaths = new ArrayList<>();
mProtectedVm = false; // Vm is unprotected by default.
mTestDeviceOptions = new LinkedHashMap<>();
mBootFiles = new LinkedHashMap<>();
mAdbConnectTimeoutMs = MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
}
/** Creates a Microdroid builder for the given APK and the payload config file in APK. */
public static MicrodroidBuilder fromFile(
@Nonnull File apkFile, @Nonnull String configPath) {
return new MicrodroidBuilder(apkFile, null, configPath);
}
/**
* Creates a Microdroid builder for the given apkPath and the payload config file in APK.
*/
public static MicrodroidBuilder fromDevicePath(
@Nonnull String apkPath, @Nonnull String configPath) {
return new MicrodroidBuilder(null, apkPath, configPath);
}
/**
* Sets the debug level.
*
* <p>Supported values: "none" and "full". Android T also supports "app_only".
*/
public MicrodroidBuilder debugLevel(String debugLevel) {
mDebugLevel = debugLevel;
return this;
}
/**
* Sets the amount of RAM to give the VM. If this is zero or negative then the default will
* be used.
*/
public MicrodroidBuilder memoryMib(int memoryMib) {
mMemoryMib = memoryMib;
return this;
}
/**
* Sets the number of vCPUs in the VM. Defaults to 1.
*
* <p>Only supported in Android T.
*/
public MicrodroidBuilder numCpus(int num) {
mNumCpus = num;
return this;
}
/**
* Sets on which host CPUs the vCPUs can run. The format is a comma-separated list of CPUs
* or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. Or
* this can be a colon-separated list of assignments of vCPU to host CPU assignments. e.g.
* "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on.
*
* <p>Only supported in Android T.
*/
public MicrodroidBuilder cpuAffinity(String affinity) {
mCpuAffinity = affinity;
return this;
}
/** Sets the CPU topology configuration. Supported values: "one_cpu" and "match_host". */
public MicrodroidBuilder cpuTopology(String cpuTopology) {
mCpuTopology = cpuTopology;
return this;
}
/** Sets whether the VM will be protected or not. */
public MicrodroidBuilder protectedVm(boolean isProtectedVm) {
mProtectedVm = isProtectedVm;
return this;
}
/** Adds extra idsig file to the list. */
public MicrodroidBuilder addExtraIdsigPath(String extraIdsigPath) {
if (!Strings.isNullOrEmpty(extraIdsigPath)) {
mExtraIdsigPaths.add(extraIdsigPath);
}
return this;
}
/**
* Sets a {@link TestDeviceOptions} for the microdroid TestDevice.
*
* @param optionName The name of the TestDeviceOption to set
* @param valueText The value
* @return the microdroid builder.
*/
public MicrodroidBuilder addTestDeviceOption(String optionName, String valueText) {
mTestDeviceOptions.put(optionName, valueText);
return this;
}
/**
* Adds a file for booting to be pushed to {@link #TEST_ROOT}.
*
* <p>Use this method if an file is required for booting microdroid. Otherwise use {@link
* TestDevice#pushFile}.
*
* @param localFile The local file on the host
* @param remoteFileName The remote file name on the device
* @return the microdroid builder.
*/
public MicrodroidBuilder addBootFile(File localFile, String remoteFileName) {
mBootFiles.put(localFile, remoteFileName);
return this;
}
/**
* Sets the timeout for adb connect to microdroid TestDevice in millis.
*
* @param timeoutMs The timeout in millis
*/
public MicrodroidBuilder setAdbConnectTimeoutMs(long timeoutMs) {
mAdbConnectTimeoutMs = timeoutMs;
return this;
}
/** Starts a Micrdroid TestDevice on the given TestDevice. */
public ITestDevice build(@Nonnull TestDevice device) throws DeviceNotAvailableException {
if (mNumCpus != null) {
if (device.getApiLevel() != 33) {
throw new IllegalStateException(
"Setting number of CPUs only supported with API level 33");
}
if (mNumCpus < 1) {
throw new IllegalArgumentException("Number of vCPUs can not be less than 1.");
}
}
if (!Strings.isNullOrEmpty(mCpuTopology)) {
device.checkApiLevelAgainstNextRelease("vm-cpu-topology", 34);
}
if (mCpuAffinity != null) {
if (device.getApiLevel() != 33) {
throw new IllegalStateException(
"Setting CPU affinity only supported with API level 33");
}
if (!Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity)
&& !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) {
throw new IllegalArgumentException(
"CPU affinity [" + mCpuAffinity + "]" + " is invalid");
}
}
return device.startMicrodroid(this);
}
}
private String handleInstallationError(InstallException e) {
String message = e.getMessage();
if (message == null) {
message =
String.format(
"InstallException during package installation. " + "cause: %s",
StreamUtil.getStackTrace(e));
}
return message;
}
private String handleInstallReceiver(InstallReceiver receiver, File packageFile) {
if (receiver.isSuccessfullyCompleted()) {
return null;
}
if (receiver.getErrorMessage() == null) {
return String.format("Installation of %s timed out", packageFile.getAbsolutePath());
}
String error = receiver.getErrorMessage();
if (error.contains("cmd: Failure calling service package")
|| error.contains("Can't find service: package")) {
String message =
String.format(
"Failed to install '%s'. Device might have"
+ " crashed, it returned: %s",
packageFile.getName(), error);
throw new DeviceRuntimeException(message, DeviceErrorIdentifier.DEVICE_CRASHED);
}
return error;
}
}