blob: 10308a378b1472daf0207004907431bf40cd8acf [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.hierarchyviewerlib.device;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.hierarchyviewerlib.ui.util.PsdFile;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
/**
* A bridge to the device.
*/
public class DeviceBridge {
public static final String TAG = "hierarchyviewer";
private static final int DEFAULT_SERVER_PORT = 4939;
// These codes must match the auto-generated codes in IWindowManager.java
// See IWindowManager.aidl as well
private static final int SERVICE_CODE_START_SERVER = 1;
private static final int SERVICE_CODE_STOP_SERVER = 2;
private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3;
private static AndroidDebugBridge sBridge;
private static final HashMap<IDevice, Integer> sDevicePortMap = new HashMap<IDevice, Integer>();
private static final HashMap<IDevice, ViewServerInfo> sViewServerInfo =
new HashMap<IDevice, ViewServerInfo>();
private static int sNextLocalPort = DEFAULT_SERVER_PORT;
public static class ViewServerInfo {
public final int protocolVersion;
public final int serverVersion;
ViewServerInfo(int serverVersion, int protocolVersion) {
this.protocolVersion = protocolVersion;
this.serverVersion = serverVersion;
}
}
/**
* Init the DeviceBridge with an existing {@link AndroidDebugBridge}.
* @param bridge the bridge object to use
*/
public static void acquireBridge(AndroidDebugBridge bridge) {
sBridge = bridge;
}
/**
* Creates an {@link AndroidDebugBridge} connected to adb at the given location.
*
* If a bridge is already running, this disconnects it and creates a new one.
*
* @param adbLocation the location to adb.
*/
public static void initDebugBridge(String adbLocation) {
if (sBridge == null) {
AndroidDebugBridge.init(false /* debugger support */);
}
if (sBridge == null || !sBridge.isConnected()) {
sBridge = AndroidDebugBridge.createBridge(adbLocation, true);
}
}
/** Disconnects the current {@link AndroidDebugBridge}. */
public static void terminate() {
AndroidDebugBridge.terminate();
}
public static IDevice[] getDevices() {
if (sBridge == null) {
return new IDevice[0];
}
return sBridge.getDevices();
}
/*
* This adds a listener to the debug bridge. The listener is notified of
* connecting/disconnecting devices, devices coming online, etc.
*/
public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
AndroidDebugBridge.addDeviceChangeListener(listener);
}
public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
AndroidDebugBridge.removeDeviceChangeListener(listener);
}
/**
* Sets up a just-connected device to work with the view server.
* <p/>
* This starts a port forwarding between a local port and a port on the
* device.
*
* @param device
*/
public static void setupDeviceForward(IDevice device) {
synchronized (sDevicePortMap) {
if (device.getState() == IDevice.DeviceState.ONLINE) {
int localPort = sNextLocalPort++;
try {
device.createForward(localPort, DEFAULT_SERVER_PORT);
sDevicePortMap.put(device, localPort);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout setting up port forwarding for " + device);
} catch (AdbCommandRejectedException e) {
Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s",
device, e.getMessage()));
} catch (IOException e) {
Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s",
device, e.getMessage()));
}
}
}
}
public static void removeDeviceForward(IDevice device) {
synchronized (sDevicePortMap) {
final Integer localPort = sDevicePortMap.get(device);
if (localPort != null) {
try {
device.removeForward(localPort, DEFAULT_SERVER_PORT);
sDevicePortMap.remove(device);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout removing port forwarding for " + device);
} catch (AdbCommandRejectedException e) {
// In this case, we want to fail silently.
} catch (IOException e) {
Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s",
device, e.getMessage()));
}
}
}
}
public static int getDeviceLocalPort(IDevice device) {
synchronized (sDevicePortMap) {
Integer port = sDevicePortMap.get(device);
if (port != null) {
return port;
}
Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber());
return -1;
}
}
public static boolean isViewServerRunning(IDevice device) {
final boolean[] result = new boolean[1];
try {
if (device.isOnline()) {
device.executeShellCommand(buildIsServerRunningShellCommand(),
new BooleanResultReader(result));
if (!result[0]) {
ViewServerInfo serverInfo = loadViewServerInfo(device);
if (serverInfo != null && serverInfo.protocolVersion > 2) {
result[0] = true;
}
}
}
} catch (TimeoutException e) {
Log.e(TAG, "Timeout checking status of view server on device " + device);
} catch (IOException e) {
Log.e(TAG, "Unable to check status of view server on device " + device);
} catch (AdbCommandRejectedException e) {
Log.e(TAG, "Adb rejected command to check status of view server on device " + device);
} catch (ShellCommandUnresponsiveException e) {
Log.e(TAG, "Unable to execute command to check status of view server on device "
+ device);
}
return result[0];
}
public static boolean startViewServer(IDevice device) {
return startViewServer(device, DEFAULT_SERVER_PORT);
}
public static boolean startViewServer(IDevice device, int port) {
final boolean[] result = new boolean[1];
try {
if (device.isOnline()) {
device.executeShellCommand(buildStartServerShellCommand(port),
new BooleanResultReader(result));
}
} catch (TimeoutException e) {
Log.e(TAG, "Timeout starting view server on device " + device);
} catch (IOException e) {
Log.e(TAG, "Unable to start view server on device " + device);
} catch (AdbCommandRejectedException e) {
Log.e(TAG, "Adb rejected command to start view server on device " + device);
} catch (ShellCommandUnresponsiveException e) {
Log.e(TAG, "Unable to execute command to start view server on device " + device);
}
return result[0];
}
public static boolean stopViewServer(IDevice device) {
final boolean[] result = new boolean[1];
try {
if (device.isOnline()) {
device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader(
result));
}
} catch (TimeoutException e) {
Log.e(TAG, "Timeout stopping view server on device " + device);
} catch (IOException e) {
Log.e(TAG, "Unable to stop view server on device " + device);
} catch (AdbCommandRejectedException e) {
Log.e(TAG, "Adb rejected command to stop view server on device " + device);
} catch (ShellCommandUnresponsiveException e) {
Log.e(TAG, "Unable to execute command to stop view server on device " + device);
}
return result[0];
}
private static String buildStartServerShellCommand(int port) {
return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$
}
private static String buildStopServerShellCommand() {
return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$
}
private static String buildIsServerRunningShellCommand() {
return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$
}
private static class BooleanResultReader extends MultiLineReceiver {
private final boolean[] mResult;
public BooleanResultReader(boolean[] result) {
mResult = result;
}
@Override
public void processNewLines(String[] strings) {
if (strings.length > 0) {
Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(strings[0]);
if (matcher.matches()) {
if (Integer.parseInt(matcher.group(1)) == 1) {
mResult[0] = true;
}
}
}
}
@Override
public boolean isCancelled() {
return false;
}
}
public static ViewServerInfo loadViewServerInfo(IDevice device) {
int server = -1;
int protocol = -1;
DeviceConnection connection = null;
try {
connection = new DeviceConnection(device);
connection.sendCommand("SERVER"); //$NON-NLS-1$
String line = connection.getInputStream().readLine();
if (line != null) {
server = Integer.parseInt(line);
}
} catch (Exception e) {
Log.e(TAG, "Unable to get view server version from device " + device);
} finally {
if (connection != null) {
connection.close();
}
}
connection = null;
try {
connection = new DeviceConnection(device);
connection.sendCommand("PROTOCOL"); //$NON-NLS-1$
String line = connection.getInputStream().readLine();
if (line != null) {
protocol = Integer.parseInt(line);
}
} catch (Exception e) {
Log.e(TAG, "Unable to get view server protocol version from device " + device);
} finally {
if (connection != null) {
connection.close();
}
}
if (server == -1 || protocol == -1) {
return null;
}
ViewServerInfo returnValue = new ViewServerInfo(server, protocol);
synchronized (sViewServerInfo) {
sViewServerInfo.put(device, returnValue);
}
return returnValue;
}
public static ViewServerInfo getViewServerInfo(IDevice device) {
synchronized (sViewServerInfo) {
return sViewServerInfo.get(device);
}
}
public static void removeViewServerInfo(IDevice device) {
synchronized (sViewServerInfo) {
sViewServerInfo.remove(device);
}
}
/*
* This loads the list of windows from the specified device. The format is:
* hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE.
*/
public static Window[] loadWindows(IDevice device) {
ArrayList<Window> windows = new ArrayList<Window>();
DeviceConnection connection = null;
ViewServerInfo serverInfo = getViewServerInfo(device);
try {
connection = new DeviceConnection(device);
connection.sendCommand("LIST"); //$NON-NLS-1$
BufferedReader in = connection.getInputStream();
String line;
while ((line = in.readLine()) != null) {
if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$
break;
}
int index = line.indexOf(' ');
if (index != -1) {
String windowId = line.substring(0, index);
int id;
if (serverInfo.serverVersion > 2) {
id = (int) Long.parseLong(windowId, 16);
} else {
id = Integer.parseInt(windowId, 16);
}
Window w = new Window(device, line.substring(index + 1), id);
windows.add(w);
}
}
// Automatic refreshing of windows was added in protocol version 3.
// Before, the user needed to specify explicitly that he wants to
// get the focused window, which was done using a special type of
// window with hash code -1.
if (serverInfo.protocolVersion < 3) {
windows.add(Window.getFocusedWindow(device));
}
} catch (Exception e) {
Log.e(TAG, "Unable to load the window list from device " + device);
} finally {
if (connection != null) {
connection.close();
}
}
// The server returns the list of windows from the window at the bottom
// to the top. We want the reverse order to put the top window on top of
// the list.
Window[] returnValue = new Window[windows.size()];
for (int i = windows.size() - 1; i >= 0; i--) {
returnValue[returnValue.length - i - 1] = windows.get(i);
}
return returnValue;
}
/*
* This gets the hash code of the window that has focus. Only works with
* protocol version 3 and above.
*/
public static int getFocusedWindow(IDevice device) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(device);
connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$
String line = connection.getInputStream().readLine();
if (line == null || line.length() == 0) {
return -1;
}
return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16);
} catch (Exception e) {
Log.e(TAG, "Unable to get the focused window from device " + device);
} finally {
if (connection != null) {
connection.close();
}
}
return -1;
}
public static ViewNode loadWindowData(Window window) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(window.getDevice());
connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$
BufferedReader in = connection.getInputStream();
ViewNode currentNode = null;
int currentDepth = -1;
String line;
while ((line = in.readLine()) != null) {
if ("DONE.".equalsIgnoreCase(line)) {
break;
}
int depth = 0;
while (line.charAt(depth) == ' ') {
depth++;
}
while (depth <= currentDepth) {
currentNode = currentNode.parent;
currentDepth--;
}
currentNode = new ViewNode(window, currentNode, line.substring(depth));
currentDepth = depth;
}
if (currentNode == null) {
return null;
}
while (currentNode.parent != null) {
currentNode = currentNode.parent;
}
ViewServerInfo serverInfo = getViewServerInfo(window.getDevice());
if (serverInfo != null) {
currentNode.protocolVersion = serverInfo.protocolVersion;
}
return currentNode;
} catch (Exception e) {
Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device "
+ window.getDevice());
Log.e(TAG, e.getMessage());
} finally {
if (connection != null) {
connection.close();
}
}
return null;
}
public static boolean loadProfileData(Window window, ViewNode viewNode) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(window.getDevice());
connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
BufferedReader in = connection.getInputStream();
int protocol;
synchronized (sViewServerInfo) {
protocol = sViewServerInfo.get(window.getDevice()).protocolVersion;
}
if (protocol < 3) {
return loadProfileData(viewNode, in);
} else {
boolean ret = loadProfileDataRecursive(viewNode, in);
if (ret) {
viewNode.setProfileRatings();
}
return ret;
}
} catch (Exception e) {
Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
+ " on device " + window.getDevice());
} finally {
if (connection != null) {
connection.close();
}
}
return false;
}
private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException {
String line;
if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$
|| line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$
return false;
}
String[] data = line.split(" ");
node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0;
node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0;
node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0;
return true;
}
private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in)
throws IOException {
if (!loadProfileData(node, in)) {
return false;
}
for (int i = 0; i < node.children.size(); i++) {
if (!loadProfileDataRecursive(node.children.get(i), in)) {
return false;
}
}
return true;
}
public static Image loadCapture(Window window, ViewNode viewNode) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(window.getDevice());
connection.getSocket().setSoTimeout(5000);
connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
return new Image(Display.getDefault(), connection.getSocket().getInputStream());
} catch (Exception e) {
Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
+ window.getTitle() + " on device " + window.getDevice());
} finally {
if (connection != null) {
connection.close();
}
}
return null;
}
public static PsdFile captureLayers(Window window) {
DeviceConnection connection = null;
DataInputStream in = null;
try {
connection = new DeviceConnection(window.getDevice());
connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$
in =
new DataInputStream(new BufferedInputStream(connection.getSocket()
.getInputStream()));
int width = in.readInt();
int height = in.readInt();
PsdFile psd = new PsdFile(width, height);
while (readLayer(in, psd)) {
}
return psd;
} catch (Exception e) {
Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device "
+ window.getDevice());
} finally {
if (in != null) {
try {
in.close();
} catch (Exception ex) {
}
}
connection.close();
}
return null;
}
private static boolean readLayer(DataInputStream in, PsdFile psd) {
try {
if (in.read() == 2) {
return false;
}
String name = in.readUTF();
boolean visible = in.read() == 1;
int x = in.readInt();
int y = in.readInt();
int dataSize = in.readInt();
byte[] data = new byte[dataSize];
int read = 0;
while (read < dataSize) {
read += in.read(data, read, dataSize - read);
}
ByteArrayInputStream arrayIn = new ByteArrayInputStream(data);
BufferedImage chunk = ImageIO.read(arrayIn);
// Ensure the image is in the right format
BufferedImage image =
new BufferedImage(chunk.getWidth(), chunk.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.drawImage(chunk, null, 0, 0);
g.dispose();
psd.addLayer(name, image, new Point(x, y), visible);
return true;
} catch (Exception e) {
return false;
}
}
public static void invalidateView(ViewNode viewNode) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(viewNode.window.getDevice());
connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
} catch (Exception e) {
Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window
+ " on device " + viewNode.window.getDevice());
} finally {
connection.close();
}
}
public static void requestLayout(ViewNode viewNode) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(viewNode.window.getDevice());
connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
} catch (Exception e) {
Log.e(TAG, "Unable to request layout for node " + viewNode + " in window "
+ viewNode.window + " on device " + viewNode.window.getDevice());
} finally {
connection.close();
}
}
public static void outputDisplayList(ViewNode viewNode) {
DeviceConnection connection = null;
try {
connection = new DeviceConnection(viewNode.window.getDevice());
connection.sendCommand("OUTPUT_DISPLAYLIST " +
viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
} catch (Exception e) {
Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window "
+ viewNode.window + " on device " + viewNode.window.getDevice());
} finally {
connection.close();
}
}
}