1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.hierarchyviewerlib.device; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.AndroidDebugBridge; 21 import com.android.ddmlib.IDevice; 22 import com.android.ddmlib.Log; 23 import com.android.ddmlib.MultiLineReceiver; 24 import com.android.ddmlib.ShellCommandUnresponsiveException; 25 import com.android.ddmlib.TimeoutException; 26 import com.android.hierarchyviewerlib.ui.util.PsdFile; 27 28 import org.eclipse.swt.graphics.Image; 29 import org.eclipse.swt.widgets.Display; 30 31 import java.awt.Graphics2D; 32 import java.awt.Point; 33 import java.awt.image.BufferedImage; 34 import java.io.BufferedInputStream; 35 import java.io.BufferedReader; 36 import java.io.ByteArrayInputStream; 37 import java.io.DataInputStream; 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.regex.Matcher; 42 import java.util.regex.Pattern; 43 44 import javax.imageio.ImageIO; 45 46 /** 47 * A bridge to the device. 48 */ 49 public class DeviceBridge { 50 51 public static final String TAG = "hierarchyviewer"; 52 53 private static final int DEFAULT_SERVER_PORT = 4939; 54 55 // These codes must match the auto-generated codes in IWindowManager.java 56 // See IWindowManager.aidl as well 57 private static final int SERVICE_CODE_START_SERVER = 1; 58 59 private static final int SERVICE_CODE_STOP_SERVER = 2; 60 61 private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3; 62 63 private static AndroidDebugBridge sBridge; 64 65 private static final HashMap<IDevice, Integer> sDevicePortMap = new HashMap<IDevice, Integer>(); 66 67 private static final HashMap<IDevice, ViewServerInfo> sViewServerInfo = 68 new HashMap<IDevice, ViewServerInfo>(); 69 70 private static int sNextLocalPort = DEFAULT_SERVER_PORT; 71 72 public static class ViewServerInfo { 73 public final int protocolVersion; 74 75 public final int serverVersion; 76 77 ViewServerInfo(int serverVersion, int protocolVersion) { 78 this.protocolVersion = protocolVersion; 79 this.serverVersion = serverVersion; 80 } 81 } 82 83 /** 84 * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. 85 * @param bridge the bridge object to use 86 */ 87 public static void acquireBridge(AndroidDebugBridge bridge) { 88 sBridge = bridge; 89 } 90 91 /** 92 * Creates an {@link AndroidDebugBridge} connected to adb at the given location. 93 * 94 * If a bridge is already running, this disconnects it and creates a new one. 95 * 96 * @param adbLocation the location to adb. 97 */ 98 public static void initDebugBridge(String adbLocation) { 99 if (sBridge == null) { 100 AndroidDebugBridge.init(false /* debugger support */); 101 } 102 if (sBridge == null || !sBridge.isConnected()) { 103 sBridge = AndroidDebugBridge.createBridge(adbLocation, true); 104 } 105 } 106 107 /** Disconnects the current {@link AndroidDebugBridge}. */ 108 public static void terminate() { 109 AndroidDebugBridge.terminate(); 110 } 111 112 public static IDevice[] getDevices() { 113 if (sBridge == null) { 114 return new IDevice[0]; 115 } 116 return sBridge.getDevices(); 117 } 118 119 /* 120 * This adds a listener to the debug bridge. The listener is notified of 121 * connecting/disconnecting devices, devices coming online, etc. 122 */ 123 public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { 124 AndroidDebugBridge.addDeviceChangeListener(listener); 125 } 126 127 public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { 128 AndroidDebugBridge.removeDeviceChangeListener(listener); 129 } 130 131 /** 132 * Sets up a just-connected device to work with the view server. 133 * <p/> 134 * This starts a port forwarding between a local port and a port on the 135 * device. 136 * 137 * @param device 138 */ 139 public static void setupDeviceForward(IDevice device) { 140 synchronized (sDevicePortMap) { 141 if (device.getState() == IDevice.DeviceState.ONLINE) { 142 int localPort = sNextLocalPort++; 143 try { 144 device.createForward(localPort, DEFAULT_SERVER_PORT); 145 sDevicePortMap.put(device, localPort); 146 } catch (TimeoutException e) { 147 Log.e(TAG, "Timeout setting up port forwarding for " + device); 148 } catch (AdbCommandRejectedException e) { 149 Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s", 150 device, e.getMessage())); 151 } catch (IOException e) { 152 Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s", 153 device, e.getMessage())); 154 } 155 } 156 } 157 } 158 159 public static void removeDeviceForward(IDevice device) { 160 synchronized (sDevicePortMap) { 161 final Integer localPort = sDevicePortMap.get(device); 162 if (localPort != null) { 163 try { 164 device.removeForward(localPort, DEFAULT_SERVER_PORT); 165 sDevicePortMap.remove(device); 166 } catch (TimeoutException e) { 167 Log.e(TAG, "Timeout removing port forwarding for " + device); 168 } catch (AdbCommandRejectedException e) { 169 // In this case, we want to fail silently. 170 } catch (IOException e) { 171 Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s", 172 device, e.getMessage())); 173 } 174 } 175 } 176 } 177 178 public static int getDeviceLocalPort(IDevice device) { 179 synchronized (sDevicePortMap) { 180 Integer port = sDevicePortMap.get(device); 181 if (port != null) { 182 return port; 183 } 184 185 Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber()); 186 return -1; 187 } 188 189 } 190 191 public static boolean isViewServerRunning(IDevice device) { 192 final boolean[] result = new boolean[1]; 193 try { 194 if (device.isOnline()) { 195 device.executeShellCommand(buildIsServerRunningShellCommand(), 196 new BooleanResultReader(result)); 197 if (!result[0]) { 198 ViewServerInfo serverInfo = loadViewServerInfo(device); 199 if (serverInfo != null && serverInfo.protocolVersion > 2) { 200 result[0] = true; 201 } 202 } 203 } 204 } catch (TimeoutException e) { 205 Log.e(TAG, "Timeout checking status of view server on device " + device); 206 } catch (IOException e) { 207 Log.e(TAG, "Unable to check status of view server on device " + device); 208 } catch (AdbCommandRejectedException e) { 209 Log.e(TAG, "Adb rejected command to check status of view server on device " + device); 210 } catch (ShellCommandUnresponsiveException e) { 211 Log.e(TAG, "Unable to execute command to check status of view server on device " 212 + device); 213 } 214 return result[0]; 215 } 216 217 public static boolean startViewServer(IDevice device) { 218 return startViewServer(device, DEFAULT_SERVER_PORT); 219 } 220 221 public static boolean startViewServer(IDevice device, int port) { 222 final boolean[] result = new boolean[1]; 223 try { 224 if (device.isOnline()) { 225 device.executeShellCommand(buildStartServerShellCommand(port), 226 new BooleanResultReader(result)); 227 } 228 } catch (TimeoutException e) { 229 Log.e(TAG, "Timeout starting view server on device " + device); 230 } catch (IOException e) { 231 Log.e(TAG, "Unable to start view server on device " + device); 232 } catch (AdbCommandRejectedException e) { 233 Log.e(TAG, "Adb rejected command to start view server on device " + device); 234 } catch (ShellCommandUnresponsiveException e) { 235 Log.e(TAG, "Unable to execute command to start view server on device " + device); 236 } 237 return result[0]; 238 } 239 240 public static boolean stopViewServer(IDevice device) { 241 final boolean[] result = new boolean[1]; 242 try { 243 if (device.isOnline()) { 244 device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader( 245 result)); 246 } 247 } catch (TimeoutException e) { 248 Log.e(TAG, "Timeout stopping view server on device " + device); 249 } catch (IOException e) { 250 Log.e(TAG, "Unable to stop view server on device " + device); 251 } catch (AdbCommandRejectedException e) { 252 Log.e(TAG, "Adb rejected command to stop view server on device " + device); 253 } catch (ShellCommandUnresponsiveException e) { 254 Log.e(TAG, "Unable to execute command to stop view server on device " + device); 255 } 256 return result[0]; 257 } 258 259 private static String buildStartServerShellCommand(int port) { 260 return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$ 261 } 262 263 private static String buildStopServerShellCommand() { 264 return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$ 265 } 266 267 private static String buildIsServerRunningShellCommand() { 268 return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$ 269 } 270 271 private static class BooleanResultReader extends MultiLineReceiver { 272 private final boolean[] mResult; 273 274 public BooleanResultReader(boolean[] result) { 275 mResult = result; 276 } 277 278 @Override 279 public void processNewLines(String[] strings) { 280 if (strings.length > 0) { 281 Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$ 282 Matcher matcher = pattern.matcher(strings[0]); 283 if (matcher.matches()) { 284 if (Integer.parseInt(matcher.group(1)) == 1) { 285 mResult[0] = true; 286 } 287 } 288 } 289 } 290 291 @Override 292 public boolean isCancelled() { 293 return false; 294 } 295 } 296 297 public static ViewServerInfo loadViewServerInfo(IDevice device) { 298 int server = -1; 299 int protocol = -1; 300 DeviceConnection connection = null; 301 try { 302 connection = new DeviceConnection(device); 303 connection.sendCommand("SERVER"); //$NON-NLS-1$ 304 String line = connection.getInputStream().readLine(); 305 if (line != null) { 306 server = Integer.parseInt(line); 307 } 308 } catch (Exception e) { 309 Log.e(TAG, "Unable to get view server version from device " + device); 310 } finally { 311 if (connection != null) { 312 connection.close(); 313 } 314 } 315 connection = null; 316 try { 317 connection = new DeviceConnection(device); 318 connection.sendCommand("PROTOCOL"); //$NON-NLS-1$ 319 String line = connection.getInputStream().readLine(); 320 if (line != null) { 321 protocol = Integer.parseInt(line); 322 } 323 } catch (Exception e) { 324 Log.e(TAG, "Unable to get view server protocol version from device " + device); 325 } finally { 326 if (connection != null) { 327 connection.close(); 328 } 329 } 330 if (server == -1 || protocol == -1) { 331 return null; 332 } 333 ViewServerInfo returnValue = new ViewServerInfo(server, protocol); 334 synchronized (sViewServerInfo) { 335 sViewServerInfo.put(device, returnValue); 336 } 337 return returnValue; 338 } 339 340 public static ViewServerInfo getViewServerInfo(IDevice device) { 341 synchronized (sViewServerInfo) { 342 return sViewServerInfo.get(device); 343 } 344 } 345 346 public static void removeViewServerInfo(IDevice device) { 347 synchronized (sViewServerInfo) { 348 sViewServerInfo.remove(device); 349 } 350 } 351 352 /* 353 * This loads the list of windows from the specified device. The format is: 354 * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE. 355 */ 356 public static Window[] loadWindows(IDevice device) { 357 ArrayList<Window> windows = new ArrayList<Window>(); 358 DeviceConnection connection = null; 359 ViewServerInfo serverInfo = getViewServerInfo(device); 360 try { 361 connection = new DeviceConnection(device); 362 connection.sendCommand("LIST"); //$NON-NLS-1$ 363 BufferedReader in = connection.getInputStream(); 364 String line; 365 while ((line = in.readLine()) != null) { 366 if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$ 367 break; 368 } 369 370 int index = line.indexOf(' '); 371 if (index != -1) { 372 String windowId = line.substring(0, index); 373 374 int id; 375 if (serverInfo.serverVersion > 2) { 376 id = (int) Long.parseLong(windowId, 16); 377 } else { 378 id = Integer.parseInt(windowId, 16); 379 } 380 381 Window w = new Window(device, line.substring(index + 1), id); 382 windows.add(w); 383 } 384 } 385 // Automatic refreshing of windows was added in protocol version 3. 386 // Before, the user needed to specify explicitly that he wants to 387 // get the focused window, which was done using a special type of 388 // window with hash code -1. 389 if (serverInfo.protocolVersion < 3) { 390 windows.add(Window.getFocusedWindow(device)); 391 } 392 } catch (Exception e) { 393 Log.e(TAG, "Unable to load the window list from device " + device); 394 } finally { 395 if (connection != null) { 396 connection.close(); 397 } 398 } 399 // The server returns the list of windows from the window at the bottom 400 // to the top. We want the reverse order to put the top window on top of 401 // the list. 402 Window[] returnValue = new Window[windows.size()]; 403 for (int i = windows.size() - 1; i >= 0; i--) { 404 returnValue[returnValue.length - i - 1] = windows.get(i); 405 } 406 return returnValue; 407 } 408 409 /* 410 * This gets the hash code of the window that has focus. Only works with 411 * protocol version 3 and above. 412 */ 413 public static int getFocusedWindow(IDevice device) { 414 DeviceConnection connection = null; 415 try { 416 connection = new DeviceConnection(device); 417 connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$ 418 String line = connection.getInputStream().readLine(); 419 if (line == null || line.length() == 0) { 420 return -1; 421 } 422 return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16); 423 } catch (Exception e) { 424 Log.e(TAG, "Unable to get the focused window from device " + device); 425 } finally { 426 if (connection != null) { 427 connection.close(); 428 } 429 } 430 return -1; 431 } 432 433 public static ViewNode loadWindowData(Window window) { 434 DeviceConnection connection = null; 435 try { 436 connection = new DeviceConnection(window.getDevice()); 437 connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$ 438 BufferedReader in = connection.getInputStream(); 439 ViewNode currentNode = null; 440 int currentDepth = -1; 441 String line; 442 while ((line = in.readLine()) != null) { 443 if ("DONE.".equalsIgnoreCase(line)) { 444 break; 445 } 446 int depth = 0; 447 while (line.charAt(depth) == ' ') { 448 depth++; 449 } 450 while (depth <= currentDepth) { 451 currentNode = currentNode.parent; 452 currentDepth--; 453 } 454 currentNode = new ViewNode(window, currentNode, line.substring(depth)); 455 currentDepth = depth; 456 } 457 if (currentNode == null) { 458 return null; 459 } 460 while (currentNode.parent != null) { 461 currentNode = currentNode.parent; 462 } 463 ViewServerInfo serverInfo = getViewServerInfo(window.getDevice()); 464 if (serverInfo != null) { 465 currentNode.protocolVersion = serverInfo.protocolVersion; 466 } 467 return currentNode; 468 } catch (Exception e) { 469 Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device " 470 + window.getDevice()); 471 } finally { 472 if (connection != null) { 473 connection.close(); 474 } 475 } 476 return null; 477 } 478 479 public static boolean loadProfileData(Window window, ViewNode viewNode) { 480 DeviceConnection connection = null; 481 try { 482 connection = new DeviceConnection(window.getDevice()); 483 connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ 484 BufferedReader in = connection.getInputStream(); 485 int protocol; 486 synchronized (sViewServerInfo) { 487 protocol = sViewServerInfo.get(window.getDevice()).protocolVersion; 488 } 489 if (protocol < 3) { 490 return loadProfileData(viewNode, in); 491 } else { 492 boolean ret = loadProfileDataRecursive(viewNode, in); 493 if (ret) { 494 viewNode.setProfileRatings(); 495 } 496 return ret; 497 } 498 } catch (Exception e) { 499 Log.e(TAG, "Unable to load profiling data for window " + window.getTitle() 500 + " on device " + window.getDevice()); 501 } finally { 502 if (connection != null) { 503 connection.close(); 504 } 505 } 506 return false; 507 } 508 509 private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException { 510 String line; 511 if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$ 512 || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$ 513 return false; 514 } 515 String[] data = line.split(" "); 516 node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0; 517 node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0; 518 node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0; 519 return true; 520 } 521 522 private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in) 523 throws IOException { 524 if (!loadProfileData(node, in)) { 525 return false; 526 } 527 for (int i = 0; i < node.children.size(); i++) { 528 if (!loadProfileDataRecursive(node.children.get(i), in)) { 529 return false; 530 } 531 } 532 return true; 533 } 534 535 public static Image loadCapture(Window window, ViewNode viewNode) { 536 DeviceConnection connection = null; 537 try { 538 connection = new DeviceConnection(window.getDevice()); 539 connection.getSocket().setSoTimeout(5000); 540 connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ 541 return new Image(Display.getDefault(), connection.getSocket().getInputStream()); 542 } catch (Exception e) { 543 Log.e(TAG, "Unable to capture data for node " + viewNode + " in window " 544 + window.getTitle() + " on device " + window.getDevice()); 545 } finally { 546 if (connection != null) { 547 connection.close(); 548 } 549 } 550 return null; 551 } 552 553 public static PsdFile captureLayers(Window window) { 554 DeviceConnection connection = null; 555 DataInputStream in = null; 556 557 try { 558 connection = new DeviceConnection(window.getDevice()); 559 560 connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$ 561 562 in = 563 new DataInputStream(new BufferedInputStream(connection.getSocket() 564 .getInputStream())); 565 566 int width = in.readInt(); 567 int height = in.readInt(); 568 569 PsdFile psd = new PsdFile(width, height); 570 571 while (readLayer(in, psd)) { 572 } 573 574 return psd; 575 } catch (Exception e) { 576 Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device " 577 + window.getDevice()); 578 } finally { 579 if (in != null) { 580 try { 581 in.close(); 582 } catch (Exception ex) { 583 } 584 } 585 connection.close(); 586 } 587 588 return null; 589 } 590 591 private static boolean readLayer(DataInputStream in, PsdFile psd) { 592 try { 593 if (in.read() == 2) { 594 return false; 595 } 596 String name = in.readUTF(); 597 boolean visible = in.read() == 1; 598 int x = in.readInt(); 599 int y = in.readInt(); 600 int dataSize = in.readInt(); 601 602 byte[] data = new byte[dataSize]; 603 int read = 0; 604 while (read < dataSize) { 605 read += in.read(data, read, dataSize - read); 606 } 607 608 ByteArrayInputStream arrayIn = new ByteArrayInputStream(data); 609 BufferedImage chunk = ImageIO.read(arrayIn); 610 611 // Ensure the image is in the right format 612 BufferedImage image = 613 new BufferedImage(chunk.getWidth(), chunk.getHeight(), 614 BufferedImage.TYPE_INT_ARGB); 615 Graphics2D g = image.createGraphics(); 616 g.drawImage(chunk, null, 0, 0); 617 g.dispose(); 618 619 psd.addLayer(name, image, new Point(x, y), visible); 620 621 return true; 622 } catch (Exception e) { 623 return false; 624 } 625 } 626 627 public static void invalidateView(ViewNode viewNode) { 628 DeviceConnection connection = null; 629 try { 630 connection = new DeviceConnection(viewNode.window.getDevice()); 631 connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 632 } catch (Exception e) { 633 Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window 634 + " on device " + viewNode.window.getDevice()); 635 } finally { 636 connection.close(); 637 } 638 } 639 640 public static void requestLayout(ViewNode viewNode) { 641 DeviceConnection connection = null; 642 try { 643 connection = new DeviceConnection(viewNode.window.getDevice()); 644 connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 645 } catch (Exception e) { 646 Log.e(TAG, "Unable to request layout for node " + viewNode + " in window " 647 + viewNode.window + " on device " + viewNode.window.getDevice()); 648 } finally { 649 connection.close(); 650 } 651 } 652 653 public static void outputDisplayList(ViewNode viewNode) { 654 DeviceConnection connection = null; 655 try { 656 connection = new DeviceConnection(viewNode.window.getDevice()); 657 connection.sendCommand("OUTPUT_DISPLAYLIST " + 658 viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 659 } catch (Exception e) { 660 Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window " 661 + viewNode.window + " on device " + viewNode.window.getDevice()); 662 } finally { 663 connection.close(); 664 } 665 } 666 667 } 668