/* * 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 sDevicePortMap = new HashMap(); private static final HashMap sViewServerInfo = new HashMap(); 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. *

* 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 windows = new ArrayList(); 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(); } } }