blob: 1e8d823d3ad1e41c14b73fa5517a730929d2dce9 [file] [log] [blame]
/*
* Copyright (C) 2016 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.FileListingService;
import com.android.ddmlib.FileListingService.FileEntry;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.SyncException.SyncError;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.command.remote.DeviceDescriptor;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.IWifiHelper.WifiConnectionResult;
import com.android.tradefed.device.cloud.GceAvdInfo;
import com.android.tradefed.device.connection.AbstractConnection;
import com.android.tradefed.device.connection.DefaultConnection;
import com.android.tradefed.device.connection.DefaultConnection.ConnectionBuilder;
import com.android.tradefed.device.contentprovider.ContentProviderHandler;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.host.IHostOptions;
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;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestLifeCycleReceiver;
import com.android.tradefed.result.ITestLoggerReceiver;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.result.StubTestRunListener;
import com.android.tradefed.result.ddmlib.TestRunToTestInvocationForwarder;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain;
import com.android.tradefed.util.ArrayUtil;
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.IRunUtil;
import com.android.tradefed.util.KeyguardControllerState;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.ProcessInfo;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StringEscapeUtils;
import com.android.tradefed.util.SystemUtil;
import com.android.tradefed.util.TimeUtil;
import com.android.tradefed.util.ZipUtil2;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.FormatMethod;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/** Default implementation of a {@link ITestDevice} Non-full stack android devices. */
public class NativeDevice
implements IManagedTestDevice, IConfigurationReceiver, ITestLoggerReceiver {
protected static final String SD_CARD = "/sdcard/";
protected static final String STORAGE_EMULATED = "/storage/emulated/";
/** On-device path where we expect ANRs to be generated. */
private static final String ANRS_PATH = "/data/anr";
/**
* Allow up to 2 minutes to receives the full logcat dump.
*/
private static final int LOGCAT_DUMP_TIMEOUT = 2 * 60 * 1000;
/** the default number of command retry attempts to perform */
protected static final int MAX_RETRY_ATTEMPTS = 2;
/** Value returned for any invalid/not found user id: UserHandle defined the -10000 value */
public static final int INVALID_USER_ID = -10000;
/** regex to match input dispatch readiness line **/
static final Pattern INPUT_DISPATCH_STATE_REGEX =
Pattern.compile("DispatchEnabled:\\s?([01])");
/** regex to match build signing key type */
private static final Pattern KEYS_PATTERN = Pattern.compile("^.*-keys$");
private static final Pattern DF_PATTERN =
Pattern.compile(
// Fs 1K-blks Used Available Use% Mounted on
"^/(\\S+)\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+\\d+%\\s+/\\S*$", Pattern.MULTILINE);
protected static final long MAX_HOST_DEVICE_TIME_OFFSET = 5 * 1000;
/** The password for encrypting and decrypting the device. */
private static final String ENCRYPTION_PASSWORD = "android";
/** The maximum system_server start delay in seconds after device boot up */
private static final int MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC = 25;
/** The time in ms to wait before starting logcat for a device */
private int mLogStartDelay = 5*1000;
/** The time in ms to wait for a device to become unavailable. Should usually be short */
private static final int DEFAULT_UNAVAILABLE_TIMEOUT = 20 * 1000;
private static final String SIM_STATE_PROP = "gsm.sim.state";
private static final String SIM_OPERATOR_PROP = "gsm.operator.alpha";
static final String MAC_ADDRESS_PATTERN = "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}";
static final String MAC_ADDRESS_COMMAND = "su root cat /sys/class/net/wlan0/address";
static final String ETHERNET_MAC_ADDRESS_COMMAND = "cat /sys/class/net/eth0/address";
static final int ETHER_ADDR_LEN = 6;
/** The network monitoring interval in ms. */
private static final int NETWORK_MONITOR_INTERVAL = 10 * 1000;
/** Wifi reconnect check interval in ms. */
private static final int WIFI_RECONNECT_CHECK_INTERVAL = 1 * 1000;
/** Wifi reconnect timeout in ms. */
private static final int WIFI_RECONNECT_TIMEOUT = 60 * 1000;
/** Pattern to find an executable file. */
private static final Pattern EXE_FILE = Pattern.compile("^[-l]r.x.+");
/** Path of the device containing the tombstones */
private static final String TOMBSTONE_PATH = "/data/tombstones/";
private static final long PROPERTY_GET_TIMEOUT = 45 * 1000L;
public static final String DEBUGFS_PATH = "/sys/kernel/debug";
private static final String CHECK_DEBUGFS_MNT_COMMAND =
String.format("mountpoint -q %s", DEBUGFS_PATH);
private static final String MOUNT_DEBUGFS_COMMAND =
String.format("mount -t debugfs debugfs %s", DEBUGFS_PATH);
private static final String UNMOUNT_DEBUGFS_COMMAND = String.format("umount %s", DEBUGFS_PATH);
/** Version number for a current development build */
private static final int CUR_DEVELOPMENT_VERSION = 10000;
/** The time in ms to wait for a 'long' command to complete. */
private long mLongCmdTimeout = 25 * 60 * 1000L;
/**
* The delimiter that separates the actual shell output and the exit status.
*
* <p>Used to determine the exit status of the command run by adb shell for devices that do not
* support shell_v2.
*/
private static final String EXIT_STATUS_DELIMITER = "x";
private IConfiguration mConfiguration;
private IDevice mIDevice;
private IDeviceRecovery mRecovery = new WaitDeviceRecovery();
protected final IDeviceStateMonitor mStateMonitor;
private TestDeviceState mState = TestDeviceState.ONLINE;
private final ReentrantLock mFastbootLock = new ReentrantLock();
private LogcatReceiver mLogcatReceiver;
private boolean mFastbootEnabled = true;
private String mFastbootPath = "fastboot";
protected TestDeviceOptions mOptions = new TestDeviceOptions();
private Process mEmulatorProcess;
private SizeLimitedOutputStream mEmulatorOutput;
private Clock mClock = Clock.systemUTC();
private RecoveryMode mRecoveryMode = RecoveryMode.AVAILABLE;
private Boolean mIsEncryptionSupported = null;
private ReentrantLock mAllocationStateLock = new ReentrantLock(true /*fair*/);
@GuardedBy("mAllocationStateLock")
private DeviceAllocationState mAllocationState = DeviceAllocationState.Unknown;
private IDeviceMonitor mAllocationMonitor = null;
private String mLastConnectedWifiSsid = null;
private String mLastConnectedWifiPsk = null;
private boolean mNetworkMonitorEnabled = false;
private ContentProviderHandler mContentProvider = null;
private boolean mShouldSkipContentProviderSetup = false;
/** Keep track of the last time Tradefed itself triggered a reboot. */
private long mLastTradefedRebootTime = 0L;
private File mExecuteShellCommandLogs = null;
private DeviceDescriptor mCachedDeviceDescriptor = null;
private final Object mCacheLock = new Object();
private String mFastbootSerialNumber = null;
private File mUnpackedFastbootDir = null;
// Connection for the device.
private AbstractConnection mConnection;
private GceAvdInfo mConnectionAvd;
private ITestLogger mTestLogger;
private List<IDeviceActionReceiver> mDeviceActionReceivers = new LinkedList<>();
/**
* Whether callback for reboot is currently executing or not. Use this flag to avoid dead loop
* scenarios like calling reboot inside a callback happening for reboot.
*/
private boolean inRebootCallback = false;
/** If the device is a Microdroid, this refers to the VM process. Otherwise, it is null. */
private Process mMicrodroidProcess = null;
private final LoadingCache<String, String> mPropertiesCache;
// If we increase the number of props, then increase the cache size of mPropertiesCache.
private final Set<String> propsToPrefetch =
ImmutableSet.of("ro.build.version.sdk", "ro.build.version.codename", "ro.build.id");
// Avoid caching any properties in those namespace
private static final Set<String> NEVER_CACHE_PROPERTIES =
ImmutableSet.of("vendor.debug", "ro.boot");
/** Interface for a generic device communication attempt. */
abstract interface DeviceAction {
/**
* Execute the device operation.
*
* @return <code>true</code> if operation is performed successfully, <code>false</code>
* otherwise
* @throws IOException, TimeoutException, AdbCommandRejectedException,
* ShellCommandUnresponsiveException, InstallException, SyncException if operation
* terminated abnormally
*/
public boolean run()
throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException,
DeviceNotAvailableException;
}
/**
* A {@link DeviceAction} for running a OS 'adb ....' command.
*/
protected class AdbAction implements DeviceAction {
/** the output from the command */
String mOutput = null;
private String[] mCmd;
private long mTimeout;
private boolean mIsShellCommand;
private Map<String, String> mEnvMap;
AdbAction(long timeout, String[] cmd, boolean isShell, Map<String, String> envMap) {
mTimeout = timeout;
mCmd = cmd;
mIsShellCommand = isShell;
mEnvMap = envMap;
}
private void logExceptionAndOutput(CommandResult result) {
CLog.w("Command exited with status: %s", result.getStatus().toString());
CLog.w("Command stdout:\n%s\n", result.getStdout());
CLog.w("Command stderr:\n%s\n", result.getStderr());
}
@Override
public boolean run() throws TimeoutException, IOException {
IRunUtil runUtil = getRunUtil();
if (!mEnvMap.isEmpty()) {
runUtil = createRunUtil();
}
for (String key : mEnvMap.keySet()) {
runUtil.setEnvVariable(key, mEnvMap.get(key));
}
CommandResult result = runUtil.runTimedCmd(mTimeout, mCmd);
// TODO: how to determine device not present with command failing for other reasons
if (result.getStatus() == CommandStatus.EXCEPTION) {
logExceptionAndOutput(result);
throw new IOException("CommandStatus was EXCEPTION, details in host log");
} else if (result.getStatus() == CommandStatus.TIMED_OUT) {
logExceptionAndOutput(result);
throw new TimeoutException("CommandStatus was TIMED_OUT, details in host log");
} else if (result.getStatus() == CommandStatus.FAILED) {
logExceptionAndOutput(result);
if (mIsShellCommand) {
// Interpret as communication failure for shell commands
throw new IOException("CommandStatus was FAILED, details in host log");
} else {
mOutput = result.getStdout();
return false;
}
}
mOutput = result.getStdout();
return true;
}
}
protected class AdbShellAction implements DeviceAction {
/** the output from the command */
CommandResult mResult = null;
private String[] mCmd;
private long mTimeout;
private File mPipeAsInput; // Used in pushFile, uses local file as input to "content write"
private OutputStream mPipeToOutput; // Used in pullFile, to pipe content from "content read"
private OutputStream mPipeToError;
AdbShellAction(
String[] cmd,
File pipeAsInput,
OutputStream pipeToOutput,
OutputStream pipeToError,
long timeout) {
mCmd = cmd;
mPipeAsInput = pipeAsInput;
mPipeToOutput = pipeToOutput;
mPipeToError = pipeToError;
mTimeout = timeout;
}
@Override
public boolean run() throws TimeoutException, IOException {
if (mPipeAsInput != null) {
mResult = getRunUtil().runTimedCmdWithInputRedirect(mTimeout, mPipeAsInput, mCmd);
} else {
mResult = getRunUtil().runTimedCmd(mTimeout, mPipeToOutput, mPipeToError, mCmd);
}
if (mResult.getStatus() == CommandStatus.EXCEPTION) {
throw new IOException(mResult.getStderr());
} else if (mResult.getStatus() == CommandStatus.TIMED_OUT) {
throw new TimeoutException(mResult.getStderr());
}
String stdErr = mResult.getStderr();
if (stdErr != null) {
stdErr = stdErr.trim();
if (stdErr.contains("device offline")) {
throw new IOException(stdErr);
}
}
// If it's not some issue with running the adb command, then we return the CommandResult
// which will contain all the infos.
return true;
}
}
/** {@link DeviceAction} for rebooting a device. */
protected class RebootDeviceAction implements DeviceAction {
private final RebootMode mRebootMode;
@Nullable private final String mReason;
RebootDeviceAction(RebootMode rebootMode, @Nullable String reason) {
mRebootMode = rebootMode;
mReason = reason;
}
public boolean isFastbootOrBootloader() {
return mRebootMode == RebootMode.REBOOT_INTO_BOOTLOADER
|| mRebootMode == RebootMode.REBOOT_INTO_FASTBOOTD;
}
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
DeviceNotAvailableException {
// Notify of reboot started for all modes
notifyRebootStarted();
getIDevice().reboot(mRebootMode.formatRebootCommand(mReason));
return true;
}
}
/**
* Creates a {@link TestDevice}.
*
* @param device the associated {@link IDevice}
* @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
* @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
* Can be null
*/
public NativeDevice(IDevice device, IDeviceStateMonitor stateMonitor,
IDeviceMonitor allocationMonitor) {
throwIfNull(device);
throwIfNull(stateMonitor);
mIDevice = device;
mStateMonitor = stateMonitor;
mAllocationMonitor = allocationMonitor;
// Keep a short timeout to expire key in case of large state changes
// such as flashing
mPropertiesCache =
CacheBuilder.newBuilder()
.maximumSize(50)
.expireAfterAccess(2, TimeUnit.MINUTES)
.build(
new CacheLoader<String, String>() {
@Override
public String load(String key) {
throw new IllegalStateException("Should never be called");
}
});
}
/** Get the {@link RunUtil} instance to use. */
@VisibleForTesting
protected IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
protected IRunUtil createRunUtil() {
return new RunUtil();
}
/** Set the Clock instance to use. */
@VisibleForTesting
protected void setClock(Clock clock) {
mClock = clock;
}
/**
* {@inheritDoc}
*/
@Override
public void setOptions(TestDeviceOptions options) {
throwIfNull(options);
mOptions = options;
if (mOptions.getFastbootBinary() != null) {
// Setup fastboot- if it's zipped, unzip it
// TODO: Dedup the logic with DeviceManager
if (".zip".equals(FileUtil.getExtension(mOptions.getFastbootBinary().getName()))) {
// Unzip the fastboot files
try {
mUnpackedFastbootDir =
ZipUtil2.extractZipToTemp(
mOptions.getFastbootBinary(), "unpacked-fastboot");
File unpackedFastboot = FileUtil.findFile(mUnpackedFastbootDir, "fastboot");
if (unpackedFastboot == null) {
throw new HarnessRuntimeException(
String.format(
"device-fastboot-binary was set, but didn't contain a"
+ " fastboot binary."),
InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
}
setFastbootPath(unpackedFastboot.getAbsolutePath());
} catch (IOException e) {
CLog.e("Failed to unpacked zipped fastboot.");
CLog.e(e);
FileUtil.recursiveDelete(mUnpackedFastbootDir);
mUnpackedFastbootDir = null;
}
} else {
setFastbootPath(mOptions.getFastbootBinary().getAbsolutePath());
}
}
mStateMonitor.setDefaultOnlineTimeout(options.getOnlineTimeout());
mStateMonitor.setDefaultAvailableTimeout(options.getAvailableTimeout());
}
/**
* Sets the max size of a tmp logcat file.
*
* @param size max byte size of tmp file
*/
void setTmpLogcatSize(long size) {
mOptions.setMaxLogcatDataSize(size);
}
/**
* Sets the time in ms to wait before starting logcat capture for a online device.
*
* @param delay the delay in ms
*/
public void setLogStartDelay(int delay) {
mLogStartDelay = delay;
}
/** {@inheritDoc} */
@Override
public void setConfiguration(IConfiguration configuration) {
mConfiguration = configuration;
}
/**
* {@inheritDoc}
*/
@Override
public IDevice getIDevice() {
synchronized (mIDevice) {
return mIDevice;
}
}
/**
* {@inheritDoc}
*/
@Override
public void setIDevice(IDevice newDevice) {
throwIfNull(newDevice);
IDevice currentDevice = mIDevice;
if (!getIDevice().equals(newDevice)) {
synchronized (currentDevice) {
mIDevice = newDevice;
}
mStateMonitor.setIDevice(mIDevice);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getSerialNumber() {
return getIDevice().getSerialNumber();
}
/**
* Fetch a device property, from the ddmlib cache by default, and falling back to either `adb
* shell getprop` or `fastboot getvar` depending on whether the device is in Fastboot or not.
*
* @param propName The name of the device property as returned by `adb shell getprop`
* @param fastbootVar The name of the equivalent fastboot variable to query. if {@code null},
* fastboot query will not be attempted
* @param description A simple description of the variable. First letter should be capitalized.
* @return A string, possibly {@code null} or empty, containing the value of the given property
*/
protected String internalGetProperty(String propName, String fastbootVar, String description)
throws DeviceNotAvailableException, UnsupportedOperationException {
if (isStateBootloaderOrFastbootd() && fastbootVar != null) {
CLog.i("Device %s is in fastboot mode, re-querying with '%s' for %s", getSerialNumber(),
fastbootVar, description);
return getFastbootVariable(fastbootVar);
}
String propValue = getProperty(propName);
if (propValue != null) {
return propValue;
} else {
CLog.d(
"property collection '%s' for device %s is null.",
description, getSerialNumber());
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public String getProperty(final String name) throws DeviceNotAvailableException {
return getPropertyWithRecovery(name, false);
}
/** Version of getProperty that allows to check device status and trigger recovery if needed. */
private String getPropertyWithRecovery(final String name, boolean recovery)
throws DeviceNotAvailableException {
if (getIDevice() instanceof StubDevice) {
return null;
}
String property = mPropertiesCache.getIfPresent(name);
if (property != null) {
CLog.d("Using property %s=%s from cache.", name, property);
return property;
}
try (CloseableTraceScope getProp = new CloseableTraceScope("get_property:" + name)) {
TestDeviceState state = getDeviceState();
if (!TestDeviceState.ONLINE.equals(state) && !TestDeviceState.RECOVERY.equals(state)) {
if (recovery) {
// Only query property for online device so trigger recovery before getting
// property.
recoverDevice();
} else {
if (mStateMonitor.waitForDeviceOnline() == null) {
CLog.w(
"Waited for device %s to be online but it is in state '%s', cannot "
+ "get property %s.",
getSerialNumber(), getDeviceState(), name);
CLog.w(
new RuntimeException(
"This is not an actual exception but to help"
+ " debugging. If this happens deterministically, "
+ " it means the caller has wrong assumption of "
+ " device state and is wasting time in waiting."));
return null;
}
}
}
String cmd = String.format("getprop %s", name);
CommandResult result =
executeShellV2Command(cmd, PROPERTY_GET_TIMEOUT, TimeUnit.MILLISECONDS, 0);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.e(
"Failed to run '%s' returning null. stdout: %s\nstderr: %s\nexit code: %s",
cmd, result.getStdout(), result.getStderr(), result.getExitCode());
if (result.getStderr().contains("device offline")) {
if (recovery) {
recoverDevice();
return getPropertyWithRecovery(name, false);
}
throw new DeviceNotAvailableException(
String.format("Device went offline when querying property: %s", name),
getSerialNumber(),
DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
}
return null;
}
if (result.getStdout() == null || result.getStdout().trim().isEmpty()) {
return null;
}
property = result.getStdout().trim();
if (property != null) {
if (!NEVER_CACHE_PROPERTIES.stream().anyMatch(p -> name.startsWith(p))) {
// Manage the cache manually to maintain exception handling
mPropertiesCache.put(name, property);
}
}
return property;
}
}
/**
* Micro optimization (about 400 millis) by prefetching all props we need rather than call 'adb
* getprop' for each one. i.e. It is just as fast to fetch all properties as it is to fetch one.
* Things like device.getApiLevel(), checkApiLevelAgainstNextRelease and getBuildAlias all call
* `adb getprop` under the hood. We fetch them in one call and call NativeDevice.setProperty.
* Even if we don't do this, NativeDevice will itself call setProperty and cache the result for
* future calls. We are just doing it slightly earlier. If the device is in recovery or there
* are other errors fetching the props, we just ignore them.
*/
public void batchPrefetchStartupBuildProps() {
String cmd = "getprop";
try (CloseableTraceScope ignored = new CloseableTraceScope("batchPrefetchProp")) {
// Skip refetching if we already have the props by counting the ones in the cache
// that we need to fetch.
int propsAlreadyPresent = 0;
for (String propName : propsToPrefetch) {
if (mPropertiesCache.getIfPresent(propName) != null) {
propsAlreadyPresent++;
} else {
break;
}
}
if (propsAlreadyPresent == propsToPrefetch.size()) {
return;
}
try {
CommandResult result =
executeShellV2Command(cmd, PROPERTY_GET_TIMEOUT, TimeUnit.MILLISECONDS, 0);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.w(
"Failed to run '%s' returning null. stdout: %s\n"
+ "stderr: %s\n"
+ "exit code: %s",
cmd, result.getStdout(), result.getStderr(), result.getExitCode());
if (result.getStdout() == null || result.getStdout().trim().isEmpty()) {
return;
}
}
for (String line : result.getStdout().split("\n")) {
String[] parts = line.trim().split("]: \\[");
if (parts.length != 2) {
continue;
}
String propName = parts[0].substring(1).trim();
String propValue = parts[1].substring(0, parts[1].length() - 1).trim();
if (propValue != null) {
if (propsToPrefetch.contains(propName)) {
mPropertiesCache.put(propName, propValue);
}
}
}
} catch (DeviceNotAvailableException e) {
// okay to ignore, the real get property will deal with it.
}
}
}
/** {@inheritDoc} */
@Override
public long getIntProperty(String name, long defaultValue) throws DeviceNotAvailableException {
String value = getProperty(name);
if (value == null) {
return defaultValue;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
private static final List<String> TRUE_VALUES = Arrays.asList("1", "y", "yes", "on", "true");
private static final List<String> FALSE_VALUES = Arrays.asList("0", "n", "no", "off", "false");
/** {@inheritDoc} */
@Override
public boolean getBooleanProperty(String name, boolean defaultValue)
throws DeviceNotAvailableException {
String value = getProperty(name);
if (value == null) {
return defaultValue;
}
if (TRUE_VALUES.contains(value)) {
return true;
}
if (FALSE_VALUES.contains(value)) {
return false;
}
return defaultValue;
}
/** {@inheritDoc} */
@Override
public boolean setProperty(String propKey, String propValue)
throws DeviceNotAvailableException {
if (propKey == null || propValue == null) {
throw new IllegalArgumentException("set property key or value cannot be null.");
}
String setPropCmd = String.format("\"setprop %s '%s'\"", propKey, propValue);
CommandResult result = executeShellV2Command(setPropCmd);
if (CommandStatus.SUCCESS.equals(result.getStatus())) {
mPropertiesCache.invalidate(propKey);
return true;
}
CLog.e(
"Something went wrong went setting property %s (command: %s): %s",
propKey, setPropCmd, result.getStderr());
return false;
}
/**
* {@inheritDoc}
*/
@Override
public String getBootloaderVersion() throws UnsupportedOperationException,
DeviceNotAvailableException {
return internalGetProperty("ro.bootloader", "version-bootloader", "Bootloader");
}
@Override
public String getBasebandVersion() throws DeviceNotAvailableException {
return internalGetProperty("gsm.version.baseband", "version-baseband", "Baseband");
}
/**
* {@inheritDoc}
*/
@Override
public String getProductType() throws DeviceNotAvailableException {
return internalGetProductType(MAX_RETRY_ATTEMPTS);
}
/**
* {@link #getProductType()}
*
* @param retryAttempts The number of times to try calling {@link #recoverDevice()} if the
* device's product type cannot be found.
*/
private String internalGetProductType(int retryAttempts) throws DeviceNotAvailableException {
String productType = internalGetProperty(DeviceProperties.BOARD, "product", "Product type");
// fallback to ro.hardware for legacy devices
if (Strings.isNullOrEmpty(productType)) {
productType = internalGetProperty(DeviceProperties.HARDWARE, "product", "Product type");
}
// Things will likely break if we don't have a valid product type. Try recovery (in case
// the device is only partially booted for some reason), and if that doesn't help, bail.
if (Strings.isNullOrEmpty(productType)) {
if (retryAttempts > 0) {
recoverDevice();
productType = internalGetProductType(retryAttempts - 1);
}
if (Strings.isNullOrEmpty(productType)) {
throw new DeviceNotAvailableException(
String.format(
"Could not determine product type for device %s.",
getSerialNumber()),
getSerialNumber(),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
}
return productType.toLowerCase();
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductType()
throws DeviceNotAvailableException, UnsupportedOperationException {
String prop = getFastbootVariable("product");
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
/**
* {@inheritDoc}
*/
@Override
public String getProductVariant() throws DeviceNotAvailableException {
String prop = internalGetProperty(DeviceProperties.VARIANT, "variant", "Product variant");
if (prop == null) {
prop =
internalGetProperty(
DeviceProperties.VARIANT_LEGACY_O_MR1, "variant", "Product variant");
}
if (prop == null) {
prop =
internalGetProperty(
DeviceProperties.VARIANT_LEGACY_LESS_EQUAL_O,
"variant",
"Product variant");
}
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductVariant()
throws DeviceNotAvailableException, UnsupportedOperationException {
String prop = getFastbootVariable("variant");
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
/** {@inheritDoc} */
@Override
public String getFastbootVariable(String variableName)
throws DeviceNotAvailableException, UnsupportedOperationException {
CommandResult result = executeFastbootCommand("getvar", variableName);
CLog.d(
"(getvar %s) output:\nstdout%s\nstderr:%s",
variableName, result.getStdout(), result.getStderr());
if (result.getStatus() == CommandStatus.SUCCESS) {
Pattern fastbootProductPattern = Pattern.compile(variableName + ":\\s(.*)\\s");
// fastboot is weird, and may dump the output on stderr instead of stdout
String resultText = result.getStdout();
if (resultText == null || resultText.length() < 1) {
resultText = result.getStderr();
}
Matcher matcher = fastbootProductPattern.matcher(resultText);
if (matcher.find()) {
return matcher.group(1);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildAlias() throws DeviceNotAvailableException {
String alias = getProperty(DeviceProperties.BUILD_ALIAS);
if (alias == null || alias.isEmpty()) {
return getBuildId();
}
return alias;
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildId() throws DeviceNotAvailableException {
String bid = getProperty(DeviceProperties.BUILD_ID);
if (bid == null) {
CLog.w("Could not get device %s build id.", getSerialNumber());
return IBuildInfo.UNKNOWN_BUILD_ID;
}
return bid;
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildFlavor() throws DeviceNotAvailableException {
String buildFlavor = getProperty(DeviceProperties.BUILD_FLAVOR);
if (buildFlavor != null && !buildFlavor.isEmpty()) {
return buildFlavor;
}
String productName = getProperty(DeviceProperties.PRODUCT);
String buildType = getProperty(DeviceProperties.BUILD_TYPE);
if (productName == null || buildType == null) {
CLog.w("Could not get device %s build flavor.", getSerialNumber());
return null;
}
return String.format("%s-%s", productName, buildType);
}
/**
* {@inheritDoc}
*/
@Override
public void executeShellCommand(final String command, final IShellOutputReceiver receiver)
throws DeviceNotAvailableException {
DeviceAction action = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException,
AdbCommandRejectedException, ShellCommandUnresponsiveException {
getIDevice().executeShellCommand(command, receiver,
getCommandTimeout(), TimeUnit.MILLISECONDS);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
@Override
public void executeShellCommand(final String command, final IShellOutputReceiver receiver,
final long maxTimeToOutputShellResponse, final TimeUnit timeUnit,
final int retryAttempts) throws DeviceNotAvailableException {
DeviceAction action = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
getIDevice().executeShellCommand(command, receiver,
maxTimeToOutputShellResponse, timeUnit);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, retryAttempts);
}
/** {@inheritDoc} */
@Override
public void executeShellCommand(
final String command,
final IShellOutputReceiver receiver,
final long maxTimeoutForCommand,
final long maxTimeToOutputShellResponse,
final TimeUnit timeUnit,
final int retryAttempts)
throws DeviceNotAvailableException {
DeviceAction action =
new DeviceAction() {
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
getIDevice()
.executeShellCommand(
command,
receiver,
maxTimeoutForCommand,
maxTimeToOutputShellResponse,
timeUnit);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, retryAttempts);
}
/**
* {@inheritDoc}
*/
@Override
public String executeShellCommand(String command) throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(command, receiver);
String output = receiver.getOutput();
if (mExecuteShellCommandLogs != null) {
// Log all output to a dedicated file as it can be very verbose.
String formatted =
LogUtil.getLogFormatString(
LogLevel.VERBOSE,
"NativeDevice",
String.format(
"%s on %s returned %s\n==== END OF OUTPUT ====\n",
command, getSerialNumber(), output));
try {
FileUtil.writeToFile(formatted, mExecuteShellCommandLogs, true);
} catch (IOException e) {
// Ignore the full error
CLog.e("Failed to log to executeShellCommand log: %s", e.getMessage());
}
}
if (output.length() > 80) {
CLog.v(
"%s on %s returned %s <truncated - See executeShellCommand log for full trace>",
command, getSerialNumber(), output.substring(0, 80));
} else {
CLog.v("%s on %s returned %s", command, getSerialNumber(), output);
}
return output;
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(String cmd) throws DeviceNotAvailableException {
return executeShellV2Command(cmd, getCommandTimeout(), TimeUnit.MILLISECONDS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(String cmd, File pipeAsInput)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd,
pipeAsInput,
null,
getCommandTimeout(),
TimeUnit.MILLISECONDS,
MAX_RETRY_ATTEMPTS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(String cmd, OutputStream pipeToOutput)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd,
null,
pipeToOutput,
getCommandTimeout(),
TimeUnit.MILLISECONDS,
MAX_RETRY_ATTEMPTS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd, null, null, maxTimeoutForCommand, timeUnit, MAX_RETRY_ATTEMPTS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit, int retryAttempts)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd, null, null, maxTimeoutForCommand, timeUnit, retryAttempts);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd,
File pipeAsInput,
OutputStream pipeToOutput,
final long maxTimeoutForCommand,
final TimeUnit timeUnit,
int retryAttempts)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd,
pipeAsInput,
pipeToOutput,
/*pipeToError=*/ null,
maxTimeoutForCommand,
timeUnit,
retryAttempts);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd,
File pipeAsInput,
OutputStream pipeToOutput,
OutputStream pipeToError,
final long maxTimeoutForCommand,
final TimeUnit timeUnit,
int retryAttempts)
throws DeviceNotAvailableException {
// If the device does not support the v2 shell features. Exit status will not be propagated
// and stdout/stderr will be merged in stdout.
//
// There's nothing we can do to separate the two streams, but we can alter the command to
// retrieve the exit status.
//
// Note that this does *not* call `adb features` on each invocation. ddmlib caches all the
// adb features on the first query.
boolean parseExitStatus =
!getIDevice().supportsFeature(IDevice.Feature.SHELL_V2)
&& getOptions().useExitStatusWorkaround();
final String[] fullCmd = buildAdbShellCommand(cmd, parseExitStatus);
AdbShellAction adbActionV2 =
new AdbShellAction(
fullCmd,
pipeAsInput,
pipeToOutput,
pipeToError,
timeUnit.toMillis(maxTimeoutForCommand));
performDeviceAction(String.format("adb %s", fullCmd[4]), adbActionV2, retryAttempts);
if (parseExitStatus) {
postProcessExitStatus(adbActionV2.mResult);
}
return adbActionV2.mResult;
}
private void postProcessExitStatus(@Nonnull CommandResult result) {
String stdout = result.getStdout();
int delimiterIndex = stdout.lastIndexOf(EXIT_STATUS_DELIMITER);
if (delimiterIndex < 0) {
result.setStatus(CommandStatus.FAILED);
return;
}
String actualStdout = stdout.substring(0, delimiterIndex);
String exitStatusText = stdout.substring(delimiterIndex + 1);
result.setExitCode(Integer.parseUnsignedInt(exitStatusText.trim()));
result.setStdout(actualStdout);
if (result.getStatus() == CommandStatus.SUCCESS && result.getExitCode() != 0) {
result.setStatus(CommandStatus.FAILED);
}
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTests(
final IRemoteAndroidTestRunner runner,
final Collection<ITestLifeCycleReceiver> listeners)
throws DeviceNotAvailableException {
RunFailureListener failureListener = new RunFailureListener();
List<ITestRunListener> runListeners = new ArrayList<>();
runListeners.add(failureListener);
runListeners.add(new TestRunToTestInvocationForwarder(listeners));
if ((mConfiguration != null)
&& mConfiguration.getCoverageOptions().isCoverageEnabled()
&& mConfiguration
.getCoverageOptions()
.getCoverageToolchains()
.contains(Toolchain.JACOCO)) {
runner.setCoverage(true);
}
DeviceAction runTestsAction =
new DeviceAction() {
@Override
public boolean run()
throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException,
SyncException {
runner.run(runListeners);
return true;
}
};
boolean result = performDeviceAction(String.format("run %s instrumentation tests",
runner.getPackageName()), runTestsAction, 0);
if (failureListener.isRunFailure()) {
waitForDeviceAvailable();
}
return result;
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTests(
IRemoteAndroidTestRunner runner, ITestLifeCycleReceiver... listeners)
throws DeviceNotAvailableException {
List<ITestLifeCycleReceiver> listenerList = new ArrayList<>();
listenerList.addAll(Arrays.asList(listeners));
return runInstrumentationTests(runner, listenerList);
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTestsAsUser(
final IRemoteAndroidTestRunner runner,
int userId,
final Collection<ITestLifeCycleReceiver> listeners)
throws DeviceNotAvailableException {
String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
boolean result = runInstrumentationTests(runner, listeners);
resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
return result;
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTestsAsUser(
IRemoteAndroidTestRunner runner, int userId, ITestLifeCycleReceiver... listeners)
throws DeviceNotAvailableException {
String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
boolean result = runInstrumentationTests(runner, listeners);
resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
return result;
}
/**
* Helper method to add user run time option to {@link RemoteAndroidTestRunner}
*
* @param runner {@link IRemoteAndroidTestRunner}
* @param userId the integer of the user id to run as.
* @return original run time options.
*/
private String appendUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner, int userId) {
if (runner instanceof RemoteAndroidTestRunner) {
String original = ((RemoteAndroidTestRunner) runner).getRunOptions();
String userRunTimeOption = String.format("--user %s", Integer.toString(userId));
String updated = (original != null) ? (original + " " + userRunTimeOption)
: userRunTimeOption;
((RemoteAndroidTestRunner) runner).setRunOptions(updated);
return original;
} else {
throw new IllegalStateException(String.format("%s runner does not support multi-user",
runner.getClass().getName()));
}
}
/**
* Helper method to reset the run time options to {@link RemoteAndroidTestRunner}
*
* @param runner {@link IRemoteAndroidTestRunner}
* @param oldRunTimeOptions
*/
private void resetUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner,
String oldRunTimeOptions) {
if (runner instanceof RemoteAndroidTestRunner) {
if (oldRunTimeOptions != null) {
((RemoteAndroidTestRunner) runner).setRunOptions(oldRunTimeOptions);
}
} else {
throw new IllegalStateException(String.format("%s runner does not support multi-user",
runner.getClass().getName()));
}
}
private static class RunFailureListener extends StubTestRunListener {
private boolean mIsRunFailure = false;
@Override
public void testRunFailed(String message) {
mIsRunFailure = true;
}
public boolean isRunFailure() {
return mIsRunFailure;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isRuntimePermissionSupported() throws DeviceNotAvailableException {
int apiLevel = getApiLevel();
boolean condition = apiLevel > 22;
if (!condition) {
CLog.w(
"isRuntimePermissionSupported requires api level above 22, device reported "
+ "'%s'",
apiLevel);
}
return condition;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAppEnumerationSupported() throws DeviceNotAvailableException {
return false;
}
/** {@inheritDoc} */
@Override
public boolean isBypassLowTargetSdkBlockSupported() throws DeviceNotAvailableException {
return checkApiLevelAgainstNextRelease(34);
}
/**
* helper method to throw exception if runtime permission isn't supported
* @throws DeviceNotAvailableException
*/
protected void ensureRuntimePermissionSupported() throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
if (!runtimePermissionSupported) {
throw new UnsupportedOperationException(
"platform on device does not support runtime permission granting!");
}
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(final File packageFile, final boolean reinstall,
final String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall, int userId,
String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall,
boolean grantPermissions, int userId, String... extraArgs)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/** {@inheritDoc} */
@Override
public String uninstallPackageForUser(final String packageName, int userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/** {@inheritDoc} */
@Override
public boolean pullFile(final String remoteFilePath, final File localFile, int userId)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.PULL_FILE_COUNT, 1);
try {
if (isSdcardOrEmulated(remoteFilePath) && userId != 0) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
return handler.pullFile(remoteFilePath, localFile);
}
}
return pullFileInternal(remoteFilePath, localFile);
} finally {
long totalTime = System.currentTimeMillis() - startTime;
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PULL_FILE_TIME, totalTime);
}
}
/** {@inheritDoc} */
@Override
public boolean pullFile(final String remoteFilePath, final File localFile)
throws DeviceNotAvailableException {
return pullFile(remoteFilePath, localFile, getCurrentUserCompatible());
}
/** {@inheritDoc} */
@Override
public File pullFile(String remoteFilePath, int userId) throws DeviceNotAvailableException {
File localFile = null;
boolean success = false;
try {
localFile = FileUtil.createTempFileForRemote(remoteFilePath, null);
if (pullFile(remoteFilePath, localFile, userId)) {
success = true;
return localFile;
}
} catch (IOException e) {
CLog.w("Encountered IOException while trying to pull '%s':", remoteFilePath);
CLog.e(e);
} finally {
if (!success) {
FileUtil.deleteFile(localFile);
}
}
return null;
}
/** {@inheritDoc} */
@Override
public File pullFile(String remoteFilePath) throws DeviceNotAvailableException {
return pullFile(remoteFilePath, getCurrentUserCompatible());
}
/**
* {@inheritDoc}
*/
@Override
public String pullFileContents(String remoteFilePath) throws DeviceNotAvailableException {
File temp = pullFile(remoteFilePath);
if (temp != null) {
try {
return FileUtil.readStringFromFile(temp);
} catch (IOException e) {
CLog.e(String.format("Could not pull file: %s", remoteFilePath));
} finally {
FileUtil.deleteFile(temp);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public File pullFileFromExternal(String remoteFilePath) throws DeviceNotAvailableException {
String externalPath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
String fullPath = new File(externalPath, remoteFilePath).getPath();
return pullFile(fullPath);
}
protected boolean pullFileInternal(String remoteFilePath, File localFile)
throws DeviceNotAvailableException {
DeviceAction pullAction =
new DeviceAction() {
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.pullFile(
interpolatePathVariables(remoteFilePath),
localFile.getAbsolutePath(),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w(
"Failed to pull %s from %s to %s. Message %s",
remoteFilePath,
getSerialNumber(),
localFile.getAbsolutePath(),
e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(
String.format("pull %s to %s", remoteFilePath, localFile.getAbsolutePath()),
pullAction,
MAX_RETRY_ATTEMPTS);
}
/**
* Helper function that watches for the string "${EXTERNAL_STORAGE}" and replaces it with the
* pathname of the EXTERNAL_STORAGE mountpoint. Specifically intended to be used for pathnames
* that are being passed to SyncService, which does not support variables inside of filenames.
*/
String interpolatePathVariables(String path) {
final String esString = "${EXTERNAL_STORAGE}";
if (path.contains(esString)) {
final String esPath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
path = path.replace(esString, esPath);
}
return path;
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushFile(final File localFile, final String remoteFilePath)
throws DeviceNotAvailableException {
return pushFileInternal(localFile, remoteFilePath, false);
}
@Override
public boolean pushFile(
final File localFile,
final String remoteFilePath,
boolean evaluateContentProviderNeeded)
throws DeviceNotAvailableException {
boolean skipContentProvider = false;
if (evaluateContentProviderNeeded) {
skipContentProvider = getCurrentUserCompatible() == 0;
}
return pushFileInternal(localFile, remoteFilePath, skipContentProvider);
}
@VisibleForTesting
boolean pushFileInternal(final File localFile, final String remoteFilePath,
boolean skipContentProvider) throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.PUSH_FILE_COUNT, 1);
try {
if (!skipContentProvider) {
if (isSdcardOrEmulated(remoteFilePath)) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
return handler.pushFile(localFile, remoteFilePath);
}
}
}
DeviceAction pushAction =
new DeviceAction() {
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
if (syncService == null) {
throw new IOException("SyncService returned null.");
}
syncService.pushFile(
localFile.getAbsolutePath(),
interpolatePathVariables(remoteFilePath),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w(
"Failed to push %s to %s on device %s. Message: '%s'. "
+ "Error code: %s",
localFile.getAbsolutePath(),
remoteFilePath,
getSerialNumber(),
e.getMessage(),
e.getErrorCode());
// TODO: check if ddmlib can report a better error
if (SyncError.TRANSFER_PROTOCOL_ERROR.equals(e.getErrorCode())) {
if (e.getMessage().contains("Permission denied")) {
return false;
}
}
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(
String.format("push %s to %s", localFile.getAbsolutePath(), remoteFilePath),
pushAction,
MAX_RETRY_ATTEMPTS);
} finally {
long totalTime = System.currentTimeMillis() - startTime;
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PUSH_FILE_TIME, totalTime);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushString(final String contents, final String remoteFilePath)
throws DeviceNotAvailableException {
File tmpFile = null;
try {
tmpFile = FileUtil.createTempFile("temp", ".txt");
FileUtil.writeToFile(contents, tmpFile);
return pushFile(tmpFile, remoteFilePath);
} catch (IOException e) {
CLog.e(e);
return false;
} finally {
FileUtil.deleteFile(tmpFile);
}
}
/** {@inheritDoc} */
@Override
public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
return doesFileExist(deviceFilePath, getCurrentUserCompatible());
}
@Override
public boolean doesFileExist(String deviceFilePath, int userId)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
// Skip ContentProvider for user 0
if (isSdcardOrEmulated(deviceFilePath) && userId != 0) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
CLog.d("Delegating check to ContentProvider doesFileExist(%s)", deviceFilePath);
return handler.doesFileExist(deviceFilePath);
}
}
CLog.d("Using 'ls' to check doesFileExist(%s)", deviceFilePath);
String lsGrep = executeShellCommand(String.format("ls \"%s\"", deviceFilePath));
return !lsGrep.contains("No such file or directory");
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.DOES_FILE_EXISTS_TIME,
System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.DOES_FILE_EXISTS_COUNT, 1);
}
}
@Override
public void registerDeviceActionReceiver(IDeviceActionReceiver deviceActionReceiver) {
mDeviceActionReceivers.add(deviceActionReceiver);
}
@Override
public void deregisterDeviceActionReceiver(IDeviceActionReceiver deviceActionReceiver) {
mDeviceActionReceivers.remove(deviceActionReceiver);
}
/** {@inheritDoc} */
@Override
public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
if (isSdcardOrEmulated(deviceFilePath)) {
int currentUser = getCurrentUserCompatible();
if (currentUser != 0) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
if (handler.deleteFile(deviceFilePath)) {
return;
}
}
}
}
// Fallback to the direct command if content provider is unsuccessful
String path = StringEscapeUtils.escapeShell(deviceFilePath);
// Escape spaces to handle filename with spaces
path = path.replaceAll(" ", "\\ ");
executeShellCommand(String.format("rm -rf %s", StringEscapeUtils.escapeShell(path)));
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.DELETE_DEVICE_FILE_TIME,
System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.DELETE_DEVICE_FILE_COUNT, 1);
}
}
/**
* {@inheritDoc}
*/
@Override
public long getExternalStoreFreeSpace() throws DeviceNotAvailableException {
String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
return getPartitionFreeSpace(externalStorePath);
}
/** {@inheritDoc} */
@Override
public long getPartitionFreeSpace(String partition) throws DeviceNotAvailableException {
CLog.i("Checking free space for %s on partition %s", getSerialNumber(), partition);
String output = getDfOutput(partition);
// Try coreutils/toybox style output first.
Long available = parseFreeSpaceFromModernOutput(output);
if (available != null) {
return available;
}
// Then the two legacy toolbox formats.
available = parseFreeSpaceFromAvailable(output);
if (available != null) {
return available;
}
available = parseFreeSpaceFromFree(partition, output);
if (available != null) {
return available;
}
CLog.e("free space command output \"%s\" did not match expected patterns", output);
return 0;
}
/**
* Run the 'df' shell command and return output, making multiple attempts if necessary.
*
* @param externalStorePath the path to check
* @return the output from 'shell df path'
* @throws DeviceNotAvailableException
*/
private String getDfOutput(String externalStorePath) throws DeviceNotAvailableException {
for (int i=0; i < MAX_RETRY_ATTEMPTS; i++) {
String output = executeShellCommand(String.format("df %s", externalStorePath));
if (output.trim().length() > 0) {
return output;
}
}
throw new DeviceUnresponsiveException(String.format(
"Device %s not returning output from df command after %d attempts",
getSerialNumber(), MAX_RETRY_ATTEMPTS), getSerialNumber());
}
/**
* Parses a partition's available space from the legacy output of a 'df' command, used
* pre-gingerbread.
* <p/>
* Assumes output format of:
* <br>/
* <code>
* [partition]: 15659168K total, 51584K used, 15607584K available (block size 32768)
* </code>
* @param dfOutput the output of df command to parse
* @return the available space in kilobytes or <code>null</code> if output could not be parsed
*/
private Long parseFreeSpaceFromAvailable(String dfOutput) {
final Pattern freeSpacePattern = Pattern.compile("(\\d+)K available");
Matcher patternMatcher = freeSpacePattern.matcher(dfOutput);
if (patternMatcher.find()) {
String freeSpaceString = patternMatcher.group(1);
try {
return Long.parseLong(freeSpaceString);
} catch (NumberFormatException e) {
// fall through
}
}
return null;
}
/**
* Parses a partition's available space from the 'table-formatted' output of a toolbox 'df'
* command, used from gingerbread to lollipop.
* <p/>
* Assumes output format of:
* <br/>
* <code>
* Filesystem Size Used Free Blksize
* <br/>
* [partition]: 3G 790M 2G 4096
* </code>
* @param dfOutput the output of df command to parse
* @return the available space in kilobytes or <code>null</code> if output could not be parsed
*/
Long parseFreeSpaceFromFree(String externalStorePath, String dfOutput) {
Long freeSpace = null;
final Pattern freeSpaceTablePattern = Pattern.compile(String.format(
//fs Size Used Free
"%s\\s+[\\w\\d\\.]+\\s+[\\w\\d\\.]+\\s+([\\d\\.]+)(\\w)", externalStorePath));
Matcher tablePatternMatcher = freeSpaceTablePattern.matcher(dfOutput);
if (tablePatternMatcher.find()) {
String numericValueString = tablePatternMatcher.group(1);
String unitType = tablePatternMatcher.group(2);
try {
float freeSpaceFloat = Float.parseFloat(numericValueString);
if (unitType.equals("M")) {
freeSpaceFloat = freeSpaceFloat * 1024;
} else if (unitType.equals("G")) {
freeSpaceFloat = freeSpaceFloat * 1024 * 1024;
}
freeSpace = (long) freeSpaceFloat;
} catch (NumberFormatException e) {
// fall through
}
}
return freeSpace;
}
/**
* Parses a partition's available space from the modern coreutils/toybox 'df' output, used
* after lollipop.
* <p/>
* Assumes output format of:
* <br/>
* <code>
* Filesystem 1K-blocks Used Available Use% Mounted on
* <br/>
* /dev/fuse 11585536 1316348 10269188 12% /mnt/shell/emulated
* </code>
* @param dfOutput the output of df command to parse
* @return the available space in kilobytes or <code>null</code> if output could not be parsed
*/
Long parseFreeSpaceFromModernOutput(String dfOutput) {
Matcher matcher = DF_PATTERN.matcher(dfOutput);
if (matcher.find()) {
try {
return Long.parseLong(matcher.group(2));
} catch (NumberFormatException e) {
// fall through
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getMountPoint(String mountName) {
try {
return mStateMonitor.getMountPoint(mountName);
} catch (DeviceNotAvailableException e) {
CLog.e(e);
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public List<MountPointInfo> getMountPointInfo() throws DeviceNotAvailableException {
final String mountInfo = executeShellCommand("cat /proc/mounts");
final String[] mountInfoLines = mountInfo.split("\r?\n");
List<MountPointInfo> list = new ArrayList<>(mountInfoLines.length);
for (String line : mountInfoLines) {
// We ignore the last two fields
// /dev/block/mtdblock4 /cache yaffs2 rw,nosuid,nodev,relatime 0 0
final String[] parts = line.split("\\s+", 5);
list.add(new MountPointInfo(parts[0], parts[1], parts[2], parts[3]));
}
return list;
}
/**
* {@inheritDoc}
*/
@Override
public MountPointInfo getMountPointInfo(String mountpoint) throws DeviceNotAvailableException {
// The overhead of parsing all of the lines should be minimal
List<MountPointInfo> mountpoints = getMountPointInfo();
for (MountPointInfo info : mountpoints) {
if (mountpoint.equals(info.mountpoint)) {
return info;
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
path = interpolatePathVariables(path);
String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
FileListingService service = getFileListingService();
IFileEntry rootFile = new FileEntryWrapper(this, service.getRoot());
return FileEntryWrapper.getDescendant(rootFile, Arrays.asList(pathComponents));
}
/**
* Unofficial helper to get a {@link FileEntry} from a non-root path. FIXME: Refactor the
* FileEntry system to have it available from any path. (even non root).
*
* @param entry a {@link FileEntry} not necessarily root as Ddmlib requires.
* @return a {@link FileEntryWrapper} representing the FileEntry.
* @throws DeviceNotAvailableException
*/
public IFileEntry getFileEntry(FileEntry entry) throws DeviceNotAvailableException {
// FileEntryWrapper is going to construct the list of child file internally.
return new FileEntryWrapper(this, entry);
}
/** {@inheritDoc} */
@Override
public boolean isExecutable(String fullPath) throws DeviceNotAvailableException {
String fileMode = executeShellCommand(String.format("ls -l %s", fullPath));
if (fileMode != null) {
return EXE_FILE.matcher(fileMode).find();
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDirectory(String path) throws DeviceNotAvailableException {
return executeShellCommand(String.format("ls -ld %s", path)).charAt(0) == 'd';
}
/**
* {@inheritDoc}
*/
@Override
public String[] getChildren(String path) throws DeviceNotAvailableException {
String lsOutput = executeShellCommand(String.format("ls -A1 %s", path));
if (lsOutput.trim().isEmpty()) {
return new String[0];
}
return lsOutput.split("\r?\n");
}
/**
* Retrieve the {@link FileListingService} for the {@link IDevice}, making multiple attempts
* and recovery operations if necessary.
* <p/>
* This is necessary because {@link IDevice#getFileListingService()} can return
* <code>null</code> if device is in fastboot. The symptom of this condition is that the
* current {@link #getIDevice()} is a {@link StubDevice}.
*
* @return the {@link FileListingService}
* @throws DeviceNotAvailableException if device communication is lost.
*/
private FileListingService getFileListingService() throws DeviceNotAvailableException {
final FileListingService[] service = new FileListingService[1];
DeviceAction serviceAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
service[0] = getIDevice().getFileListingService();
if (service[0] == null) {
// could not get file listing service - must be a stub device - enter recovery
throw new IOException("Could not get file listing service");
}
return true;
}
};
performDeviceAction("getFileListingService", serviceAction, MAX_RETRY_ATTEMPTS);
return service[0];
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushDir(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
return pushDir(localFileDir, deviceFilePath, new HashSet<>());
}
/** {@inheritDoc} */
@Override
public boolean pushDir(
File localFileDir, String deviceFilePath, Set<String> excludedDirectories)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
if (isSdcardOrEmulated(deviceFilePath)) {
Integer currentUser = getCurrentUserCompatible();
if (currentUser != 0) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
return handler.pushDir(localFileDir, deviceFilePath, excludedDirectories);
}
} else {
// Remove the special handling when content provider performance is better
CLog.d("Push without content provider for user '%s'", currentUser);
}
}
return pushDirInternal(localFileDir, deviceFilePath, excludedDirectories);
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PUSH_DIR_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.PUSH_DIR_COUNT, 1);
}
}
private boolean pushDirInternal(
File localFileDir, String deviceFilePath, Set<String> excludedDirectories)
throws DeviceNotAvailableException {
if (!localFileDir.isDirectory()) {
CLog.e("file %s is not a directory", localFileDir.getAbsolutePath());
return false;
}
File[] childFiles = localFileDir.listFiles();
if (childFiles == null) {
CLog.e("Could not read files in %s", localFileDir.getAbsolutePath());
return false;
}
for (File childFile : childFiles) {
String remotePath = String.format("%s/%s", deviceFilePath, childFile.getName());
if (childFile.isDirectory()) {
// If we encounter a filtered directory do not push it.
if (excludedDirectories.contains(childFile.getName())) {
CLog.d(
"%s directory was not pushed because it was filtered.",
childFile.getAbsolutePath());
continue;
}
executeShellCommand(String.format("mkdir -p \"%s\"", remotePath));
if (!pushDirInternal(childFile, remotePath, excludedDirectories)) {
return false;
}
} else if (childFile.isFile()) {
if (!pushFileInternal(childFile, remotePath, true)) {
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean pullDir(String deviceFilePath, File localDir)
throws DeviceNotAvailableException {
long startTime = System.currentTimeMillis();
try {
int currentUser = getCurrentUserCompatible();
if (isSdcardOrEmulated(deviceFilePath)) {
if (currentUser != 0) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
return handler.pullDir(deviceFilePath, localDir);
}
}
}
return pullDirInternal(deviceFilePath, localDir, currentUser);
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.PULL_DIR_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.PULL_DIR_COUNT, 1);
}
}
private boolean pullDirInternal(String deviceFilePath, File localDir, int userId)
throws DeviceNotAvailableException {
if (!localDir.isDirectory()) {
CLog.e("Local path %s is not a directory", localDir.getAbsolutePath());
return false;
}
if (!doesFileExist(deviceFilePath, userId)) {
CLog.e("Device path %s does not exist to be pulled.", deviceFilePath);
return false;
}
if (!isDirectory(deviceFilePath)) {
CLog.e("Device path %s is not a directory", deviceFilePath);
return false;
}
FileEntry entryRoot =
new FileEntry(null, deviceFilePath, FileListingService.TYPE_DIRECTORY, false);
IFileEntry entry = getFileEntry(entryRoot);
Collection<IFileEntry> children = entry.getChildren(false);
if (children.isEmpty()) {
CLog.i("Device path is empty, nothing to do.");
return true;
}
for (IFileEntry item : children) {
if (item.isDirectory()) {
// handle sub dir
File subDir = new File(localDir, item.getName());
if (!subDir.mkdir()) {
CLog.w(
"Failed to create sub directory %s, aborting.",
subDir.getAbsolutePath());
return false;
}
String deviceSubDir = item.getFullPath();
if (!pullDirInternal(deviceSubDir, subDir, userId)) {
CLog.w("Failed to pull sub directory %s from device, aborting", deviceSubDir);
return false;
}
} else {
// handle regular file
File localFile = new File(localDir, item.getName());
String fullPath = item.getFullPath();
if (!pullFileInternal(fullPath, localFile)) {
CLog.w("Failed to pull file %s from device, aborting", fullPath);
return false;
}
}
}
return true;
}
/** Checks whether path is external storage path. */
private boolean isSdcardOrEmulated(String path) {
return path.startsWith(SD_CARD) || path.startsWith(STORAGE_EMULATED);
}
/**
* {@inheritDoc}
*/
@Override
public boolean syncFiles(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
if (localFileDir == null || deviceFilePath == null) {
throw new IllegalArgumentException("syncFiles does not take null arguments");
}
CLog.i("Syncing %s to %s on device %s",
localFileDir.getAbsolutePath(), deviceFilePath, getSerialNumber());
if (!localFileDir.isDirectory()) {
CLog.e("file %s is not a directory", localFileDir.getAbsolutePath());
return false;
}
// get the real destination path. This is done because underlying syncService.push
// implementation will add localFileDir.getName() to destination path
deviceFilePath = String.format("%s/%s", interpolatePathVariables(deviceFilePath),
localFileDir.getName());
if (!doesFileExist(deviceFilePath)) {
executeShellCommand(String.format("mkdir -p \"%s\"", deviceFilePath));
}
IFileEntry remoteFileEntry = getFileEntry(deviceFilePath);
if (remoteFileEntry == null) {
CLog.e("Could not find remote file entry %s ", deviceFilePath);
return false;
}
return syncFiles(localFileDir, remoteFileEntry);
}
/**
* Recursively sync newer files.
*
* @param localFileDir the local {@link File} directory to sync
* @param remoteFileEntry the remote destination {@link IFileEntry}
* @return <code>true</code> if files were synced successfully
* @throws DeviceNotAvailableException
*/
private boolean syncFiles(File localFileDir, final IFileEntry remoteFileEntry)
throws DeviceNotAvailableException {
CLog.d("Syncing %s to %s on %s", localFileDir.getAbsolutePath(),
remoteFileEntry.getFullPath(), getSerialNumber());
// find newer files to sync
File[] localFiles = localFileDir.listFiles(new NoHiddenFilesFilter());
ArrayList<String> filePathsToSync = new ArrayList<>();
for (File localFile : localFiles) {
IFileEntry entry = remoteFileEntry.findChild(localFile.getName());
if (entry == null) {
CLog.d("Detected missing file path %s", localFile.getAbsolutePath());
filePathsToSync.add(localFile.getAbsolutePath());
} else if (localFile.isDirectory()) {
// This directory exists remotely. recursively sync it to sync only its newer files
// contents
if (!syncFiles(localFile, entry)) {
return false;
}
} else if (isNewer(localFile, entry)) {
CLog.d("Detected newer file %s", localFile.getAbsolutePath());
filePathsToSync.add(localFile.getAbsolutePath());
}
}
if (filePathsToSync.size() == 0) {
CLog.d("No files to sync");
return true;
}
final String files[] = filePathsToSync.toArray(new String[filePathsToSync.size()]);
DeviceAction syncAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.push(files, remoteFileEntry.getFileEntry(),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to sync files to %s on device %s. Message %s",
remoteFileEntry.getFullPath(), getSerialNumber(), e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("sync files %s", remoteFileEntry.getFullPath()),
syncAction, MAX_RETRY_ATTEMPTS);
}
/**
* Queries the file listing service for a given directory
*
* @param remoteFileEntry
* @throws DeviceNotAvailableException
*/
FileEntry[] getFileChildren(final FileEntry remoteFileEntry)
throws DeviceNotAvailableException {
// time this operation because its known to hang
FileQueryAction action = new FileQueryAction(remoteFileEntry,
getIDevice().getFileListingService());
performDeviceAction("buildFileCache", action, 1 /* one retry */);
return action.mFileContents;
}
private class FileQueryAction implements DeviceAction {
FileEntry[] mFileContents = null;
private final FileEntry mRemoteFileEntry;
private final FileListingService mService;
FileQueryAction(FileEntry remoteFileEntry, FileListingService service) {
throwIfNull(remoteFileEntry);
throwIfNull(service);
mRemoteFileEntry = remoteFileEntry;
mService = service;
}
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
mFileContents = mService.getChildrenSync(mRemoteFileEntry);
return true;
}
}
/**
* A {@link FilenameFilter} that rejects hidden (ie starts with ".") files.
*/
private static class NoHiddenFilesFilter implements FilenameFilter {
/**
* {@inheritDoc}
*/
@Override
public boolean accept(File dir, String name) {
return !name.startsWith(".");
}
}
/**
* helper to get the timezone from the device. Example: "Europe/London"
*/
private String getDeviceTimezone() {
try {
// This may not be set at first, default to GMT in this case.
String timezone = getProperty("persist.sys.timezone");
if (timezone != null) {
return timezone.trim();
}
} catch (DeviceNotAvailableException e) {
// Fall through on purpose
}
return "GMT";
}
/**
* Return <code>true</code> if local file is newer than remote file. {@link IFileEntry} being
* accurate to the minute, in case of equal times, the file will be considered newer.
*/
@VisibleForTesting
protected boolean isNewer(File localFile, IFileEntry entry) {
final String entryTimeString = String.format("%s %s", entry.getDate(), entry.getTime());
try {
String timezone = getDeviceTimezone();
// expected format of a FileEntry's date and time
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
format.setTimeZone(TimeZone.getTimeZone(timezone));
Date remoteDate = format.parse(entryTimeString);
long offset = 0;
try {
offset = getDeviceTimeOffset(null);
} catch (DeviceNotAvailableException e) {
offset = 0;
}
CLog.i("Device offset time: %s", offset);
// localFile.lastModified has granularity of ms, but remoteDate.getTime only has
// granularity of minutes. Shift remoteDate.getTime() backward by one minute so newly
// modified files get synced
return localFile.lastModified() > (remoteDate.getTime() - 60 * 1000 + offset);
} catch (ParseException e) {
CLog.e("Error converting remote time stamp %s for %s on device %s", entryTimeString,
entry.getFullPath(), getSerialNumber());
}
// sync file by default
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String executeAdbCommand(String... cmdArgs) throws DeviceNotAvailableException {
return executeAdbCommand(getCommandTimeout(), cmdArgs);
}
/** {@inheritDoc} */
@Override
public String executeAdbCommand(long timeout, String... cmdArgs)
throws DeviceNotAvailableException {
return executeAdbCommand(getCommandTimeout(), new HashMap<>(), cmdArgs);
}
/** {@inheritDoc} */
@Override
public String executeAdbCommand(long timeout, Map<String, String> envMap, String... cmdArgs)
throws DeviceNotAvailableException {
final String[] fullCmd = buildAdbCommand(cmdArgs);
AdbAction adbAction = new AdbAction(timeout, fullCmd, "shell".equals(cmdArgs[0]), envMap);
performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
return adbAction.mOutput;
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
// TODO: fix mixed use of fastboot timeout and command timeout
return doFastbootCommand(getCommandTimeout(), cmdArgs);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeFastbootCommand(long timeout, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(timeout, cmdArgs);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeLongFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return executeLongFastbootCommand(new HashMap<>(), cmdArgs);
}
/** {@inheritDoc} */
@Override
public CommandResult executeLongFastbootCommand(
Map<String, String> envVarMap, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
// TODO: fix mixed use of fastboot timeout and command timeout
return doFastbootCommand(getLongCommandTimeout(), envVarMap, cmdArgs);
}
/**
* Do a fastboot command with environment variables set
*
* @param timeout timeout for the fastboot command
* @param envVarMap environment variables that needs to be set before execute the fastboot
* command
* @param cmdArgs
* @return {@link CommandResult} of the fastboot command
* @throws DeviceNotAvailableException
* @throws UnsupportedOperationException
*/
private CommandResult doFastbootCommand(
final long timeout, Map<String, String> envVarMap, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(String.format(
"Attempted to fastboot on device %s , but fastboot is not available. Aborting.",
getSerialNumber()));
}
File fastbootTmpDir = getHostOptions().getFastbootTmpDir();
if (fastbootTmpDir != null) {
envVarMap.put("TMPDIR", fastbootTmpDir.getAbsolutePath());
}
final String[] fullCmd = buildFastbootCommand(cmdArgs);
for (int i = 0; i < MAX_RETRY_ATTEMPTS; i++) {
try (CloseableTraceScope ignored = new CloseableTraceScope("fastboot " + cmdArgs[0])) {
CommandResult result = simpleFastbootCommand(timeout, envVarMap, fullCmd);
if (!isRecoveryNeeded(result)) {
return result;
}
CLog.w("Recovery needed after executing fastboot command");
if (result != null) {
CLog.v(
"fastboot command output:\nstdout: %s\nstderr:%s",
result.getStdout(), result.getStderr());
}
recoverDeviceFromBootloader();
}
}
throw new DeviceUnresponsiveException(
String.format(
"Attempted fastboot %s multiple "
+ "times on device %s without communication success. Aborting.",
cmdArgs[0], getSerialNumber()),
getSerialNumber());
}
/**
* Do a fastboot command
*
* @param timeout timeout for the fastboot command
* @param cmdArgs
* @return {@link CommandResult} of the fastboot command
* @throws DeviceNotAvailableException
* @throws UnsupportedOperationException
*/
private CommandResult doFastbootCommand(final long timeout, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(timeout, new HashMap<>(), cmdArgs);
}
/**
* {@inheritDoc}
*/
@Override
public boolean getUseFastbootErase() {
return mOptions.getUseFastbootErase();
}
/**
* {@inheritDoc}
*/
@Override
public void setUseFastbootErase(boolean useFastbootErase) {
mOptions.setUseFastbootErase(useFastbootErase);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult fastbootWipePartition(String partition)
throws DeviceNotAvailableException {
if (mOptions.getUseFastbootErase()) {
return executeLongFastbootCommand("erase", partition);
} else {
return executeLongFastbootCommand("format", partition);
}
}
/**
* Evaluate the given fastboot result to determine if recovery mode needs to be entered
*
* @param fastbootResult the {@link CommandResult} from a fastboot command
* @return <code>true</code> if recovery mode should be entered, <code>false</code> otherwise.
*/
private boolean isRecoveryNeeded(CommandResult fastbootResult) {
if (fastbootResult.getStatus().equals(CommandStatus.TIMED_OUT)) {
// fastboot commands always time out if devices is not present
return true;
} else {
// check for specific error messages in result that indicate bad device communication
// and recovery mode is needed
if (fastbootResult.getStderr() == null ||
fastbootResult.getStderr().contains("data transfer failure (Protocol error)") ||
fastbootResult.getStderr().contains("status read failed (No such device)")) {
CLog.w("Bad fastboot response from device %s. stderr: %s. Entering recovery",
getSerialNumber(), fastbootResult.getStderr());
return true;
}
}
return false;
}
/** Get the max time allowed in ms for commands. */
long getCommandTimeout() {
return mOptions.getAdbCommandTimeout();
}
/**
* Set the max time allowed in ms for commands.
*/
void setLongCommandTimeout(long timeout) {
mLongCmdTimeout = timeout;
}
/**
* Get the max time allowed in ms for commands.
*/
long getLongCommandTimeout() {
return mLongCmdTimeout;
}
/** Set the max time allowed in ms for commands. */
void setCommandTimeout(long timeout) {
mOptions.setAdbCommandTimeout(timeout);
}
/**
* Builds the OS command for the given adb command and args
*/
private String[] buildAdbCommand(String... commandArgs) {
return ArrayUtil.buildArray(new String[] {"adb", "-s", getSerialNumber()},
commandArgs);
}
/** Builds the OS command for the given adb shell command session and args */
protected String[] buildAdbShellCommand(String command, boolean forceExitStatusDetection) {
// TODO: implement the shell v2 support in ddmlib itself.
String[] commandArgs =
QuotationAwareTokenizer.tokenizeLine(
command,
/** No logging */
false);
String[] exitStatusProbe;
if (forceExitStatusDetection) {
exitStatusProbe = new String[] {";", "echo", EXIT_STATUS_DELIMITER + "$?"};
} else {
exitStatusProbe = new String[] {};
}
return ArrayUtil.buildArray(
new String[] {"adb", "-s", getSerialNumber(), "shell"},
commandArgs,
exitStatusProbe);
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
return ArrayUtil.buildArray(
new String[] {getFastbootPath(), "-s", getFastbootSerialNumber()}, commandArgs);
}
/**
* Performs an action on this device. Attempts to recover device and optionally retry command if
* action fails.
*
* @param actionDescription a short description of action to be performed. Used for logging
* purposes only.
* @param action the action to be performed
* @param retryAttempts the retry attempts to make for action if it fails but recovery succeeds
* @return <code>true</code> if action was performed successfully
* @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
* success
*/
protected boolean performDeviceAction(
String actionDescription, final DeviceAction action, int retryAttempts)
throws DeviceNotAvailableException {
Exception lastException = null;
try (CloseableTraceScope ignored = new CloseableTraceScope(actionDescription)) {
for (int i = 0; i < retryAttempts + 1; i++) {
boolean shouldRecover = true;
try {
return action.run();
} catch (TimeoutException e) {
logDeviceActionException(actionDescription, e, false);
lastException = e;
} catch (IOException e) {
logDeviceActionException(actionDescription, e, true);
lastException = e;
} catch (InstallException e) {
logDeviceActionException(actionDescription, e, true);
lastException = e;
} catch (SyncException e) {
logDeviceActionException(actionDescription, e, true);
lastException = e;
// a SyncException is not necessarily a device communication problem
// do additional diagnosis
if (!e.getErrorCode().equals(SyncError.BUFFER_OVERRUN)
&& !e.getErrorCode().equals(SyncError.TRANSFER_PROTOCOL_ERROR)) {
// this is a logic problem, doesn't need recovery or to be retried
return false;
}
} catch (AdbCommandRejectedException e) {
// Workaround to not recover device if TCP adb is used.
if (isAdbTcp()
&& (action instanceof RebootDeviceAction)
&& ((RebootDeviceAction) action).isFastbootOrBootloader()) {
CLog.d(
"Ignore AdbCommandRejectedException when TCP device is rebooted"
+ " into fastboot.");
return true;
}
lastException = e;
logDeviceActionException(actionDescription, e, false);
} catch (ShellCommandUnresponsiveException e) {
// ShellCommandUnresponsiveException is thrown when no output occurs within the
// timeout. It doesn't necessarily mean the device is offline.
shouldRecover = false;
lastException = e;
CLog.w(
"Command: '%s' on '%s' went over its timeout for outputing a response.",
actionDescription, getSerialNumber());
}
if (shouldRecover) {
recoverDevice();
}
}
if (retryAttempts > 0) {
throw new DeviceUnresponsiveException(
String.format(
"Attempted %s multiple times "
+ "on device %s without communication success. Aborting.",
actionDescription, getSerialNumber()),
lastException,
getSerialNumber(),
DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
}
return false;
}
}
/**
* Log an entry for given exception
*
* @param actionDescription the action's description
* @param e the exception
* @param logFullTrace whether the full exception stack trace should be logged
*/
private void logDeviceActionException(
String actionDescription, Exception e, boolean logFullTrace) {
CLog.w("%s (%s) when attempting %s on device %s", e.getClass().getSimpleName(),
getExceptionMessage(e), actionDescription, getSerialNumber());
if (logFullTrace) {
CLog.w(e);
}
}
/**
* Make a best effort attempt to retrieve a meaningful short descriptive message for given
* {@link Exception}
*
* @param e the {@link Exception}
* @return a short message
*/
private String getExceptionMessage(Exception e) {
StringBuilder msgBuilder = new StringBuilder();
if (e.getMessage() != null) {
msgBuilder.append(e.getMessage());
}
if (e.getCause() != null) {
msgBuilder.append(" cause: ");
msgBuilder.append(e.getCause().getClass().getSimpleName());
if (e.getCause().getMessage() != null) {
msgBuilder.append(" (");
msgBuilder.append(e.getCause().getMessage());
msgBuilder.append(")");
}
}
return msgBuilder.toString();
}
/**
* Attempts to recover device communication.
*
* @throws DeviceNotAvailableException if device is no longer available
*/
@Override
public boolean recoverDevice() throws DeviceNotAvailableException {
getConnection().reconnectForRecovery(getSerialNumber());
if (mRecoveryMode.equals(RecoveryMode.NONE)) {
CLog.i("Skipping recovery on %s", getSerialNumber());
return false;
}
CLog.i("Attempting recovery on %s", getSerialNumber());
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.RECOVERY_ROUTINE_COUNT, 1);
long startTime = System.currentTimeMillis();
try {
try {
mRecovery.recoverDevice(mStateMonitor, mRecoveryMode.equals(RecoveryMode.ONLINE));
} catch (DeviceUnresponsiveException due) {
RecoveryMode previousRecoveryMode = mRecoveryMode;
mRecoveryMode = RecoveryMode.NONE;
try {
boolean enabled = enableAdbRoot();
CLog.d(
"Device Unresponsive during recovery, is root still enabled: %s",
enabled);
} catch (DeviceUnresponsiveException e) {
// Ignore exception thrown here to rethrow original exception.
CLog.e("Exception occurred during recovery adb root:");
CLog.e(e);
Throwable cause = e.getCause();
if (cause != null && cause instanceof AdbCommandRejectedException) {
AdbCommandRejectedException adbException =
(AdbCommandRejectedException) cause;
if (adbException.isDeviceOffline()
|| adbException.wasErrorDuringDeviceSelection()) {
// Upgrade exception to DNAE to reflect gravity
throw new DeviceNotAvailableException(
cause.getMessage(),
adbException,
getSerialNumber(),
DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
}
}
}
mRecoveryMode = previousRecoveryMode;
throw due;
}
if (mRecoveryMode.equals(RecoveryMode.AVAILABLE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
// this might be a runtime reset - still need to run post boot setup steps
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
postBootSetup();
mRecoveryMode = RecoveryMode.AVAILABLE;
} else if (mRecoveryMode.equals(RecoveryMode.ONLINE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
enableAdbRoot();
mRecoveryMode = RecoveryMode.ONLINE;
}
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.RECOVERY_TIME, System.currentTimeMillis() - startTime);
}
CLog.i("Recovery successful for %s", getSerialNumber());
return true;
}
/**
* Attempts to recover device fastboot communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
private void recoverDeviceFromBootloader() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in bootloader", getSerialNumber());
mRecovery.recoverDeviceBootloader(mStateMonitor);
CLog.i("Bootloader recovery successful for %s", getSerialNumber());
}
private void recoverDeviceFromFastbootd() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in fastbootd", getSerialNumber());
mRecovery.recoverDeviceFastbootd(mStateMonitor);
CLog.i("Fastbootd recovery successful for %s", getSerialNumber());
}
private void recoverDeviceInRecovery() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in recovery", getSerialNumber());
mRecovery.recoverDeviceRecovery(mStateMonitor);
CLog.i("Recovery mode recovery successful for %s", getSerialNumber());
}
/**
* {@inheritDoc}
*/
@Override
public void startLogcat() {
if (mLogcatReceiver != null) {
CLog.d("Already capturing logcat for %s, ignoring", getSerialNumber());
return;
}
mLogcatReceiver = createLogcatReceiver();
mLogcatReceiver.start();
}
/**
* {@inheritDoc}
*/
@Override
public void clearLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.clear();
}
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("MustBeClosedChecker")
public InputStreamSource getLogcat() {
if (mLogcatReceiver == null) {
if (!(getIDevice() instanceof StubDevice)) {
TestDeviceState state = getDeviceState();
if (!TestDeviceState.ONLINE.equals(state)) {
CLog.w("Skipping logcat capture, no buffer and device state is '%s'", state);
} else {
CLog.w(
"Not capturing logcat for %s in background, returning a logcat dump",
getSerialNumber());
return getLogcatDump();
}
}
return new ByteArrayInputStreamSource(new byte[0]);
} else {
return mLogcatReceiver.getLogcatData();
}
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("MustBeClosedChecker")
public InputStreamSource getLogcat(int maxBytes) {
if (mLogcatReceiver == null) {
CLog.w("Not capturing logcat for %s in background, returning a logcat dump "
+ "ignoring size", getSerialNumber());
return getLogcatDump();
} else {
return mLogcatReceiver.getLogcatData(maxBytes);
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getLogcatSince(long date) {
int deviceApiLevel;
try {
deviceApiLevel = getApiLevel();
if (deviceApiLevel <= 22) {
CLog.i("Api level too low to use logcat -t 'time' reverting to dump");
return getLogcatDump();
}
} catch (DeviceNotAvailableException e) {
// For convenience of interface, we catch the DNAE here.
CLog.e(e);
return getLogcatDump();
}
String dateFormatted;
if (deviceApiLevel >= 24) {
// Use 'sssss.mmm' epoch time format supported since API 24.
dateFormatted = String.format(Locale.US, "%d.%03d", date / 1000, date % 1000);
} else {
// Convert date to format needed by the command:
// 'MM-DD HH:mm:ss.mmm' or 'YYYY-MM-DD HH:mm:ss.mmm'
SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
dateFormatted = format.format(new Date(date));
}
LargeOutputReceiver largeReceiver = null;
try {
// use IDevice directly because we don't want callers to handle
// DeviceNotAvailableException for this method
largeReceiver =
new LargeOutputReceiver(
"getLogcatSince",
getSerialNumber(),
getOptions().getMaxLogcatDataSize());
String command =
String.format(
"%s -t '%s'", LogcatReceiver.getDefaultLogcatCmd(this), dateFormatted);
getIDevice().executeShellCommand(command, largeReceiver);
return largeReceiver.getData();
} catch (IOException|AdbCommandRejectedException|
ShellCommandUnresponsiveException|TimeoutException e) {
CLog.w("Failed to get logcat dump from %s: %s", getSerialNumber(), e.getMessage());
CLog.e(e);
} finally {
if (largeReceiver != null) {
largeReceiver.cancel();
largeReceiver.delete();
}
}
return new ByteArrayInputStreamSource(new byte[0]);
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getLogcatDump() {
long startTime = System.currentTimeMillis();
LargeOutputReceiver largeReceiver = null;
try (CloseableTraceScope ignored = new CloseableTraceScope("getLogcatDump")) {
// use IDevice directly because we don't want callers to handle
// DeviceNotAvailableException for this method
largeReceiver =
new LargeOutputReceiver(
"getLogcatDump",
getSerialNumber(),
getOptions().getMaxLogcatDataSize());
// add -d parameter to make this a non blocking call
getIDevice()
.executeShellCommand(
LogcatReceiver.getDefaultLogcatCmd(this) + " -d",
largeReceiver,
LOGCAT_DUMP_TIMEOUT,
LOGCAT_DUMP_TIMEOUT,
TimeUnit.MILLISECONDS);
return largeReceiver.getData();
} catch (IOException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (TimeoutException e) {
CLog.w("Failed to get logcat dump from %s: timeout", getSerialNumber());
} catch (AdbCommandRejectedException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (ShellCommandUnresponsiveException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} finally {
if (largeReceiver != null) {
largeReceiver.cancel();
largeReceiver.delete();
}
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.LOGCAT_DUMP_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.LOGCAT_DUMP_COUNT, 1);
}
return new ByteArrayInputStreamSource(new byte[0]);
}
/**
* {@inheritDoc}
*/
@Override
public void stopLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.stop();
mLogcatReceiver = null;
} else {
CLog.w("Attempting to stop logcat when not capturing for %s", getSerialNumber());
}
}
/** Factory method to create a {@link LogcatReceiver}. */
@VisibleForTesting
LogcatReceiver createLogcatReceiver() {
String logcatOptions = mOptions.getLogcatOptions();
if (SystemUtil.isLocalMode()) {
mLogStartDelay = 0;
}
if (logcatOptions == null) {
return new LogcatReceiver(this, mOptions.getMaxLogcatDataSize(), mLogStartDelay);
} else {
return new LogcatReceiver(
this,
String.format("%s %s", LogcatReceiver.getDefaultLogcatCmd(this), logcatOptions),
mOptions.getMaxLogcatDataSize(),
mLogStartDelay);
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getBugreport() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean logBugreport(String dataName, ITestLogger listener) {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public Bugreport takeBugreport() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getBugreportz() {
return null;
}
/** {@inheritDoc} */
@Override
public boolean logAnrs(ITestLogger logger) throws DeviceNotAvailableException {
if (!doesFileExist(ANRS_PATH)) {
CLog.d("No ANRs at %s", ANRS_PATH);
return true;
}
boolean root = enableAdbRoot();
if (!root) {
CLog.d("Skipping logAnrs, need to be root.");
}
File localDir = null;
long startTime = System.currentTimeMillis();
try {
localDir = FileUtil.createTempDir("pulled-anrs");
boolean success = pullDir(ANRS_PATH, localDir);
if (!success) {
CLog.w("Failed to pull %s", ANRS_PATH);
return false;
}
if (localDir.listFiles().length == 0) {
return true;
}
for (File f : localDir.listFiles()) {
try (FileInputStreamSource source = new FileInputStreamSource(f)) {
String name = f.getName();
LogDataType type = LogDataType.ANRS;
if (name.startsWith("dumptrace")) {
type = LogDataType.DUMPTRACE;
}
logger.testLog(name, type, source);
}
}
} catch (IOException e) {
CLog.e(e);
return false;
} finally {
FileUtil.recursiveDelete(localDir);
}
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.ANR_TIME, System.currentTimeMillis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.ANR_COUNT, 1);
return true;
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot(String format, boolean rescale)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot(long displayId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/** {@inheritDoc} */
@Override
public void clearLastConnectedWifiNetwork() {
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
return connectToWifiNetwork(wifiSsid, wifiPsk, false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk, boolean scanSsid)
throws DeviceNotAvailableException {
LinkedHashMap<String, String> ssidToPsk = new LinkedHashMap<>();
ssidToPsk.put(wifiSsid, wifiPsk);
return connectToWifiNetwork(ssidToPsk, scanSsid);
}
/** {@inheritDoc}f */
@Override
public boolean connectToWifiNetwork(Map<String, String> wifiSsidToPsk)
throws DeviceNotAvailableException {
return connectToWifiNetwork(wifiSsidToPsk, false);
}
/** {@inheritDoc} */
@Override
public boolean connectToWifiNetwork(Map<String, String> wifiSsidToPsk, boolean scanSsid)
throws DeviceNotAvailableException {
// Clears the last connected wifi network.
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
// Connects to wifi network. It retries up to {@link TestDeviceOptions@getWifiAttempts()}
// times
Random rnd = new Random();
int backoffSlotCount = 2;
int slotTime = mOptions.getWifiRetryWaitTime();
int waitTime = 0;
long startTime = mClock.millis();
try (CloseableTraceScope ignored = new CloseableTraceScope("connectToWifiNetwork")) {
for (int i = 1; i <= mOptions.getWifiAttempts(); i++) {
boolean failedToEnableWifi = false;
for (Map.Entry<String, String> ssidToPsk : wifiSsidToPsk.entrySet()) {
String wifiSsid = ssidToPsk.getKey();
String wifiPsk = Strings.emptyToNull(ssidToPsk.getValue());
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.WIFI_CONNECT_RETRY_COUNT, i);
CLog.i("Connecting to wifi network %s on %s", wifiSsid, getSerialNumber());
IWifiHelper wifi = null;
if (!getOptions().useCmdWifiCommands()
|| !enableAdbRoot()
|| getApiLevel() < 31) {
wifi = createWifiHelper(false);
} else {
wifi = createWifiHelper(true);
}
WifiConnectionResult result =
wifi.connectToNetwork(
wifiSsid,
wifiPsk,
mOptions.getConnCheckUrl(),
scanSsid,
mOptions.getDefaultNetworkType());
final Map<String, String> wifiInfo = wifi.getWifiInfo();
if (WifiConnectionResult.SUCCESS.equals(result)) {
CLog.i(
"Successfully connected to wifi network %s(%s) on %s",
wifiSsid, wifiInfo.get("bssid"), getSerialNumber());
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.WIFI_AP_NAME, wifiSsid);
mLastConnectedWifiSsid = wifiSsid;
mLastConnectedWifiPsk = wifiPsk;
return true;
} else if (WifiConnectionResult.FAILED_TO_ENABLE.equals(result)) {
CLog.w("Failed to enable wifi");
failedToEnableWifi = true;
} else {
failedToEnableWifi = false;
CLog.w(
"Failed to connect to wifi network %s(%s) on %s on attempt %d of"
+ " %d",
wifiSsid,
wifiInfo.get("bssid"),
getSerialNumber(),
i,
mOptions.getWifiAttempts());
}
}
if (mClock.millis() - startTime >= mOptions.getMaxWifiConnectTime()) {
CLog.e(
"Failed to connect to wifi after %d ms. Aborting.",
mOptions.getMaxWifiConnectTime());
break;
}
// Do not sleep for the last iteration and when failed to enable wifi.
if (i < mOptions.getWifiAttempts() && !failedToEnableWifi) {
if (mOptions.isWifiExpoRetryEnabled()) {
// use binary exponential back-offs when retrying.
waitTime = rnd.nextInt(backoffSlotCount) * slotTime;
backoffSlotCount *= 2;
}
CLog.e(
"Waiting for %d ms before reconnecting to %s...",
waitTime, wifiSsidToPsk.keySet().toString());
getRunUtil().sleep(waitTime);
}
}
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.WIFI_CONNECT_TIME, mClock.millis() - startTime);
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_CONNECT_COUNT, 1);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean checkConnectivity() throws DeviceNotAvailableException {
IWifiHelper wifi = createWifiHelper();
return wifi.checkConnectivity(mOptions.getConnCheckUrl());
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetworkIfNeeded(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
return connectToWifiNetworkIfNeeded(wifiSsid, wifiPsk, false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetworkIfNeeded(String wifiSsid, String wifiPsk, boolean scanSsid)
throws DeviceNotAvailableException {
if (!checkConnectivity()) {
return connectToWifiNetwork(wifiSsid, wifiPsk, scanSsid);
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWifiEnabled() throws DeviceNotAvailableException {
final IWifiHelper wifi = createWifiHelper();
try {
return wifi.isWifiEnabled();
} catch (RuntimeException e) {
CLog.w("Failed to create WifiHelper: %s", e.getMessage());
return false;
}
}
/**
* Checks that the device is currently successfully connected to given wifi SSID.
*
* @param wifiSSID the wifi ssid
* @return <code>true</code> if device is currently connected to wifiSSID and has network
* connectivity. <code>false</code> otherwise
* @throws DeviceNotAvailableException if connection with device was lost
*/
boolean checkWifiConnection(String wifiSSID) throws DeviceNotAvailableException {
CLog.i("Checking connection with wifi network %s on %s", wifiSSID, getSerialNumber());
final IWifiHelper wifi = createWifiHelper();
// getSSID returns SSID as "SSID"
final String quotedSSID = String.format("\"%s\"", wifiSSID);
boolean test = wifi.isWifiEnabled();
CLog.v("%s: wifi enabled? %b", getSerialNumber(), test);
if (test) {
final String actualSSID = wifi.getSSID();
test = quotedSSID.equals(actualSSID);
CLog.v("%s: SSID match (%s, %s, %b)", getSerialNumber(), quotedSSID, actualSSID, test);
}
if (test) {
test = wifi.hasValidIp();
CLog.v("%s: validIP? %b", getSerialNumber(), test);
}
if (test) {
test = checkConnectivity();
CLog.v("%s: checkConnectivity returned %b", getSerialNumber(), test);
}
return test;
}
/**
* {@inheritDoc}
*/
@Override
public boolean disconnectFromWifi() throws DeviceNotAvailableException {
CLog.i("Disconnecting from wifi on %s", getSerialNumber());
// Clears the last connected wifi network.
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
IWifiHelper wifi = createWifiHelper();
return wifi.disconnectFromNetwork();
}
/**
* {@inheritDoc}
*/
@Override
public String getIpAddress() throws DeviceNotAvailableException {
IWifiHelper wifi = createWifiHelper();
return wifi.getIpAddress();
}
/**
* {@inheritDoc}
*/
@Override
public boolean enableNetworkMonitor() throws DeviceNotAvailableException {
mNetworkMonitorEnabled = false;
IWifiHelper wifi = createWifiHelper();
wifi.stopMonitor();
if (wifi.startMonitor(NETWORK_MONITOR_INTERVAL, mOptions.getConnCheckUrl())) {
mNetworkMonitorEnabled = true;
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean disableNetworkMonitor() throws DeviceNotAvailableException {
mNetworkMonitorEnabled = false;
IWifiHelper wifi = createWifiHelper();
List<Long> samples = wifi.stopMonitor();
if (!samples.isEmpty()) {
int failures = 0;
long totalLatency = 0;
for (Long sample : samples) {
if (sample < 0) {
failures += 1;
} else {
totalLatency += sample;
}
}
double failureRate = failures * 100.0 / samples.size();
double avgLatency = 0.0;
if (failures < samples.size()) {
avgLatency = totalLatency / (samples.size() - failures);
}
CLog.d("[metric] url=%s, window=%ss, failure_rate=%.2f%%, latency_avg=%.2f",
mOptions.getConnCheckUrl(), samples.size() * NETWORK_MONITOR_INTERVAL / 1000,
failureRate, avgLatency);
}
return true;
}
/**
* Create a {@link WifiHelper} to use
*
* <p>
*
* @throws DeviceNotAvailableException
*/
@VisibleForTesting
IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
// current wifi helper won't work on AndroidNativeDevice
// TODO: create a new Wifi helper with supported feature of AndroidNativeDevice when
// we learn what is available.
throw new UnsupportedOperationException("Wifi helper is not supported.");
}
/**
* Create a {@link WifiHelper} to use
*
* @param useV2 Whether to use WifiHelper v2 which does not install any apk.
* <p>
* @throws DeviceNotAvailableException
*/
@VisibleForTesting
IWifiHelper createWifiHelper(boolean useV2) throws DeviceNotAvailableException {
// current wifi helper won't work on AndroidNativeDevice
// TODO: create a new Wifi helper with supported feature of AndroidNativeDevice when
// we learn what is available.
throw new UnsupportedOperationException("Wifi helper is not supported.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean clearErrorDialogs() throws DeviceNotAvailableException {
CLog.e("No support for Screen's features");
return false;
}
/** {@inheritDoc} */
@Override
public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for keyguard querying.");
}
IDeviceStateMonitor getDeviceStateMonitor() {
return mStateMonitor;
}
/** {@inheritDoc} */
@Override
public void postBootSetup() throws DeviceNotAvailableException {
if (getOptions().shouldDisableReboot()) {
return;
}
getConnection().reconnect(getSerialNumber());
CLog.d("postBootSetup started");
long startTime = System.currentTimeMillis();
try (CloseableTraceScope ignored = new CloseableTraceScope("postBootSetup")) {
enableAdbRoot();
prePostBootSetup();
for (String command : mOptions.getPostBootCommands()) {
long start = System.currentTimeMillis();
try (CloseableTraceScope cmdTrace = new CloseableTraceScope(command)) {
executeShellCommand(command);
}
if (command.startsWith("sleep")) {
InvocationMetricLogger.addInvocationPairMetrics(
InvocationMetricKey.host_sleep, start, System.currentTimeMillis());
}
}
} finally {
long elapsed = System.currentTimeMillis() - startTime;
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.POSTBOOT_SETUP_TIME, elapsed);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.POSTBOOT_SETUP_COUNT, 1);
CLog.d("postBootSetup done: %s", TimeUtil.formatElapsedTime(elapsed));
}
}
/**
* Allows each device type (AndroidNativeDevice, TestDevice) to override this method for
* specific post boot setup.
*
* @throws DeviceNotAvailableException
*/
protected void prePostBootSetup() throws DeviceNotAvailableException {
// Empty on purpose.
}
/**
* Ensure wifi connection is re-established after boot. This is intended to be called after TF
* initiated reboots(ones triggered by {@link #reboot()}) only.
*
* @throws DeviceNotAvailableException
*/
void postBootWifiSetup() throws DeviceNotAvailableException {
CLog.d("postBootWifiSetup started");
long startTime = System.currentTimeMillis();
try (CloseableTraceScope ignored = new CloseableTraceScope("postBootWifiSetup")) {
if (mLastConnectedWifiSsid != null) {
reconnectToWifiNetwork();
}
if (mNetworkMonitorEnabled) {
if (!enableNetworkMonitor()) {
CLog.w(
"Failed to enable network monitor on %s after reboot",
getSerialNumber());
}
}
} finally {
long elapsed = System.currentTimeMillis() - startTime;
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.POSTBOOT_WIFI_SETUP_TIME, elapsed);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.POSTBOOT_WIFI_SETUP_COUNT, 1);
CLog.d("postBootWifiSetup done: %s", TimeUtil.formatElapsedTime(elapsed));
}
}
void reconnectToWifiNetwork() throws DeviceNotAvailableException {
// First, wait for wifi to re-connect automatically.
long startTime = System.currentTimeMillis();
boolean isConnected = checkConnectivity();
while (!isConnected && (System.currentTimeMillis() - startTime) < WIFI_RECONNECT_TIMEOUT) {
getRunUtil().sleep(WIFI_RECONNECT_CHECK_INTERVAL);
isConnected = checkConnectivity();
}
if (isConnected) {
return;
}
// If wifi is still not connected, try to re-connect on our own.
final String wifiSsid = mLastConnectedWifiSsid;
if (!connectToWifiNetworkIfNeeded(mLastConnectedWifiSsid, mLastConnectedWifiPsk)) {
throw new NetworkNotAvailableException(
String.format("Failed to connect to wifi network %s on %s after reboot",
wifiSsid, getSerialNumber()));
}
}
/**
* {@inheritDoc}
*/
@Override
public void rebootIntoBootloader()
throws DeviceNotAvailableException, UnsupportedOperationException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot into Bootloader");
return;
}
rebootIntoFastbootInternal(true);
}
/** {@inheritDoc} */
@Override
public void rebootIntoFastbootd()
throws DeviceNotAvailableException, UnsupportedOperationException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot into Fastbootd");
return;
}
rebootIntoFastbootInternal(false);
}
/**
* Reboots the device into bootloader or fastbootd mode.
*
* @param isBootloader true to boot the device into bootloader mode, false to boot the device
* into fastbootd mode.
* @throws DeviceNotAvailableException if connection with device is lost and cannot be
* recovered.
*/
private void rebootIntoFastbootInternal(boolean isBootloader)
throws DeviceNotAvailableException {
invalidatePropertyCache();
final RebootMode mode =
isBootloader ? RebootMode.REBOOT_INTO_BOOTLOADER : RebootMode.REBOOT_INTO_FASTBOOTD;
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(
String.format("Fastboot is not available and cannot reboot into %s", mode));
}
// Force wait for snapuserd in progress just to be sure
waitForSnapuserd(SnapuserdWaitPhase.BLOCK_BEFORE_RELEASING);
long startTime = System.currentTimeMillis();
try (CloseableTraceScope ignored =
new CloseableTraceScope("reboot_in_" + mode.toString())) {
// Update fastboot serial number before entering fastboot mode
mStateMonitor.setFastbootSerialNumber(getFastbootSerialNumber());
// If we go to bootloader, it's probably for flashing so ensure we re-check the provider
mShouldSkipContentProviderSetup = false;
CLog.i(
"Rebooting device %s in state %s into %s",
getSerialNumber(), getDeviceState(), mode);
if (isStateBootloaderOrFastbootd()) {
CLog.i(
"device %s already in %s. Rebooting anyway",
getSerialNumber(), getDeviceState());
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BOOTLOADER_SAME_STATE_REBOOT, 1);
executeFastbootCommand(String.format("reboot-%s", mode));
} else {
CLog.i("Booting device %s into %s", getSerialNumber(), mode);
doAdbReboot(mode, null);
}
// We want to wait on a command that verifies we've rebooted.
// However, it is possible to issue this command too quickly and get
// a response before the device has begun the reboot process (see
// b/242200753).
// While not as clean as we'd like, we wait 1.5 seconds before
// issuing any waiting commands, as devices generally take much
// longer than 1.5 seconds to reboot anyway.
getRunUtil().sleep(1500);
if (RebootMode.REBOOT_INTO_FASTBOOTD.equals(mode)
&& getHostOptions().isFastbootdEnable()) {
if (!mStateMonitor.waitForDeviceFastbootd(
getFastbootPath(), mOptions.getFastbootTimeout())) {
recoverDeviceFromFastbootd();
}
} else {
waitForDeviceBootloader();
}
} finally {
long elapsedTime = System.currentTimeMillis() - startTime;
if (RebootMode.REBOOT_INTO_FASTBOOTD.equals(mode)) {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.FASTBOOTD_REBOOT_TIME, elapsedTime);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.FASTBOOTD_REBOOT_COUNT, 1);
} else {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BOOTLOADER_REBOOT_TIME, elapsedTime);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.BOOTLOADER_REBOOT_COUNT, 1);
}
}
}
/** {@inheritDoc} */
@Override
public boolean isStateBootloaderOrFastbootd() {
return TestDeviceState.FASTBOOT.equals(getDeviceState())
|| TestDeviceState.FASTBOOTD.equals(getDeviceState());
}
/**
* {@inheritDoc}
*/
@Override
public void reboot() throws DeviceNotAvailableException {
reboot(null);
}
/** {@inheritDoc} */
@Override
public void reboot(@Nullable String reason) throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d("'%s' action is disabled during reboot callback. Ignoring.", "Reboot");
return;
}
internalRebootUntilOnline(reason);
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
setRecoveryMode(cachedRecoveryMode);
try (CloseableTraceScope ignored =
new CloseableTraceScope("reboot_waitForDeviceAvailable")) {
waitForDeviceAvailable(mOptions.getRebootTimeout());
}
postBootSetup();
postBootWifiSetup();
// notify of reboot end here. Full reboots will end here as well as reboots from Bootloader
// or Fastboot mode.
notifyRebootEnded();
}
@Override
public void rebootUserspace() throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d("'%s' action is disabled during reboot callback. Ignoring.", "Reboot Userspace");
return;
}
rebootUserspaceUntilOnline();
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (isEncryptionSupported()) {
if (isDeviceEncrypted()) {
CLog.e("Device is encrypted after userspace reboot!");
unlockDevice();
}
}
setRecoveryMode(cachedRecoveryMode);
waitForDeviceAvailable(mOptions.getRebootTimeout());
postBootSetup();
postBootWifiSetup();
}
@Override
public void rebootUntilOnline() throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot Until Online");
return;
}
try {
internalRebootUntilOnline(null);
} finally {
if (!mDeviceActionReceivers.isEmpty()) {
CLog.d(
"DeviceActionReceivers were not notified after rebootUntilOnline on %s.",
getSerialNumber());
}
}
}
/** {@inheritDoc} */
@Override
public void rebootUntilOnline(@Nullable String reason) throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot Until Online");
return;
}
try {
internalRebootUntilOnline(reason);
} finally {
if (!mDeviceActionReceivers.isEmpty()) {
CLog.d(
"DeviceActionReceivers were not notified after rebootUntilOnline on %s.",
getSerialNumber());
}
}
}
private void internalRebootUntilOnline(@Nullable String reason)
throws DeviceNotAvailableException {
long rebootStart = System.currentTimeMillis();
try (CloseableTraceScope ignored = new CloseableTraceScope("rebootUntilOnline")) {
// Invalidate cache before reboots
mPropertiesCache.invalidateAll();
doReboot(RebootMode.REBOOT_FULL, reason);
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
waitForDeviceOnline();
enableAdbRoot();
setRecoveryMode(cachedRecoveryMode);
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.ADB_REBOOT_TIME, System.currentTimeMillis() - rebootStart);
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.ADB_REBOOT_ROUTINE_COUNT, 1);
}
}
@Override
public void rebootUserspaceUntilOnline() throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot Userspace Until Online");
return;
}
doReboot(RebootMode.REBOOT_USERSPACE, null);
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
waitForDeviceOnline();
enableAdbRoot();
setRecoveryMode(cachedRecoveryMode);
}
/**
* {@inheritDoc}
*/
@Override
public void rebootIntoRecovery() throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot into Recovery");
return;
}
if (isStateBootloaderOrFastbootd()) {
CLog.w("device %s in fastboot when requesting boot to recovery. " +
"Rebooting to userspace first.", getSerialNumber());
internalRebootUntilOnline(null);
}
doAdbReboot(RebootMode.REBOOT_INTO_RECOVERY, null);
if (!waitForDeviceInRecovery(mOptions.getAdbRecoveryTimeout())) {
recoverDeviceInRecovery();
}
}
/** {@inheritDoc} */
@Override
public void rebootIntoSideload() throws DeviceNotAvailableException {
rebootIntoSideload(false);
}
/** {@inheritDoc} */
@Override
public void rebootIntoSideload(boolean autoReboot) throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Reboot into Sideload");
return;
}
if (isStateBootloaderOrFastbootd()) {
CLog.w(
"device %s in fastboot when requesting boot to sideload. "
+ "Rebooting to userspace first.",
getSerialNumber());
internalRebootUntilOnline(null);
}
final RebootMode rebootMode;
if (autoReboot) {
rebootMode = RebootMode.REBOOT_INTO_SIDELOAD_AUTO_REBOOT;
} else {
rebootMode = RebootMode.REBOOT_INTO_SIDELOAD;
}
doAdbReboot(rebootMode, null);
if (!waitForDeviceInSideload(mOptions.getAdbRecoveryTimeout())) {
// using recovery mode because sideload is a sub-mode under recovery
recoverDeviceInRecovery();
}
}
/**
* {@inheritDoc}
*/
@Override
public void nonBlockingReboot() throws DeviceNotAvailableException {
if (isInRebootCallback()) {
CLog.d(
"'%s' action is disabled during reboot callback. Ignoring.",
"Non Blocking Reboot");
return;
}
try {
doReboot(RebootMode.REBOOT_FULL, null);
} finally {
if (!mDeviceActionReceivers.isEmpty()) {
CLog.d(
"DeviceActionReceivers were not notified after nonBlockingReboot on %s.",
getSerialNumber());
}
}
}
/**
* A mode of a reboot.
*
* <p>Source of truth for available modes is defined in init.
*/
@VisibleForTesting
protected enum RebootMode {
REBOOT_FULL(""),
REBOOT_USERSPACE("userspace"),
REBOOT_INTO_FASTBOOTD("fastboot"),
REBOOT_INTO_BOOTLOADER("bootloader"),
REBOOT_INTO_SIDELOAD("sideload"),
REBOOT_INTO_SIDELOAD_AUTO_REBOOT("sideload-auto-reboot"),
REBOOT_INTO_RECOVERY("recovery");
private final String mRebootTarget;
RebootMode(String rebootTarget) {
mRebootTarget = rebootTarget;
}
@Nullable
String formatRebootCommand(@Nullable String reason) {
if (this == REBOOT_FULL) {
return Strings.isNullOrEmpty(reason) ? null : reason;
} else {
return Strings.isNullOrEmpty(reason) ? mRebootTarget : mRebootTarget + "," + reason;
}
}
@Override
public String toString() {
return mRebootTarget;
}
}
/**
* Trigger a reboot of the device, offers no guarantee of the device state after the call.
*
* @param rebootMode a mode of this reboot
* @param reason reason for this reboot
* @throws DeviceNotAvailableException
* @throws UnsupportedOperationException
*/
@VisibleForTesting
void doReboot(RebootMode rebootMode, @Nullable final String reason)
throws DeviceNotAvailableException, UnsupportedOperationException {
// Track Tradefed reboot time
mLastTradefedRebootTime = System.currentTimeMillis();
if (isStateBootloaderOrFastbootd()) {
CLog.i("device %s in %s. Rebooting to userspace.", getSerialNumber(), getDeviceState());
executeFastbootCommand("reboot");
} else {
if (mOptions.shouldDisableReboot()) {
CLog.i("Device reboot disabled by options, skipped.");
return;
}
if (reason == null) {
CLog.i("Rebooting device %s mode: %s", getSerialNumber(), rebootMode.name());
} else {
CLog.i(
"Rebooting device %s mode: %s reason: %s",
getSerialNumber(), rebootMode.name(), reason);
}
doAdbReboot(rebootMode, reason);
postAdbReboot();
}
}
/**
* Possible extra actions that can be taken after a reboot.
*
* @throws DeviceNotAvailableException
*/
protected void postAdbReboot() throws DeviceNotAvailableException {
// Check if device shows as unavailable (as expected after reboot).
boolean notAvailable = waitForDeviceNotAvailable(DEFAULT_UNAVAILABLE_TIMEOUT);
if (!notAvailable) {
CLog.w("Did not detect device %s becoming unavailable after reboot", getSerialNumber());
}
getConnection().reconnect(getSerialNumber());
}
/**
* Perform a adb reboot.
*
* @param rebootMode a mode of this reboot.
* @param reason for this reboot.
* @throws DeviceNotAvailableException
*/
protected void doAdbReboot(RebootMode rebootMode, @Nullable final String reason)
throws DeviceNotAvailableException {
getConnection().notifyAdbRebootCalled();
DeviceAction rebootAction = createRebootDeviceAction(rebootMode, reason);
performDeviceAction("reboot", rebootAction, MAX_RETRY_ATTEMPTS);
}
/**
* Create a {@link RebootDeviceAction} to be used when performing a reboot action.
*
* @param rebootMode a mode of this reboot.
* @param reason for this reboot.
* @return the created {@link RebootDeviceAction}.
*/
protected RebootDeviceAction createRebootDeviceAction(
RebootMode rebootMode, @Nullable final String reason) {
return new RebootDeviceAction(rebootMode, reason);
}
/**
* Wait to see the device going unavailable (stop reporting to adb).
*
* @param operationDesc The name of the operation that is waiting for unavailable.
* @param time The time to wait for unavailable to occur.
* @return True if device did become unavailable.
*/
protected boolean waitForDeviceNotAvailable(String operationDesc, long time) {
// TODO: a bit of a race condition here. Would be better to start a
// before the operation
if (!mStateMonitor.waitForDeviceNotAvailable(time)) {
// above check is flaky, ignore till better solution is found
CLog.w("Did not detect device %s becoming unavailable after %s", getSerialNumber(),
operationDesc);
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceNotAvailable(long waitTime) {
return mStateMonitor.waitForDeviceNotAvailable(waitTime);
}
/**
* {@inheritDoc}
*/
@Override
public boolean enableAdbRoot() throws DeviceNotAvailableException {
// adb root is a relatively intensive command, so do a brief check first to see
// if its necessary or not
if (isAdbRoot()) {
CLog.i("adb is already running as root on %s", getSerialNumber());
// Still check for online, in some case we could see the root, but device could be
// very early in its cycle.
waitForDeviceOnline();
return true;
}
// Don't enable root if user requested no root
if (!isEnableAdbRoot()) {
CLog.i("\"enable-root\" set to false; ignoring 'adb root' request");
return false;
}
InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.ADB_ROOT_ROUTINE_COUNT, 1);
long startTime = System.currentTimeMillis();
try (CloseableTraceScope ignored = new CloseableTraceScope("adb_root")) {
CLog.i("adb root on device %s", getSerialNumber());
int attempts = MAX_RETRY_ATTEMPTS + 1;
for (int i = 1; i <= attempts; i++) {
String output = executeAdbCommand("root");
// wait for device to disappear from adb
boolean res =
waitForDeviceNotAvailable(
"root", getOptions().getAdbRootUnavailableTimeout());
if (!res && TestDeviceState.ONLINE.equals(getDeviceState())) {
if (isAdbRoot()) {
return true;
}
}
postAdbRootAction();
// wait for device to be back online
waitForDeviceOnline();
if (isAdbRoot()) {
return true;
}
CLog.w(
"'adb root' on %s unsuccessful on attempt %d of %d. Output: '%s'",
getSerialNumber(), i, attempts, output);
}
return false;
} finally {
InvocationMetricLogger.addInvocationMetrics(
InvocationMetricKey.ADB_ROOT_TIME, System.currentTimeMillis() - startTime);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean disableAdbRoot() throws DeviceNotAvailableException {
if (!isAdbRoot()) {
CLog.i("adb is already unroot on %s", getSerialNumber());
return true;
}
CLog.i("adb unroot on device %s", getSerialNumber());
int attempts = MAX_RETRY_ATTEMPTS + 1;
for (int i=1; i <= attempts; i++) {
String output = executeAdbCommand("unroot");
// wait for device to disappear from adb
waitForDeviceNotAvailable("unroot", 5 * 1000);
postAdbUnrootAction();
// wait for device to be back online
waitForDeviceOnline();
if (!isAdbRoot()) {
return true;
}
CLog.w("'adb unroot' on %s unsuccessful on attempt %d of %d. Output: '%s'",
getSerialNumber(), i, attempts, output);
}
return false;
}
/**
* Override if the device needs some specific actions to be taken after adb root and before the
* device is back online.
* Default implementation doesn't include any addition actions.
* adb root is not guaranteed to be enabled at this stage.
* @throws DeviceNotAvailableException
*/
public void postAdbRootAction() throws DeviceNotAvailableException {
getConnection().reconnect(getSerialNumber());
}
/**
* Override if the device needs some specific actions to be taken after adb unroot and before
* the device is back online.
* Default implementation doesn't include any additional actions.
* adb root is not guaranteed to be disabled at this stage.
* @throws DeviceNotAvailableException
*/
public void postAdbUnrootAction() throws DeviceNotAvailableException {
getConnection().reconnect(getSerialNumber());
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAdbRoot() throws DeviceNotAvailableException {
String output = executeShellCommand("id");
return output.contains("uid=0(root)");
}
/**
* {@inheritDoc}
*/
@Override
public boolean unlockDevice() throws DeviceNotAvailableException,
UnsupportedOperationException {
if (!isEncryptionSupported()) {
throw new UnsupportedOperationException(String.format("Can't unlock device %s: "
+ "encryption not supported", getSerialNumber()));
}
if (!isDeviceEncrypted()) {
CLog.d("Device %s is not encrypted, skipping", getSerialNumber());
return true;
}
String encryptionType = getProperty("ro.crypto.type");
if (!"block".equals(encryptionType)) {
CLog.d(
"Skipping unlockDevice since it's not encrypted. ro.crypto.type=%s",
encryptionType);
return true;
}
CLog.i("Unlocking device %s", getSerialNumber());
enableAdbRoot();
// FIXME: currently, vcd checkpw can return an empty string when it never should. Try 3
// times.
String output;
int i = 0;
do {
// Enter the password. Output will be:
// "200 [X] -1" if the password has already been entered correctly,
// "200 [X] 0" if the password is entered correctly,
// "200 [X] N" where N is any positive number if the password is incorrect,
// any other string if there is an error.
output = executeShellCommand(String.format("vdc cryptfs checkpw \"%s\"",
ENCRYPTION_PASSWORD)).trim();
if (output.startsWith("200 ") && output.endsWith(" -1")) {
return true;
}
if (!output.isEmpty() && !(output.startsWith("200 ") && output.endsWith(" 0"))) {
CLog.e("checkpw gave output '%s' while trying to unlock device %s",
output, getSerialNumber());
return false;
}
getRunUtil().sleep(500);
} while (output.isEmpty() && ++i < 3);
if (output.isEmpty()) {
CLog.e("checkpw gave no output while trying to unlock device %s");
}
// Restart the framework. Output will be:
// "200 [X] 0" if the user data partition can be mounted,
// "200 [X] -1" if the user data partition can not be mounted (no correct password given),
// any other string if there is an error.
output = executeShellCommand("vdc cryptfs restart").trim();
if (!(output.startsWith("200 ") && output.endsWith(" 0"))) {
CLog.e("restart gave output '%s' while trying to unlock device %s", output,
getSerialNumber());
return false;
}
waitForDeviceAvailable();
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDeviceEncrypted() throws DeviceNotAvailableException {
String output = getProperty("ro.crypto.state");
if (output == null && isEncryptionSupported()) {
CLog.w("Property ro.crypto.state is null on device %s", getSerialNumber());
}
if (output == null) {
return false;
}
return "encrypted".equals(output.trim());
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEncryptionSupported() throws DeviceNotAvailableException {
if (!isEnableAdbRoot()) {
CLog.i("root is required for encryption");
mIsEncryptionSupported = false;
return mIsEncryptionSupported;
}
if (mIsEncryptionSupported != null) {
return mIsEncryptionSupported.booleanValue();
}
enableAdbRoot();
String output = getProperty("ro.crypto.state");
if (output == null || "unsupported".equals(output.trim())) {
mIsEncryptionSupported = false;
return mIsEncryptionSupported;
}
mIsEncryptionSupported = true;
return mIsEncryptionSupported;
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline(long waitTime) throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceOnline(waitTime) == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline() throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceOnline() == null) {
recoverDevice();
}
}
/** {@inheritDoc} */
@Override
public boolean waitForDeviceAvailable(long waitTime) throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceAvailable(waitTime) == null) {
return recoverDevice();
}
return true;
}
/** {@inheritDoc} */
@Override
public boolean waitForDeviceAvailable() throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceAvailable() == null) {
return recoverDevice();
}
return true;
}
/** {@inheritDoc} */
@Override
public boolean waitForDeviceAvailableInRecoverPath(final long waitTime)
throws DeviceNotAvailableException {
return mStateMonitor.waitForDeviceAvailableInRecoverPath(waitTime) != null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceInRecovery(long waitTime) {
return mStateMonitor.waitForDeviceInRecovery(waitTime);
}
/** {@inheritDoc} */
@Override
public void waitForDeviceBootloader() throws DeviceNotAvailableException {
if (mOptions.useUpdatedBootloaderStatus()) {
CommandResult commandResult =
simpleFastbootCommand(
mOptions.getFastbootTimeout(),
buildFastbootCommand("getvar", "product"));
if (!CommandStatus.SUCCESS.equals(commandResult.getStatus())) {
CLog.e(
"Waiting for device in bootloader. Status: %s.\nstdout:%s\nstderr:%s",
commandResult.getStatus(),
commandResult.getStdout(),
commandResult.getStderr());
recoverDeviceFromBootloader();
} else {
setDeviceState(TestDeviceState.FASTBOOT);
}
} else {
if (!mStateMonitor.waitForDeviceBootloader(mOptions.getFastbootTimeout())) {
recoverDeviceFromBootloader();
}
}
}
/** {@inheritDoc} */
@Override
public boolean waitForDeviceInSideload(long waitTime) {
return mStateMonitor.waitForDeviceInSideload(waitTime);
}
/**
* Small helper function to throw an NPE if the passed arg is null. This should be used when
* some value will be stored and used later, in which case it'll avoid hard-to-trace
* asynchronous NullPointerExceptions by throwing the exception synchronously. This is not
* intended to be used where the NPE would be thrown synchronously -- just let the jvm take care
* of it in that case.
*/
private void throwIfNull(Object obj) {
if (obj == null) {
throw new NullPointerException();
}
}
/** Retrieve this device's recovery mechanism. */
@VisibleForTesting
IDeviceRecovery getRecovery() {
return mRecovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecovery(IDeviceRecovery recovery) {
throwIfNull(recovery);
mRecovery = recovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecoveryMode(RecoveryMode mode) {
throwIfNull(mRecoveryMode);
mRecoveryMode = mode;
}
/**
* {@inheritDoc}
*/
@Override
public RecoveryMode getRecoveryMode() {
return mRecoveryMode;
}
/**
* {@inheritDoc}
*/
@Override
public void setFastbootEnabled(boolean fastbootEnabled) {
mFastbootEnabled = fastbootEnabled;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isFastbootEnabled() {
return mFastbootEnabled;
}
/**
* {@inheritDoc}
*/
@Override
public void setFastbootPath(String fastbootPath) {
mFastbootPath = fastbootPath;
// ensure the device and its associated recovery use the same fastboot version.
mRecovery.setFastbootPath(fastbootPath);
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootPath() {
return mFastbootPath;
}
/** {@inheritDoc} */
@Override
public String getFastbootVersion() {
try {
CommandResult res = executeFastbootCommand("--version");
return res.getStdout().trim();
} catch (DeviceNotAvailableException e) {
// Ignored for host side request
}
return null;
}
/** {@inheritDoc} */
@Override
public String getFastbootSerialNumber() {
if (mFastbootSerialNumber != null) {
return mFastbootSerialNumber;
}
// Only devices which use TCP adb have different fastboot serial number because IPv6
// link-local address will be used in fastboot mode.
if (!isAdbTcp()) {
mFastbootSerialNumber = getSerialNumber();
CLog.i(
"Device %s's fastboot serial number is %s",
getSerialNumber(), mFastbootSerialNumber);
return mFastbootSerialNumber;
}
mFastbootSerialNumber = getSerialNumber();
byte[] macEui48Bytes;
try {
boolean adbRoot = isAdbRoot();
if (!adbRoot) {
enableAdbRoot();
}
macEui48Bytes = getEUI48MacAddressInBytes(ETHERNET_MAC_ADDRESS_COMMAND);
if (!adbRoot) {
disableAdbRoot();
}
} catch (DeviceNotAvailableException e) {
CLog.e("Device %s isn't available when get fastboot serial number", getSerialNumber());
CLog.e(e);
return getSerialNumber();
}
String net_interface = getHostOptions().getNetworkInterface();
if (net_interface == null || macEui48Bytes == null) {
CLog.i(
"Device %s's fastboot serial number is %s",
getSerialNumber(), mFastbootSerialNumber);
return mFastbootSerialNumber;
}
// Create a link-local Inet6Address from the MAC address. The EUI-48 MAC address
// is converted to an EUI-64 MAC address per RFC 4291. The resulting EUI-64 is
// used to construct a link-local IPv6 address per RFC 4862.
byte[] addr = new byte[16];
addr[0] = (byte) 0xfe;
addr[1] = (byte) 0x80;
addr[8] = (byte) (macEui48Bytes[0] ^ (byte) 0x02); // flip the link-local bit
addr[9] = macEui48Bytes[1];
addr[10] = macEui48Bytes[2];
addr[11] = (byte) 0xff;
addr[12] = (byte) 0xfe;
addr[13] = macEui48Bytes[3];
addr[14] = macEui48Bytes[4];
addr[15] = macEui48Bytes[5];
try {
String host_addr = Inet6Address.getByAddress(null, addr, 0).getHostAddress();
mFastbootSerialNumber = "tcp:" + host_addr.split("%")[0] + "%" + net_interface;
} catch (UnknownHostException e) {
CLog.w("Failed to get %s's IPv6 link-local address", getSerialNumber());
CLog.w(e);
}
CLog.i(
"Device %s's fastboot serial number is %s",
getSerialNumber(), mFastbootSerialNumber);
return mFastbootSerialNumber;
}
/**
* {@inheritDoc}
*/
@Override
public void setDeviceState(final TestDeviceState deviceState) {
if (!deviceState.equals(getDeviceState())) {
// disable state changes while fastboot lock is held, because issuing fastboot command
// will disrupt state
if (isStateBootloaderOrFastbootd() && mFastbootLock.isLocked()) {
return;
}
mState = deviceState;
if (!(getIDevice() instanceof StubDevice)) {
CLog.logAndDisplay(
LogLevel.DEBUG,
"Device %s state is now %s",
getSerialNumber(),
deviceState);
}
mStateMonitor.setState(deviceState);
}
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceState getDeviceState() {
return mState;
}
@Override
public boolean isAdbTcp() {
return mStateMonitor.isAdbTcp();
}
/**
* {@inheritDoc}
*/
@Override
public String switchToAdbTcp() throws DeviceNotAvailableException {
String ipAddress = getIpAddress();
if (ipAddress == null) {
CLog.e("connectToTcp failed: Device %s doesn't have an IP", getSerialNumber());
return null;
}
String port = "5555";
executeAdbCommand("tcpip", port);
// TODO: analyze result? wait for device offline?
return String.format("%s:%s", ipAddress, port);
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchToAdbUsb() throws DeviceNotAvailableException {
executeAdbCommand("usb");
// TODO: analyze result? wait for device offline?
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void setEmulatorProcess(Process p) {
mEmulatorProcess = p;
}
/**
* For emulator set {@link SizeLimitedOutputStream} to log output
* @param output to log the output
*/
public void setEmulatorOutputStream(SizeLimitedOutputStream output) {
mEmulatorOutput = output;
}
/**
* {@inheritDoc}
*/
@Override
public void stopEmulatorOutput() {
if (mEmulatorOutput != null) {
mEmulatorOutput.delete();
mEmulatorOutput = null;
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getEmulatorOutput() {
if (getIDevice().isEmulator()) {
if (mEmulatorOutput == null) {
CLog.w("Emulator output for %s was not captured in background",
getSerialNumber());
} else {
try {
return new SnapshotInputStreamSource(
"getEmulatorOutput", mEmulatorOutput.getData());
} catch (IOException e) {
CLog.e("Failed to get %s data.", getSerialNumber());
CLog.e(e);
}
}
}
return new ByteArrayInputStreamSource(new byte[0]);
}
/**
* {@inheritDoc}
*/
@Override
public Process getEmulatorProcess() {
return mEmulatorProcess;
}
/**
* @return <code>true</code> if adb root should be enabled on device
*/
public boolean isEnableAdbRoot() {
return mOptions.isEnableAdbRoot();
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/** {@inheritDoc} */
@Override
public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/** {@inheritDoc} */
@Override
public boolean isPackageInstalled(String packageName, String userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/** {@inheritDoc} */
@Override
public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/** {@inheritDoc} */
@Override
public List<PackageInfo> getAppPackageInfos() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/** {@inheritDoc} */
@Override
public Set<String> getMainlineModuleInfo() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceOptions getOptions() {
return mOptions;
}
/**
* {@inheritDoc}
*/
@Override
public int getApiLevel() throws DeviceNotAvailableException {
int apiLevel = UNKNOWN_API_LEVEL;
try {
String prop = getProperty(DeviceProperties.SDK_VERSION);
apiLevel = Integer.parseInt(prop);
} catch (NumberFormatException nfe) {
CLog.w(
"Unable to get API level from "
+ DeviceProperties.SDK_VERSION
+ ", falling back to UNKNOWN.",
nfe);
// ignore, return unknown instead
}
return apiLevel;
}
/** {@inheritDoc} */
@Override
public boolean checkApiLevelAgainstNextRelease(int strictMinLevel)
throws DeviceNotAvailableException {
int apiLevel = getApiLevel();
if (apiLevel > strictMinLevel) {
return true;
}
String codeName = getPropertyWithRecovery(DeviceProperties.BUILD_CODENAME, true);
if (codeName == null) {
throw new DeviceRuntimeException(
String.format(
"Failed to query property '%s'. device returned null.",
DeviceProperties.BUILD_CODENAME),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
codeName = codeName.trim();
// CUR_DEVELOPMENT_VERSION is the code used by Android for a pre-finalized SDK
if (strictMinLevel == CUR_DEVELOPMENT_VERSION && !"REL".equals(codeName)) {
return true;
}
apiLevel = apiLevel + ("REL".equals(codeName) ? 0 : 1);
if (strictMinLevel > apiLevel) {
return false;
}
return true;
}
protected int getApiLevelSafe() {
try {
return getApiLevel();
} catch (DeviceNotAvailableException e) {
CLog.e(e);
return UNKNOWN_API_LEVEL;
}
}
/** {@inheritDoc} */
@Override
public int getLaunchApiLevel() throws DeviceNotAvailableException {
try {
String prop = getProperty(DeviceProperties.FIRST_API_LEVEL);
return Integer.parseInt(prop);
} catch (NumberFormatException nfe) {
CLog.w(
"Unable to get first launch API level from "
+ DeviceProperties.FIRST_API_LEVEL
+ ", falling back to getApiLevel().",
nfe);
}
return getApiLevel();
}
@Override
public IDeviceStateMonitor getMonitor() {
return mStateMonitor;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceShell(long waitTime) {
return mStateMonitor.waitForDeviceShell(waitTime);
}
@Override
public DeviceAllocationState getAllocationState() {
return mAllocationState;
}
/**
* {@inheritDoc}
* <p>
* Process the DeviceEvent, which may or may not transition this device to a new allocation
* state.
* </p>
*/
@Override
public DeviceEventResponse handleAllocationEvent(DeviceEvent event) {
// keep track of whether state has actually changed or not
boolean stateChanged = false;
DeviceAllocationState newState;
DeviceAllocationState oldState = mAllocationState;
mAllocationStateLock.lock();
try {
// update oldState here, just in case in changed before we got lock
oldState = mAllocationState;
newState = mAllocationState.handleDeviceEvent(event);
if (oldState != newState) {
// state has changed! record this fact, and store the new state
stateChanged = true;
mAllocationState = newState;
}
} finally {
mAllocationStateLock.unlock();
}
if (stateChanged && mAllocationMonitor != null) {
// state has changed! Lets inform the allocation monitor listener
mAllocationMonitor.notifyDeviceStateChange(getSerialNumber(), oldState, newState);
}
return new DeviceEventResponse(newState, stateChanged);
}
/** {@inheritDoc} */
@Override
public long getDeviceTimeOffset(Date date) throws DeviceNotAvailableException {
long deviceTime = getDeviceDate();
if (date == null) {
date = new Date();
}
long offset = date.getTime() - deviceTime;
CLog.d("Time offset = %d ms", offset);
return offset;
}
/**
* {@inheritDoc}
*/
@Override
public void setDate(Date date) throws DeviceNotAvailableException {
if (date == null) {
date = new Date();
}
long timeOffset = getDeviceTimeOffset(date);
// no need to set date
if (Math.abs(timeOffset) <= MAX_HOST_DEVICE_TIME_OFFSET) {
return;
}
String dateString = null;
if (getApiLevel() < 23) {
// set date in epoch format
dateString = Long.toString(date.getTime() / 1000); //ms to s
} else {
// set date with POSIX like params
SimpleDateFormat sdf = new java.text.SimpleDateFormat(
"MMddHHmmyyyy.ss");
sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
dateString = sdf.format(date);
}
// best effort, no verification
// Use TZ= to default to UTC timezone (b/128353510 for background)
executeShellCommand("TZ=UTC date -u " + dateString);
}
/** {@inheritDoc} */
@Override
public long getDeviceDate() throws DeviceNotAvailableException {
String deviceTimeString = executeShellCommand("date +%s");
Long deviceTime = null;
try {
deviceTime = Long.valueOf(deviceTimeString.trim());
} catch (NumberFormatException nfe) {
CLog.i("Invalid device time: \"%s\", ignored.", nfe);
return 0;
}
// Convert from seconds to milliseconds
return deviceTime * 1000L;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
return mStateMonitor.waitForBootComplete(timeOut);
}
/**
* {@inheritDoc}
*/
@Override
public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMultiUserSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean canSwitchToHeadlessSystemUser() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isMainUserPermanentAdmin() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public int createUserNoThrow(String name) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name, boolean guest, boolean ephemeral)
throws DeviceNotAvailableException, IllegalStateException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public int createUser(String name, boolean guest, boolean ephemeral, boolean forTesting)
throws DeviceNotAvailableException, IllegalStateException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean startUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isVisibleBackgroundUsersSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported()
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void remountSystemWritable() throws DeviceNotAvailableException {
String verity = getProperty("partition.system.verified");
// have the property set (regardless state) implies verity is enabled, so we send adb
// command to disable verity
if (verity != null && !verity.isEmpty()) {
executeAdbCommand("disable-verity");
mPropertiesCache.invalidate("partition.system.verified");
reboot();
}
enableAdbRoot();
executeAdbCommand("remount");
waitForDeviceAvailable();
}
/** {@inheritDoc} */
@Override
public void remountVendorWritable() throws DeviceNotAvailableException {
String verity = getProperty("partition.vendor.verified");
// have the property set (regardless state) implies verity is enabled, so we send adb
// command to disable verity
if (verity != null && !verity.isEmpty()) {
executeAdbCommand("disable-verity");
mPropertiesCache.invalidate("partition.vendor.verified");
reboot();
}
enableAdbRoot();
executeAdbCommand("remount");
waitForDeviceAvailable();
}
/** {@inheritDoc} */
@Override
public void remountSystemReadOnly() throws DeviceNotAvailableException {
String verity = getProperty("partition.system.verified");
// have the property set (regardless state) implies verity is enabled, so we send adb
// command to disable verity
if (verity == null || verity.isEmpty()) {
executeAdbCommand("enable-verity");
reboot();
}
}
/** {@inheritDoc} */
@Override
public void remountVendorReadOnly() throws DeviceNotAvailableException {
String verity = getProperty("partition.vendor.verified");
// have the property set (regardless state) implies verity is enabled, so we send adb
// command to disable verity
if (verity == null || verity.isEmpty()) {
executeAdbCommand("enable-verity");
reboot();
}
}
/** {@inheritDoc} */
@Override
public Integer getPrimaryUserId() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public Integer getMainUserId() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** Used internally to fallback to non-user logic */
private int getCurrentUserCompatible() throws DeviceNotAvailableException {
try {
return getCurrentUser();
} catch (RuntimeException e) {
return 0;
}
}
/**
* {@inheritDoc}
*/
@Override
public int getCurrentUser() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isUserVisible(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public boolean isUserVisibleOnDisplay(int userId, int displayId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getUserFlags(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFeature(String feature) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support pm's features.");
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(String namespace, String key)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(int userId, String namespace, String key)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/** {@inheritDoc} */
@Override
public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(String namespace, String key, String value)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(int userId, String namespace, String key, String value)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildSigningKeys() throws DeviceNotAvailableException {
String buildTags = getProperty(DeviceProperties.BUILD_TAGS);
if (buildTags != null) {
String[] tags = buildTags.split(",");
for (String tag : tags) {
Matcher m = KEYS_PATTERN.matcher(tag);
if (m.matches()) {
return tag;
}
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getAndroidId(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean setDeviceOwner(String componentName, int userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean removeAdmin(String componentName, int userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public void removeOwners() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void disableKeyguard() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Window Manager's features");
}
/** {@inheritDoc} */
@Override
public String getDeviceClass() {
IDevice device = getIDevice();
if (device == null) {
CLog.w("No IDevice instance, cannot determine device class.");
return "";
}
return device.getClass().getSimpleName();
}
/** {@inheritDoc} */
@Override
public void preInvocationSetup(IBuildInfo info, MultiMap<String, String> attributes)
throws TargetSetupError, DeviceNotAvailableException {
// Default implementation
mContentProvider = null;
mShouldSkipContentProviderSetup = false;
try {
mExecuteShellCommandLogs =
FileUtil.createTempFile("TestDevice_ExecuteShellCommands", ".txt");
} catch (IOException e) {
throw new TargetSetupError(
"Failed to create the executeShellCommand log file.",
e,
getDeviceDescriptor(),
InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
}
initializeConnection(info, attributes);
}
protected void initializeConnection(IBuildInfo info, MultiMap<String, String> attributes)
throws DeviceNotAvailableException, TargetSetupError {
try (CloseableTraceScope ignored = new CloseableTraceScope("initializeConnection")) {
ConnectionBuilder builder =
new ConnectionBuilder(getRunUtil(), this, info, getLogger());
if (attributes != null) {
builder.addAttributes(attributes);
}
addExtraConnectionBuilderArgs(builder);
mConnection = DefaultConnection.createConnection(builder);
CLog.d("Using connection: %s (%s)", mConnection, getIDevice());
mConnection.initializeConnection();
}
}
protected void addExtraConnectionBuilderArgs(ConnectionBuilder builder) {
if (mConnectionAvd != null) {
builder.setExistingAvdInfo(mConnectionAvd);
}
}
public final void setConnectionAvdInfo(GceAvdInfo avdInfo) {
mConnectionAvd = avdInfo;
}
/** {@inheritDoc} */
@Override
public void postInvocationTearDown(Throwable exception) {
invalidatePropertyCache();
mConfiguration = null;
mIsEncryptionSupported = null;
FileUtil.deleteFile(mExecuteShellCommandLogs);
mExecuteShellCommandLogs = null;
FileUtil.recursiveDelete(mUnpackedFastbootDir);
getConnection().tearDownConnection();
mConnectionAvd = null;
mDeviceActionReceivers.clear();
// Default implementation
if (getIDevice() instanceof StubDevice) {
return;
}
// Reset the Content Provider bit.
mShouldSkipContentProviderSetup = false;
try {
// If we never installed it, don't even bother checking for it during tear down.
if (mContentProvider == null) {
return;
}
if (exception instanceof DeviceNotAvailableException) {
CLog.e(
"Skip Tradefed Content Provider teardown due to"
+ " DeviceNotAvailableException.");
return;
}
if (TestDeviceState.ONLINE.equals(getDeviceState())) {
mContentProvider.tearDown();
}
} catch (DeviceNotAvailableException e) {
CLog.e(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isHeadless() throws DeviceNotAvailableException {
if (getProperty(DeviceProperties.BUILD_HEADLESS) != null) {
return true;
}
return false;
}
protected void checkApiLevelAgainst(String feature, int strictMinLevel) {
try {
if (getApiLevel() < strictMinLevel){
throw new HarnessRuntimeException(
String.format(
"%s not supported on %s. " + "Must be API %d.",
feature, getSerialNumber(), strictMinLevel),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
} catch (DeviceNotAvailableException e) {
throw new HarnessRuntimeException(
"Device became unavailable while checking API level",
e,
DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
}
}
@Override
public DeviceDescriptor getCachedDeviceDescriptor() {
return getCachedDeviceDescriptor(false);
}
/** {@inheritDoc} */
@Override
public DeviceDescriptor getCachedDeviceDescriptor(boolean shortDescriptor) {
synchronized (mCacheLock) {
if (DeviceAllocationState.Allocated.equals(getAllocationState())) {
if (mCachedDeviceDescriptor == null) {
// Create the cache the very first time when it's allocated.
mCachedDeviceDescriptor = getDeviceDescriptor(false);
return mCachedDeviceDescriptor;
}
return mCachedDeviceDescriptor;
}
// If device is not allocated, just return current information
mCachedDeviceDescriptor = null;
return getDeviceDescriptor(shortDescriptor);
}
}
@Override
public DeviceDescriptor getDeviceDescriptor() {
return getDeviceDescriptor(false);
}
/** {@inheritDoc} */
@Override
public DeviceDescriptor getDeviceDescriptor(boolean shortDescriptor) {
IDeviceSelection selector = new DeviceSelectionOptions();
IDevice idevice = getIDevice();
try {
boolean isTemporary = false;
if (idevice instanceof NullDevice) {
isTemporary = ((NullDevice) idevice).isTemporary();
}
if (shortDescriptor) {
// Return only info that do not require device inspection
return new DeviceDescriptor(
idevice.getSerialNumber(),
null,
idevice instanceof StubDevice,
idevice.getState(),
getAllocationState(),
getDeviceState(),
null,
null,
null,
null,
null,
null,
getDeviceClass(),
null,
null,
null,
isTemporary,
null,
null,
idevice);
}
// All the operations to create the descriptor need to be safe (should not trigger any
// device side effects like recovery)
String sdkVersion = null;
String buildAlias = null;
String hardwareRev = null;
if (TestDeviceState.ONLINE.equals(getDeviceState())) {
sdkVersion = getPropertyWithRecovery(DeviceProperties.SDK_VERSION, false);
buildAlias = getPropertyWithRecovery(DeviceProperties.BUILD_ALIAS, false);
hardwareRev = getPropertyWithRecovery(DeviceProperties.HARDWARE_REVISION, false);
}
return new DeviceDescriptor(
idevice.getSerialNumber(),
null,
idevice instanceof StubDevice,
idevice.getState(),
getAllocationState(),
getDeviceState(),
getDisplayString(selector.getDeviceProductType(idevice)),
getDisplayString(selector.getDeviceProductVariant(idevice)),
getDisplayString(sdkVersion),
getDisplayString(buildAlias),
getDisplayString(hardwareRev),
getDisplayString(getBattery()),
getDeviceClass(),
getDisplayString(getMacAddress()),
getDisplayString(getSimState()),
getDisplayString(getSimOperator()),
isTemporary,
null,
null,
idevice);
} catch (RuntimeException|DeviceNotAvailableException e) {
CLog.e("Exception while building device '%s' description:", getSerialNumber());
CLog.e(e);
}
return null;
}
/**
* Return the displayable string for given object
*/
private String getDisplayString(Object o) {
return o == null ? "unknown" : o.toString();
}
/** {@inheritDoc} */
@Override
public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException {
String pidString = getProcessPid(processName);
if (pidString == null) {
return null;
}
long startTime = getProcessStartTimeByPid(pidString);
if (startTime == -1L) {
return null;
}
return new ProcessInfo(
getProcessUserByPid(pidString),
Integer.parseInt(pidString),
processName,
startTime);
}
/** Return the process start time since epoch for the given pid string */
private long getProcessStartTimeByPid(String pidString) throws DeviceNotAvailableException {
String output = executeShellCommand(String.format("ps -p %s -o stime=", pidString));
if (output != null && !output.trim().isEmpty()) {
output = output.trim();
String dateInSeconds;
// On API 28 and lower, there is a bug in toybox that prevents date from parsing
// timestamps containing a space, e.g. -D"%Y-%m-%d %H:%M:%S" cannot be used to parse
// the stime:19 output from ps. Instead, we'll reconstruct the timestamp.
if (getApiLevel() <= 28) {
dateInSeconds =
executeShellCommand(
"date -d \"$(date +%Y:%m:%d):"
+ output
+ "\" +%s -D \"%Y:%m:%d:%H:%M:%S\"");
} else {
dateInSeconds = executeShellCommand("date -d\"" + output + "\" +%s");
}
if (Strings.isNullOrEmpty(dateInSeconds)) {
return -1L;
}
try {
return Long.parseLong(dateInSeconds.trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse the start time for process:");
CLog.e(e);
return -1L;
}
}
return -1L;
}
/** Return the process user for the given pid string */
private String getProcessUserByPid(String pidString) throws DeviceNotAvailableException {
String output = executeShellCommand("stat -c%U /proc/" + pidString);
if (output != null && !output.trim().isEmpty()) {
try {
return output.trim();
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/** {@inheritDoc} */
@Override
public Map<Long, String> getBootHistory() throws DeviceNotAvailableException {
String output = getProperty(DeviceProperties.BOOT_REASON_HISTORY);
/* Sample output:
kernel_panic,1556587278
reboot,,1556238008
reboot,,1556237796
reboot,,1556237725
*/
Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
if (Strings.isNullOrEmpty(output)) {
return bootHistory;
}
for (String line : output.split("\\n")) {
String infoStr[] = line.split(",");
String startStr = infoStr[infoStr.length - 1];
try {
long startTime = Long.parseLong(startStr.trim());
bootHistory.put(startTime, infoStr[0].trim());
} catch (NumberFormatException e) {
CLog.e("Fail to parse boot time from line %s", line);
}
}
return bootHistory;
}
/** {@inheritDoc} */
@Override
public Map<Long, String> getBootHistorySince(long utcEpochTime, TimeUnit timeUnit)
throws DeviceNotAvailableException {
long utcEpochTimeSec = TimeUnit.SECONDS.convert(utcEpochTime, timeUnit);
Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
for (Map.Entry<Long, String> entry : getBootHistory().entrySet()) {
if (entry.getKey() >= utcEpochTimeSec) {
bootHistory.put(entry.getKey(), entry.getValue());
}
}
return bootHistory;
}
private boolean hasNormalRebootSince(long utcEpochTime, TimeUnit timeUnit)
throws DeviceNotAvailableException {
Map<Long, String> bootHistory = getBootHistorySince(utcEpochTime, timeUnit);
if (bootHistory.isEmpty()) {
CLog.w("There is no reboot history since %s", utcEpochTime);
return false;
}
CLog.i(
"There are new boot history since %d. NewBootHistory = %s",
utcEpochTime, bootHistory);
// Check if there is reboot reason other than "reboot".
// Raise RuntimeException if there is abnormal reboot.
for (Map.Entry<Long, String> entry : bootHistory.entrySet()) {
if (!"reboot".equals(entry.getValue())) {
throw new HarnessRuntimeException(
String.format(
"Device %s has abnormal reboot reason %s at %d",
getSerialNumber(), entry.getValue(), entry.getKey()),
DeviceErrorIdentifier.UNEXPECTED_REBOOT);
}
}
return true;
}
/**
* Check current system process is restarted after last reboot
*
* @param systemServerProcess the system_server {@link ProcessInfo}
* @return true if system_server process restarted after last reboot; false if not
* @throws DeviceNotAvailableException
*/
private boolean checkSystemProcessRestartedAfterLastReboot(ProcessInfo systemServerProcess)
throws DeviceNotAvailableException {
// If time gap from last reboot to current system_server process start time is more than
// MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP seconds, we conclude the system_server restarted
// after boot up.
if (!hasNormalRebootSince(
systemServerProcess.getStartTime() - MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC,
TimeUnit.SECONDS)) {
CLog.i(
"Device last reboot is more than %s seconds away from current system_server "
+ "process start time. The system_server process restarted after "
+ "last boot up",
MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC);
return true;
} else {
// Current system_server start within MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP
// seconds after device last boot up
return false;
}
}
/** {@inheritDoc} */
@Override
public boolean deviceSoftRestartedSince(long utcEpochTime, TimeUnit timeUnit)
throws DeviceNotAvailableException {
ProcessInfo currSystemServerProcess = getProcessByName("system_server");
if (currSystemServerProcess == null) {
CLog.i("The system_server process is not available on the device.");
return true;
}
// The system_server process started at or before utcEpochTime, there is no soft-restart
if (Math.abs(
currSystemServerProcess.getStartTime()
- TimeUnit.SECONDS.convert(utcEpochTime, timeUnit))
<= 1) {
return false;
}
// The system_server process restarted after device utcEpochTime in second.
// Check if there is new reboot history, if no new reboot, device soft-restarted.
// If there is no normal reboot, soft-restart is detected.
if (!hasNormalRebootSince(utcEpochTime, timeUnit)) {
return true;
}
// There is new reboot since utcEpochTime. Check if system_server restarted after boot up.
return checkSystemProcessRestartedAfterLastReboot(currSystemServerProcess);
}
/** {@inheritDoc} */
@Override
public boolean deviceSoftRestarted(ProcessInfo prevSystemServerProcess)
throws DeviceNotAvailableException {
if (prevSystemServerProcess == null) {
CLog.i("The given system_server process is null. Abort deviceSoftRestarted check.");
return false;
}
ProcessInfo currSystemServerProcess = getProcessByName("system_server");
if (currSystemServerProcess == null) {
CLog.i("The system_server process is not available on the device.");
return true;
}
// Compare the start time with a 1 seconds accuracy due to how the date is computed
if (currSystemServerProcess.getPid() == prevSystemServerProcess.getPid()
&& Math.abs(
currSystemServerProcess.getStartTime()
- prevSystemServerProcess.getStartTime())
<= 1) {
return false;
}
CLog.v(
"current system_server: %s; prev system_server: %s",
currSystemServerProcess, prevSystemServerProcess);
// The system_server process restarted.
// Check boot history with previous system_server start time.
// If there is no normal reboot, soft-restart is detected
if (!hasNormalRebootSince(prevSystemServerProcess.getStartTime(), TimeUnit.SECONDS)) {
return true;
}
// There is reboot since prevSystemServerProcess.getStartTime().
// Check if system_server restarted after boot up.
return checkSystemProcessRestartedAfterLastReboot(currSystemServerProcess);
}
/**
* Validates that the given input is a valid MAC address
*
* @param address input to validate
* @return true if the input is a valid MAC address
*/
boolean isMacAddress(String address) {
Pattern macPattern = Pattern.compile(MAC_ADDRESS_PATTERN);
Matcher macMatcher = macPattern.matcher(address);
return macMatcher.find();
}
/**
* Query Mac address from the device
*
* @param command the query command
* @return the MAC address of the device, null if it fails to query from the device
*/
private String getMacAddress(String command) {
if (getIDevice() instanceof StubDevice) {
// Do not query MAC addresses from stub devices.
return null;
}
if (!TestDeviceState.ONLINE.equals(mState)) {
// Only query MAC addresses from online devices.
return null;
}
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
try {
mIDevice.executeShellCommand(command, receiver);
} catch (IOException | TimeoutException | AdbCommandRejectedException |
ShellCommandUnresponsiveException e) {
CLog.w(
"Failed to query MAC address for %s by '%s'",
mIDevice.getSerialNumber(), command);
CLog.w(e);
}
String output = receiver.getOutput().trim();
if (isMacAddress(output)) {
return output;
}
CLog.d(
"No valid MAC address queried from device %s by '%s'",
mIDevice.getSerialNumber(), command);
return null;
}
/** {@inheritDoc} */
@Override
public String getMacAddress() {
return getMacAddress(MAC_ADDRESS_COMMAND);
}
/**
* Query EUI-48 MAC address from the device
*
* @param command the query command
* @return the EUI-48 MAC address in long, 0 if it fails to query from the device
* @throws IllegalArgumentException
*/
long getEUI48MacAddressInLong(String command) {
String addr = getMacAddress(command);
if (addr == null) {
return 0;
}
String[] parts = addr.split(":");
if (parts.length != ETHER_ADDR_LEN) {
throw new IllegalArgumentException(addr + " was not a valid MAC address");
}
long longAddr = 0;
for (int i = 0; i < parts.length; i++) {
int x = Integer.valueOf(parts[i], 16);
if (x < 0 || 0xff < x) {
throw new IllegalArgumentException(addr + "was not a valid MAC address");
}
longAddr = x + (longAddr << 8);
}
return longAddr;
}
/**
* Query EUI-48 MAC address from the device
*
* @param command the query command
* @return the EUI-48 MAC address in byte[], null if it fails to query from the device
* @throws IllegalArgumentException
*/
byte[] getEUI48MacAddressInBytes(String command) {
long addr = getEUI48MacAddressInLong(command);
if (addr == 0) {
return null;
}
byte[] bytes = new byte[ETHER_ADDR_LEN];
int index = ETHER_ADDR_LEN;
while (index-- > 0) {
bytes[index] = (byte) addr;
addr = addr >> 8;
}
return bytes;
}
/** {@inheritDoc} */
@Override
public String getSimState() {
if (getIDevice() instanceof StubDevice) {
// Do not query SIM state from stub devices.
return null;
}
if (!TestDeviceState.ONLINE.equals(mState)) {
// Only query SIM state from online devices.
return null;
}
try {
return getPropertyWithRecovery(SIM_STATE_PROP, false);
} catch (DeviceNotAvailableException dnae) {
CLog.w("DeviceNotAvailableException while fetching SIM state");
return null;
}
}
/** {@inheritDoc} */
@Override
public String getSimOperator() {
if (getIDevice() instanceof StubDevice) {
// Do not query SIM operator from stub devices.
return null;
}
if (!TestDeviceState.ONLINE.equals(mState)) {
// Only query SIM operator from online devices.
return null;
}
try {
return getPropertyWithRecovery(SIM_OPERATOR_PROP, false);
} catch (DeviceNotAvailableException dnae) {
CLog.w("DeviceNotAvailableException while fetching SIM operator");
return null;
}
}
/** {@inheritDoc} */
@Override
public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("dumpHeap is not supported.");
}
/** {@inheritDoc} */
@Override
public String getProcessPid(String process) throws DeviceNotAvailableException {
String output = executeShellCommand(String.format("pidof %s", process)).trim();
if (checkValidPid(output)) {
return output;
}
CLog.e("Failed to find a valid pid for process '%s'.", process);
return null;
}
/** {@inheritDoc} */
@Override
@FormatMethod
public void logOnDevice(String tag, LogLevel level, String format, Object... args) {
String message = String.format(format, args);
try {
String levelLetter = logLevelToLogcatLevel(level);
String command = String.format("log -t %s -p %s '%s'", tag, levelLetter, message);
executeShellCommand(command);
} catch (DeviceNotAvailableException e) {
CLog.e("Device went not available when attempting to log '%s'", message);
CLog.e(e);
}
}
/** Convert the {@link LogLevel} to the letter used in log (see 'adb shell log --help'). */
private String logLevelToLogcatLevel(LogLevel level) {
switch (level) {
case DEBUG:
return "d";
case ERROR:
return "e";
case INFO:
return "i";
case VERBOSE:
return "v";
case WARN:
return "w";
default:
return "i";
}
}
/** {@inheritDoc} */
@Override
public long getTotalMemory() {
// "/proc/meminfo" always returns value in kilobytes.
long totalMemory = 0;
String output = null;
try {
output = executeShellCommand("cat /proc/meminfo | grep MemTotal");
} catch (DeviceNotAvailableException e) {
CLog.e(e);
return -1;
}
if (output.isEmpty()) {
return -1;
}
String[] results = output.split("\\s+");
try {
totalMemory = Long.parseLong(results[1].replaceAll("\\D+", ""));
} catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
CLog.e(e);
return -1;
}
return totalMemory * 1024;
}
/** {@inheritDoc} */
@Override
public Integer getBattery() {
if (getIDevice() instanceof StubDevice) {
return null;
}
if (isStateBootloaderOrFastbootd()) {
return null;
}
try {
// Use default 5 minutes freshness
Future<Integer> batteryFuture = getIDevice().getBattery();
// Get cached value or wait up to 500ms for battery level query
return batteryFuture.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException
| ExecutionException
| java.util.concurrent.TimeoutException e) {
CLog.w(
"Failed to query battery level for %s: %s",
getIDevice().getSerialNumber(), e.toString());
}
return null;
}
/** {@inheritDoc} */
@Override
public Set<Long> listDisplayIds() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("dumpsys SurfaceFlinger is not supported.");
}
@Override
public Set<Integer> listDisplayIdsForStartingVisibleBackgroundUsers()
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public long getLastExpectedRebootTimeMillis() {
return mLastTradefedRebootTime;
}
/** {@inheritDoc} */
@Override
public List<File> getTombstones() throws DeviceNotAvailableException {
List<File> tombstones = new ArrayList<>();
if (!isAdbRoot()) {
CLog.w("Device was not root, cannot collect tombstones.");
return tombstones;
}
for (String tombName : getChildren(TOMBSTONE_PATH)) {
File tombFile = pullFile(TOMBSTONE_PATH + tombName);
if (tombFile != null) {
tombstones.add(tombFile);
}
}
return tombstones;
}
@Override
public Set<DeviceFoldableState> getFoldableStates() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for foldable states.");
}
@Override
public DeviceFoldableState getCurrentFoldableState() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for foldable states.");
}
/** Validate that pid is an integer and not empty. */
private boolean checkValidPid(String output) {
if (output.isEmpty()) {
return false;
}
try {
Integer.parseInt(output);
} catch (NumberFormatException e) {
CLog.e(e);
return false;
}
return true;
}
/** Gets the {@link IHostOptions} instance to use. */
@VisibleForTesting
IHostOptions getHostOptions() {
return GlobalConfiguration.getInstance().getHostOptions();
}
/** Returns the {@link ContentProviderHandler} or null if not available. */
@VisibleForTesting
ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
// If disabled at the device level, don't attempt any checks.
if (!getOptions().shouldUseContentProvider()) {
return null;
}
// Prevent usage of content provider before API 28 as it would not work well since content
// tool is not working before P.
if (getApiLevel() < 28) {
return null;
}
if (mContentProvider == null) {
mContentProvider = new ContentProviderHandler(this);
}
// Force the install if we saw an error with content provider installation.
if (mContentProvider.contentProviderNotFound()) {
mShouldSkipContentProviderSetup = false;
}
if (!mShouldSkipContentProviderSetup) {
boolean res = mContentProvider.setUp();
if (!res) {
// TODO: once CP becomes a requirement, throw/fail the test if CP can't be found
return null;
}
mShouldSkipContentProviderSetup = true;
}
return mContentProvider;
}
/** Reset the flag for content provider setup in order to trigger it again. */
public void resetContentProviderSetup() {
mShouldSkipContentProviderSetup = false;
}
/** The log that contains all the {@link #executeShellCommand(String)} logs. */
public final File getExecuteShellCommandLog() {
return mExecuteShellCommandLogs;
}
/** Executes a simple fastboot command and report the status of the command. */
@VisibleForTesting
protected CommandResult simpleFastbootCommand(final long timeout, String[] fullCmd)
throws UnsupportedOperationException {
return simpleFastbootCommand(timeout, new HashMap<>(), fullCmd);
}
/**
* Executes a simple fastboot command with environment variables and report the status of the
* command.
*/
@VisibleForTesting
protected CommandResult simpleFastbootCommand(
final long timeout, Map<String, String> envVarMap, String[] fullCmd)
throws UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(
String.format(
"Attempted to fastboot on device %s , but fastboot "
+ "is disabled. Aborting.",
getSerialNumber()));
}
IRunUtil runUtil;
if (!envVarMap.isEmpty()) {
runUtil = new RunUtil();
} else {
runUtil = getRunUtil();
}
for (Map.Entry<String, String> entry : envVarMap.entrySet()) {
CLog.v(
String.format(
"Set environment variable %s to %s", entry.getKey(), entry.getValue()));
runUtil.setEnvVariable(entry.getKey(), entry.getValue());
}
CommandResult result = new CommandResult(CommandStatus.EXCEPTION);
// block state changes while executing a fastboot command, since
// device will disappear from fastboot devices while command is being executed
mFastbootLock.lock();
try {
if (mOptions.getFastbootOutputTimeout() > 0) {
result =
runUtil.runTimedCmdWithOutputMonitor(
timeout, mOptions.getFastbootOutputTimeout(), fullCmd);
} else {
result = runUtil.runTimedCmd(timeout, fullCmd);
}
} finally {
mFastbootLock.unlock();
}
return result;
}
/** The current connection associated with the device. */
@Override
public AbstractConnection getConnection() {
if (mConnection == null) {
mConnection =
DefaultConnection.createInopConnection(
new ConnectionBuilder(getRunUtil(), this, null, getLogger()));
}
return mConnection;
}
/** Check if debugfs is mounted. */
@Override
public boolean isDebugfsMounted() throws DeviceNotAvailableException {
return CommandStatus.SUCCESS.equals(
executeShellV2Command(CHECK_DEBUGFS_MNT_COMMAND).getStatus());
}
/** Mount debugfs. */
@Override
public void mountDebugfs() throws DeviceNotAvailableException {
if (isDebugfsMounted()) {
CLog.w("debugfs already mounted.");
return;
}
CommandResult result = executeShellV2Command(MOUNT_DEBUGFS_COMMAND);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.e("Failed to mount debugfs. %s", result);
throw new DeviceRuntimeException(
"'" + MOUNT_DEBUGFS_COMMAND + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
}
/** Unmount debugfs. */
@Override
public void unmountDebugfs() throws DeviceNotAvailableException {
if (!isDebugfsMounted()) {
CLog.w("debugfs not mounted to unmount.");
return;
}
CommandResult result = executeShellV2Command(UNMOUNT_DEBUGFS_COMMAND);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.e("Failed to unmount debugfs. %s", result);
throw new DeviceRuntimeException(
"'" + UNMOUNT_DEBUGFS_COMMAND + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
}
/**
* Notifies all {@link IDeviceActionReceiver} about reboot start event.
*
* @throws DeviceNotAvailableException
*/
protected void notifyRebootStarted() throws DeviceNotAvailableException {
try (CloseableTraceScope ignored = new CloseableTraceScope("rebootStartedCallbacks")) {
for (IDeviceActionReceiver dar : mDeviceActionReceivers) {
try {
inRebootCallback = true;
dar.rebootStarted(this);
} catch (DeviceNotAvailableException dnae) {
inRebootCallback = false;
throw dnae;
} catch (Exception e) {
logDeviceActionException("notifyRebootStarted", e, true);
} finally {
inRebootCallback = false;
}
}
}
}
/**
* Notifies all {@link IDeviceActionReceiver} about reboot end event.
*
* @throws DeviceNotAvailableException
*/
protected void notifyRebootEnded() throws DeviceNotAvailableException {
try (CloseableTraceScope ignored = new CloseableTraceScope("rebootEndedCallbacks")) {
for (IDeviceActionReceiver dar : mDeviceActionReceivers) {
try {
inRebootCallback = true;
dar.rebootEnded(this);
} catch (DeviceNotAvailableException dnae) {
inRebootCallback = false;
throw dnae;
} catch (Exception e) {
logDeviceActionException("notifyRebootEnded", e, true);
} finally {
inRebootCallback = false;
}
}
}
}
/**
* Returns whether reboot callbacks is currently being executed or not. All public api's for
* reboot should be disabled if true.
*/
protected boolean isInRebootCallback() {
return inRebootCallback;
}
@Override
public void setTestLogger(ITestLogger testLogger) {
mTestLogger = testLogger;
}
protected ITestLogger getLogger() {
return mTestLogger;
}
/**
* Marks the TestDevice as microdroid and sets its CID.
*
* @param process Process of the Microdroid VM.
*/
protected void setMicrodroidProcess(Process process) {
mMicrodroidProcess = process;
}
/**
* @return Returns the Process of the Microdroid VM. If TestDevice is not a Microdroid, returns
* null.
*/
public Process getMicrodroidProcess() {
return mMicrodroidProcess;
}
protected void setTestDeviceOptions(Map<String, String> deviceOptions) {
try {
OptionSetter setter = new OptionSetter(this.getOptions());
for (Map.Entry<String, String> optionsKeyValue : deviceOptions.entrySet()) {
setter.setOptionValue(optionsKeyValue.getKey(), optionsKeyValue.getValue());
}
} catch (ConfigurationException e) {
CLog.w(e);
}
}
public void invalidatePropertyCache() {
mPropertiesCache.invalidateAll();
}
}