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 public boolean isCancelled() { 292 return false; 293 } 294 } 295 296 public static ViewServerInfo loadViewServerInfo(IDevice device) { 297 int server = -1; 298 int protocol = -1; 299 DeviceConnection connection = null; 300 try { 301 connection = new DeviceConnection(device); 302 connection.sendCommand("SERVER"); //$NON-NLS-1$ 303 String line = connection.getInputStream().readLine(); 304 if (line != null) { 305 server = Integer.parseInt(line); 306 } 307 } catch (Exception e) { 308 Log.e(TAG, "Unable to get view server version from device " + device); 309 } finally { 310 if (connection != null) { 311 connection.close(); 312 } 313 } 314 connection = null; 315 try { 316 connection = new DeviceConnection(device); 317 connection.sendCommand("PROTOCOL"); //$NON-NLS-1$ 318 String line = connection.getInputStream().readLine(); 319 if (line != null) { 320 protocol = Integer.parseInt(line); 321 } 322 } catch (Exception e) { 323 Log.e(TAG, "Unable to get view server protocol version from device " + device); 324 } finally { 325 if (connection != null) { 326 connection.close(); 327 } 328 } 329 if (server == -1 || protocol == -1) { 330 return null; 331 } 332 ViewServerInfo returnValue = new ViewServerInfo(server, protocol); 333 synchronized (sViewServerInfo) { 334 sViewServerInfo.put(device, returnValue); 335 } 336 return returnValue; 337 } 338 339 public static ViewServerInfo getViewServerInfo(IDevice device) { 340 synchronized (sViewServerInfo) { 341 return sViewServerInfo.get(device); 342 } 343 } 344 345 public static void removeViewServerInfo(IDevice device) { 346 synchronized (sViewServerInfo) { 347 sViewServerInfo.remove(device); 348 } 349 } 350 351 /* 352 * This loads the list of windows from the specified device. The format is: 353 * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE. 354 */ 355 public static Window[] loadWindows(IDevice device) { 356 ArrayList<Window> windows = new ArrayList<Window>(); 357 DeviceConnection connection = null; 358 ViewServerInfo serverInfo = getViewServerInfo(device); 359 try { 360 connection = new DeviceConnection(device); 361 connection.sendCommand("LIST"); //$NON-NLS-1$ 362 BufferedReader in = connection.getInputStream(); 363 String line; 364 while ((line = in.readLine()) != null) { 365 if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$ 366 break; 367 } 368 369 int index = line.indexOf(' '); 370 if (index != -1) { 371 String windowId = line.substring(0, index); 372 373 int id; 374 if (serverInfo.serverVersion > 2) { 375 id = (int) Long.parseLong(windowId, 16); 376 } else { 377 id = Integer.parseInt(windowId, 16); 378 } 379 380 Window w = new Window(device, line.substring(index + 1), id); 381 windows.add(w); 382 } 383 } 384 // Automatic refreshing of windows was added in protocol version 3. 385 // Before, the user needed to specify explicitly that he wants to 386 // get the focused window, which was done using a special type of 387 // window with hash code -1. 388 if (serverInfo.protocolVersion < 3) { 389 windows.add(Window.getFocusedWindow(device)); 390 } 391 } catch (Exception e) { 392 Log.e(TAG, "Unable to load the window list from device " + device); 393 } finally { 394 if (connection != null) { 395 connection.close(); 396 } 397 } 398 // The server returns the list of windows from the window at the bottom 399 // to the top. We want the reverse order to put the top window on top of 400 // the list. 401 Window[] returnValue = new Window[windows.size()]; 402 for (int i = windows.size() - 1; i >= 0; i--) { 403 returnValue[returnValue.length - i - 1] = windows.get(i); 404 } 405 return returnValue; 406 } 407 408 /* 409 * This gets the hash code of the window that has focus. Only works with 410 * protocol version 3 and above. 411 */ 412 public static int getFocusedWindow(IDevice device) { 413 DeviceConnection connection = null; 414 try { 415 connection = new DeviceConnection(device); 416 connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$ 417 String line = connection.getInputStream().readLine(); 418 if (line == null || line.length() == 0) { 419 return -1; 420 } 421 return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16); 422 } catch (Exception e) { 423 Log.e(TAG, "Unable to get the focused window from device " + device); 424 } finally { 425 if (connection != null) { 426 connection.close(); 427 } 428 } 429 return -1; 430 } 431 432 public static ViewNode loadWindowData(Window window) { 433 DeviceConnection connection = null; 434 try { 435 connection = new DeviceConnection(window.getDevice()); 436 connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$ 437 BufferedReader in = connection.getInputStream(); 438 ViewNode currentNode = null; 439 int currentDepth = -1; 440 String line; 441 while ((line = in.readLine()) != null) { 442 if ("DONE.".equalsIgnoreCase(line)) { 443 break; 444 } 445 int depth = 0; 446 while (line.charAt(depth) == ' ') { 447 depth++; 448 } 449 while (depth <= currentDepth) { 450 currentNode = currentNode.parent; 451 currentDepth--; 452 } 453 currentNode = new ViewNode(window, currentNode, line.substring(depth)); 454 currentDepth = depth; 455 } 456 if (currentNode == null) { 457 return null; 458 } 459 while (currentNode.parent != null) { 460 currentNode = currentNode.parent; 461 } 462 ViewServerInfo serverInfo = getViewServerInfo(window.getDevice()); 463 if (serverInfo != null) { 464 currentNode.protocolVersion = serverInfo.protocolVersion; 465 } 466 return currentNode; 467 } catch (Exception e) { 468 Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device " 469 + window.getDevice()); 470 } finally { 471 if (connection != null) { 472 connection.close(); 473 } 474 } 475 return null; 476 } 477 478 public static boolean loadProfileData(Window window, ViewNode viewNode) { 479 DeviceConnection connection = null; 480 try { 481 connection = new DeviceConnection(window.getDevice()); 482 connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ 483 BufferedReader in = connection.getInputStream(); 484 int protocol; 485 synchronized (sViewServerInfo) { 486 protocol = sViewServerInfo.get(window.getDevice()).protocolVersion; 487 } 488 if (protocol < 3) { 489 return loadProfileData(viewNode, in); 490 } else { 491 boolean ret = loadProfileDataRecursive(viewNode, in); 492 if (ret) { 493 viewNode.setProfileRatings(); 494 } 495 return ret; 496 } 497 } catch (Exception e) { 498 Log.e(TAG, "Unable to load profiling data for window " + window.getTitle() 499 + " on device " + window.getDevice()); 500 } finally { 501 if (connection != null) { 502 connection.close(); 503 } 504 } 505 return false; 506 } 507 508 private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException { 509 String line; 510 if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$ 511 || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$ 512 return false; 513 } 514 String[] data = line.split(" "); 515 node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0; 516 node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0; 517 node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0; 518 return true; 519 } 520 521 private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in) 522 throws IOException { 523 if (!loadProfileData(node, in)) { 524 return false; 525 } 526 for (int i = 0; i < node.children.size(); i++) { 527 if (!loadProfileDataRecursive(node.children.get(i), in)) { 528 return false; 529 } 530 } 531 return true; 532 } 533 534 public static Image loadCapture(Window window, ViewNode viewNode) { 535 DeviceConnection connection = null; 536 try { 537 connection = new DeviceConnection(window.getDevice()); 538 connection.getSocket().setSoTimeout(5000); 539 connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ 540 return new Image(Display.getDefault(), connection.getSocket().getInputStream()); 541 } catch (Exception e) { 542 Log.e(TAG, "Unable to capture data for node " + viewNode + " in window " 543 + window.getTitle() + " on device " + window.getDevice()); 544 } finally { 545 if (connection != null) { 546 connection.close(); 547 } 548 } 549 return null; 550 } 551 552 public static PsdFile captureLayers(Window window) { 553 DeviceConnection connection = null; 554 DataInputStream in = null; 555 556 try { 557 connection = new DeviceConnection(window.getDevice()); 558 559 connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$ 560 561 in = 562 new DataInputStream(new BufferedInputStream(connection.getSocket() 563 .getInputStream())); 564 565 int width = in.readInt(); 566 int height = in.readInt(); 567 568 PsdFile psd = new PsdFile(width, height); 569 570 while (readLayer(in, psd)) { 571 } 572 573 return psd; 574 } catch (Exception e) { 575 Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device " 576 + window.getDevice()); 577 } finally { 578 if (in != null) { 579 try { 580 in.close(); 581 } catch (Exception ex) { 582 } 583 } 584 connection.close(); 585 } 586 587 return null; 588 } 589 590 private static boolean readLayer(DataInputStream in, PsdFile psd) { 591 try { 592 if (in.read() == 2) { 593 return false; 594 } 595 String name = in.readUTF(); 596 boolean visible = in.read() == 1; 597 int x = in.readInt(); 598 int y = in.readInt(); 599 int dataSize = in.readInt(); 600 601 byte[] data = new byte[dataSize]; 602 int read = 0; 603 while (read < dataSize) { 604 read += in.read(data, read, dataSize - read); 605 } 606 607 ByteArrayInputStream arrayIn = new ByteArrayInputStream(data); 608 BufferedImage chunk = ImageIO.read(arrayIn); 609 610 // Ensure the image is in the right format 611 BufferedImage image = 612 new BufferedImage(chunk.getWidth(), chunk.getHeight(), 613 BufferedImage.TYPE_INT_ARGB); 614 Graphics2D g = image.createGraphics(); 615 g.drawImage(chunk, null, 0, 0); 616 g.dispose(); 617 618 psd.addLayer(name, image, new Point(x, y), visible); 619 620 return true; 621 } catch (Exception e) { 622 return false; 623 } 624 } 625 626 public static void invalidateView(ViewNode viewNode) { 627 DeviceConnection connection = null; 628 try { 629 connection = new DeviceConnection(viewNode.window.getDevice()); 630 connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 631 } catch (Exception e) { 632 Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window 633 + " on device " + viewNode.window.getDevice()); 634 } finally { 635 connection.close(); 636 } 637 } 638 639 public static void requestLayout(ViewNode viewNode) { 640 DeviceConnection connection = null; 641 try { 642 connection = new DeviceConnection(viewNode.window.getDevice()); 643 connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 644 } catch (Exception e) { 645 Log.e(TAG, "Unable to request layout for node " + viewNode + " in window " 646 + viewNode.window + " on device " + viewNode.window.getDevice()); 647 } finally { 648 connection.close(); 649 } 650 } 651 652 public static void outputDisplayList(ViewNode viewNode) { 653 DeviceConnection connection = null; 654 try { 655 connection = new DeviceConnection(viewNode.window.getDevice()); 656 connection.sendCommand("OUTPUT_DISPLAYLIST " + 657 viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ 658 } catch (Exception e) { 659 Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window " 660 + viewNode.window + " on device " + viewNode.window.getDevice()); 661 } finally { 662 connection.close(); 663 } 664 } 665 666 } 667