blob: 55d285d6b936000b2394dd61e35fc28d6b3abb57 [file] [log] [blame]
/*
* Copyright (C) 2007 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.ddmlib;
import com.android.annotations.concurrency.GuardedBy;
import com.android.ddmlib.log.LogReceiver;
import java.io.File;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A Device. It can be a physical device or an emulator.
*/
final class Device implements IDevice {
private static final int INSTALL_TIMEOUT = 2*60*1000; //2min
private static final int BATTERY_TIMEOUT = 2*1000; //2 seconds
private static final int GETPROP_TIMEOUT = 2*1000; //2 seconds
/** Emulator Serial Number regexp. */
static final String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$
/** Serial number of the device */
private String mSerialNumber = null;
/** Name of the AVD */
private String mAvdName = null;
/** State of the device. */
private DeviceState mState = null;
/** Device properties. */
private final Map<String, String> mProperties = new HashMap<String, String>();
private final Map<String, String> mMountPoints = new HashMap<String, String>();
@GuardedBy("mClients")
private final List<Client> mClients = new ArrayList<Client>();
/** Maps pid's of clients in {@link #mClients} to their package name. */
private final Map<Integer, String> mClientInfo = new ConcurrentHashMap<Integer, String>();
private DeviceMonitor mMonitor;
private static final String LOG_TAG = "Device";
private static final char SEPARATOR = '-';
private static final String UNKNOWN_PACKAGE = ""; //$NON-NLS-1$
/**
* Socket for the connection monitoring client connection/disconnection.
*/
private SocketChannel mSocketChannel;
private boolean mArePropertiesSet = false;
private Integer mLastBatteryLevel = null;
private long mLastBatteryCheckTime = 0;
private String mName;
/**
* Output receiver for "pm install package.apk" command line.
*/
private static final class InstallReceiver extends MultiLineReceiver {
private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$
private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$
private String mErrorMessage = null;
public InstallReceiver() {
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (!line.isEmpty()) {
if (line.startsWith(SUCCESS_OUTPUT)) {
mErrorMessage = null;
} else {
Matcher m = FAILURE_PATTERN.matcher(line);
if (m.matches()) {
mErrorMessage = m.group(1);
}
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
public String getErrorMessage() {
return mErrorMessage;
}
}
/**
* Output receiver for "dumpsys battery" command line.
*/
private static final class BatteryReceiver extends MultiLineReceiver {
private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)");
private static final Pattern SCALE = Pattern.compile("\\s*scale: (\\d+)");
private Integer mBatteryLevel = null;
private Integer mBatteryScale = null;
/**
* Get the parsed percent battery level.
* @return
*/
public Integer getBatteryLevel() {
if (mBatteryLevel != null && mBatteryScale != null) {
return (mBatteryLevel * 100) / mBatteryScale;
}
return null;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
if (batteryMatch.matches()) {
try {
mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
batteryMatch.group(1)));
}
}
Matcher scaleMatch = SCALE.matcher(line);
if (scaleMatch.matches()) {
try {
mBatteryScale = Integer.parseInt(scaleMatch.group(1));
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
batteryMatch.group(1)));
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getSerialNumber()
*/
@Override
public String getSerialNumber() {
return mSerialNumber;
}
@Override
public String getAvdName() {
return mAvdName;
}
/**
* Sets the name of the AVD
*/
void setAvdName(String avdName) {
if (!isEmulator()) {
throw new IllegalArgumentException(
"Cannot set the AVD name of the device is not an emulator");
}
mAvdName = avdName;
}
@Override
public String getName() {
if (mName != null) {
return mName;
}
if (isOnline()) {
// cache name only if device is online
mName = constructName();
return mName;
} else {
return constructName();
}
}
private String constructName() {
if (isEmulator()) {
String avdName = getAvdName();
if (avdName != null) {
return String.format("%s [%s]", avdName, getSerialNumber());
} else {
return getSerialNumber();
}
} else {
String manufacturer = null;
String model = null;
try {
manufacturer = cleanupStringForDisplay(
getPropertyCacheOrSync(PROP_DEVICE_MANUFACTURER));
model = cleanupStringForDisplay(
getPropertyCacheOrSync(PROP_DEVICE_MODEL));
} catch (Exception e) {
// If there are exceptions thrown while attempting to get these properties,
// we can just use the serial number, so ignore these exceptions.
}
StringBuilder sb = new StringBuilder(20);
if (manufacturer != null) {
sb.append(manufacturer);
sb.append(SEPARATOR);
}
if (model != null) {
sb.append(model);
sb.append(SEPARATOR);
}
sb.append(getSerialNumber());
return sb.toString();
}
}
private String cleanupStringForDisplay(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isLetterOrDigit(c)) {
sb.append(Character.toLowerCase(c));
} else {
sb.append('_');
}
}
return sb.toString();
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getState()
*/
@Override
public DeviceState getState() {
return mState;
}
/**
* Changes the state of the device.
*/
void setState(DeviceState state) {
mState = state;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getProperties()
*/
@Override
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(mProperties);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getPropertyCount()
*/
@Override
public int getPropertyCount() {
return mProperties.size();
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getProperty(java.lang.String)
*/
@Override
public String getProperty(String name) {
return mProperties.get(name);
}
@Override
public boolean arePropertiesSet() {
return mArePropertiesSet;
}
@Override
public String getPropertyCacheOrSync(String name) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
if (mArePropertiesSet) {
return getProperty(name);
} else {
return getPropertySync(name);
}
}
@Override
public String getPropertySync(String name) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
CountDownLatch latch = new CountDownLatch(1);
CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch);
executeShellCommand(String.format("getprop '%s'", name), receiver, GETPROP_TIMEOUT);
try {
latch.await(GETPROP_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return null;
}
String value = receiver.getOutput().trim();
if (value.isEmpty()) {
return null;
}
return value;
}
@Override
public String getMountPoint(String name) {
return mMountPoints.get(name);
}
@Override
public String toString() {
return mSerialNumber;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isOnline()
*/
@Override
public boolean isOnline() {
return mState == DeviceState.ONLINE;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isEmulator()
*/
@Override
public boolean isEmulator() {
return mSerialNumber.matches(RE_EMULATOR_SN);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isOffline()
*/
@Override
public boolean isOffline() {
return mState == DeviceState.OFFLINE;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isBootLoader()
*/
@Override
public boolean isBootLoader() {
return mState == DeviceState.BOOTLOADER;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getSyncService()
*/
@Override
public SyncService getSyncService()
throws TimeoutException, AdbCommandRejectedException, IOException {
SyncService syncService = new SyncService(AndroidDebugBridge.getSocketAddress(), this);
if (syncService.openSync()) {
return syncService;
}
return null;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getFileListingService()
*/
@Override
public FileListingService getFileListingService() {
return new FileListingService(this);
}
@Override
public RawImage getScreenshot()
throws TimeoutException, AdbCommandRejectedException, IOException {
return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this);
}
@Override
public void executeShellCommand(String command, IShellOutputReceiver receiver)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this,
receiver, DdmPreferences.getTimeOut());
}
@Override
public void executeShellCommand(String command, IShellOutputReceiver receiver,
int maxTimeToOutputResponse)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this,
receiver, maxTimeToOutputResponse);
}
@Override
public void runEventLogService(LogReceiver receiver)
throws TimeoutException, AdbCommandRejectedException, IOException {
AdbHelper.runEventLogService(AndroidDebugBridge.getSocketAddress(), this, receiver);
}
@Override
public void runLogService(String logname, LogReceiver receiver)
throws TimeoutException, AdbCommandRejectedException, IOException {
AdbHelper.runLogService(AndroidDebugBridge.getSocketAddress(), this, logname, receiver);
}
@Override
public void createForward(int localPort, int remotePort)
throws TimeoutException, AdbCommandRejectedException, IOException {
AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this,
String.format("tcp:%d", localPort), //$NON-NLS-1$
String.format("tcp:%d", remotePort)); //$NON-NLS-1$
}
@Override
public void createForward(int localPort, String remoteSocketName,
DeviceUnixSocketNamespace namespace) throws TimeoutException,
AdbCommandRejectedException, IOException {
AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this,
String.format("tcp:%d", localPort), //$NON-NLS-1$
String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$
}
@Override
public void removeForward(int localPort, int remotePort)
throws TimeoutException, AdbCommandRejectedException, IOException {
AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this,
String.format("tcp:%d", localPort), //$NON-NLS-1$
String.format("tcp:%d", remotePort)); //$NON-NLS-1$
}
@Override
public void removeForward(int localPort, String remoteSocketName,
DeviceUnixSocketNamespace namespace) throws TimeoutException,
AdbCommandRejectedException, IOException {
AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this,
String.format("tcp:%d", localPort), //$NON-NLS-1$
String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$
}
Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) {
mMonitor = monitor;
mSerialNumber = serialNumber;
mState = deviceState;
}
DeviceMonitor getMonitor() {
return mMonitor;
}
@Override
public boolean hasClients() {
synchronized (mClients) {
return !mClients.isEmpty();
}
}
@Override
public Client[] getClients() {
synchronized (mClients) {
return mClients.toArray(new Client[mClients.size()]);
}
}
@Override
public Client getClient(String applicationName) {
synchronized (mClients) {
for (Client c : mClients) {
if (applicationName.equals(c.getClientData().getClientDescription())) {
return c;
}
}
}
return null;
}
void addClient(Client client) {
synchronized (mClients) {
mClients.add(client);
}
addClientInfo(client);
}
List<Client> getClientList() {
return mClients;
}
void clearClientList() {
synchronized (mClients) {
mClients.clear();
}
clearClientInfo();
}
/**
* Removes a {@link Client} from the list.
* @param client the client to remove.
* @param notify Whether or not to notify the listeners of a change.
*/
void removeClient(Client client, boolean notify) {
mMonitor.addPortToAvailableList(client.getDebuggerListenPort());
synchronized (mClients) {
mClients.remove(client);
}
if (notify) {
mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST);
}
removeClientInfo(client);
}
/**
* Sets the client monitoring socket.
* @param socketChannel the sockets
*/
void setClientMonitoringSocket(SocketChannel socketChannel) {
mSocketChannel = socketChannel;
}
/**
* Returns the client monitoring socket.
*/
SocketChannel getClientMonitoringSocket() {
return mSocketChannel;
}
void update(int changeMask) {
if ((changeMask & CHANGE_BUILD_INFO) != 0) {
mArePropertiesSet = true;
}
mMonitor.getServer().deviceChanged(this, changeMask);
}
void update(Client client, int changeMask) {
mMonitor.getServer().clientChanged(client, changeMask);
updateClientInfo(client, changeMask);
}
void addProperty(String label, String value) {
mProperties.put(label, value);
}
void setMountingPoint(String name, String value) {
mMountPoints.put(name, value);
}
private void addClientInfo(Client client) {
ClientData cd = client.getClientData();
setClientInfo(cd.getPid(), cd.getClientDescription());
}
private void updateClientInfo(Client client, int changeMask) {
if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
addClientInfo(client);
}
}
private void removeClientInfo(Client client) {
int pid = client.getClientData().getPid();
mClientInfo.remove(pid);
}
private void clearClientInfo() {
mClientInfo.clear();
}
private void setClientInfo(int pid, String pkgName) {
if (pkgName == null) {
pkgName = UNKNOWN_PACKAGE;
}
mClientInfo.put(pid, pkgName);
}
@Override
public String getClientName(int pid) {
String pkgName = mClientInfo.get(pid);
return pkgName == null ? UNKNOWN_PACKAGE : pkgName;
}
@Override
public void pushFile(String local, String remote)
throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
SyncService sync = null;
try {
String targetFileName = getFileName(local);
Log.d(targetFileName, String.format("Uploading %1$s onto device '%2$s'",
targetFileName, getSerialNumber()));
sync = getSyncService();
if (sync != null) {
String message = String.format("Uploading file onto device '%1$s'",
getSerialNumber());
Log.d(LOG_TAG, message);
sync.pushFile(local, remote, SyncService.getNullProgressMonitor());
} else {
throw new IOException("Unable to open sync connection!");
}
} catch (TimeoutException e) {
Log.e(LOG_TAG, "Error during Sync: timeout.");
throw e;
} catch (SyncException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} catch (IOException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} finally {
if (sync != null) {
sync.close();
}
}
}
@Override
public void pullFile(String remote, String local)
throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
SyncService sync = null;
try {
String targetFileName = getFileName(remote);
Log.d(targetFileName, String.format("Downloading %1$s from device '%2$s'",
targetFileName, getSerialNumber()));
sync = getSyncService();
if (sync != null) {
String message = String.format("Downloading file from device '%1$s'",
getSerialNumber());
Log.d(LOG_TAG, message);
sync.pullFile(remote, local, SyncService.getNullProgressMonitor());
} else {
throw new IOException("Unable to open sync connection!");
}
} catch (TimeoutException e) {
Log.e(LOG_TAG, "Error during Sync: timeout.");
throw e;
} catch (SyncException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} catch (IOException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} finally {
if (sync != null) {
sync.close();
}
}
}
@Override
public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs)
throws InstallException {
try {
String remoteFilePath = syncPackageToDevice(packageFilePath);
String result = installRemotePackage(remoteFilePath, reinstall, extraArgs);
removeRemotePackage(remoteFilePath);
return result;
} catch (IOException e) {
throw new InstallException(e);
} catch (AdbCommandRejectedException e) {
throw new InstallException(e);
} catch (TimeoutException e) {
throw new InstallException(e);
} catch (SyncException e) {
throw new InstallException(e);
}
}
@Override
public String syncPackageToDevice(String localFilePath)
throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
SyncService sync = null;
try {
String packageFileName = getFileName(localFilePath);
String remoteFilePath = String.format("/data/local/tmp/%1$s", packageFileName); //$NON-NLS-1$
Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'",
packageFileName, getSerialNumber()));
sync = getSyncService();
if (sync != null) {
String message = String.format("Uploading file onto device '%1$s'",
getSerialNumber());
Log.d(LOG_TAG, message);
sync.pushFile(localFilePath, remoteFilePath, SyncService.getNullProgressMonitor());
} else {
throw new IOException("Unable to open sync connection!");
}
return remoteFilePath;
} catch (TimeoutException e) {
Log.e(LOG_TAG, "Error during Sync: timeout.");
throw e;
} catch (SyncException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} catch (IOException e) {
Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
throw e;
} finally {
if (sync != null) {
sync.close();
}
}
}
/**
* Helper method to retrieve the file name given a local file path
* @param filePath full directory path to file
* @return {@link String} file name
*/
private String getFileName(String filePath) {
return new File(filePath).getName();
}
@Override
public String installRemotePackage(String remoteFilePath, boolean reinstall,
String... extraArgs) throws InstallException {
try {
InstallReceiver receiver = new InstallReceiver();
StringBuilder optionString = new StringBuilder();
if (reinstall) {
optionString.append("-r ");
}
for (String arg : extraArgs) {
optionString.append(arg);
optionString.append(' ');
}
String cmd = String.format("pm install %1$s \"%2$s\"", optionString.toString(),
remoteFilePath);
executeShellCommand(cmd, receiver, INSTALL_TIMEOUT);
return receiver.getErrorMessage();
} catch (TimeoutException e) {
throw new InstallException(e);
} catch (AdbCommandRejectedException e) {
throw new InstallException(e);
} catch (ShellCommandUnresponsiveException e) {
throw new InstallException(e);
} catch (IOException e) {
throw new InstallException(e);
}
}
@Override
public void removeRemotePackage(String remoteFilePath) throws InstallException {
try {
executeShellCommand(String.format("rm \"%1$s\"", remoteFilePath),
new NullOutputReceiver(), INSTALL_TIMEOUT);
} catch (IOException e) {
throw new InstallException(e);
} catch (TimeoutException e) {
throw new InstallException(e);
} catch (AdbCommandRejectedException e) {
throw new InstallException(e);
} catch (ShellCommandUnresponsiveException e) {
throw new InstallException(e);
}
}
@Override
public String uninstallPackage(String packageName) throws InstallException {
try {
InstallReceiver receiver = new InstallReceiver();
executeShellCommand("pm uninstall " + packageName, receiver, INSTALL_TIMEOUT);
return receiver.getErrorMessage();
} catch (TimeoutException e) {
throw new InstallException(e);
} catch (AdbCommandRejectedException e) {
throw new InstallException(e);
} catch (ShellCommandUnresponsiveException e) {
throw new InstallException(e);
} catch (IOException e) {
throw new InstallException(e);
}
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#reboot()
*/
@Override
public void reboot(String into)
throws TimeoutException, AdbCommandRejectedException, IOException {
AdbHelper.reboot(into, AndroidDebugBridge.getSocketAddress(), this);
}
@Override
public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException,
IOException, ShellCommandUnresponsiveException {
// use default of 5 minutes
return getBatteryLevel(5 * 60 * 1000);
}
@Override
public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
if (mLastBatteryLevel != null
&& mLastBatteryCheckTime > (System.currentTimeMillis() - freshnessMs)) {
return mLastBatteryLevel;
}
BatteryReceiver receiver = new BatteryReceiver();
executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT);
mLastBatteryLevel = receiver.getBatteryLevel();
mLastBatteryCheckTime = System.currentTimeMillis();
return mLastBatteryLevel;
}
}