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; 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.RawImage; 24 import com.android.ddmlib.TimeoutException; 25 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; 26 import com.android.hierarchyviewerlib.device.DeviceBridge; 27 import com.android.hierarchyviewerlib.device.ViewNode; 28 import com.android.hierarchyviewerlib.device.Window; 29 import com.android.hierarchyviewerlib.device.WindowUpdater; 30 import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo; 31 import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; 32 import com.android.hierarchyviewerlib.models.DeviceSelectionModel; 33 import com.android.hierarchyviewerlib.models.PixelPerfectModel; 34 import com.android.hierarchyviewerlib.models.TreeViewModel; 35 import com.android.hierarchyviewerlib.ui.CaptureDisplay; 36 import com.android.hierarchyviewerlib.ui.TreeView; 37 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; 38 import com.android.hierarchyviewerlib.ui.util.PsdFile; 39 40 import org.eclipse.swt.SWT; 41 import org.eclipse.swt.SWTException; 42 import org.eclipse.swt.graphics.Image; 43 import org.eclipse.swt.graphics.ImageData; 44 import org.eclipse.swt.graphics.ImageLoader; 45 import org.eclipse.swt.graphics.PaletteData; 46 import org.eclipse.swt.widgets.Display; 47 import org.eclipse.swt.widgets.FileDialog; 48 import org.eclipse.swt.widgets.Shell; 49 50 import java.io.FileNotFoundException; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 import java.util.HashSet; 54 import java.util.Timer; 55 import java.util.TimerTask; 56 57 /** 58 * This is the class where most of the logic resides. 59 */ 60 public abstract class HierarchyViewerDirector implements IDeviceChangeListener, 61 IWindowChangeListener { 62 63 protected static HierarchyViewerDirector sDirector; 64 65 public static final String TAG = "hierarchyviewer"; 66 67 private int mPixelPerfectRefreshesInProgress = 0; 68 69 private Timer mPixelPerfectRefreshTimer = new Timer(); 70 71 private boolean mAutoRefresh = false; 72 73 public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5; 74 75 private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL; 76 77 private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask; 78 79 private String mFilterText = ""; //$NON-NLS-1$ 80 81 public void terminate() { 82 WindowUpdater.terminate(); 83 mPixelPerfectRefreshTimer.cancel(); 84 } 85 86 public abstract String getAdbLocation(); 87 88 public static HierarchyViewerDirector getDirector() { 89 return sDirector; 90 } 91 92 /** 93 * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. 94 * @param bridge the bridge object to use 95 */ 96 public void acquireBridge(AndroidDebugBridge bridge) { 97 DeviceBridge.acquireBridge(bridge); 98 } 99 100 /** 101 * Creates an {@link AndroidDebugBridge} connected to adb at the given location. 102 * 103 * If a bridge is already running, this disconnects it and creates a new one. 104 * 105 * @param adbLocation the location to adb. 106 */ 107 public void initDebugBridge() { 108 DeviceBridge.initDebugBridge(getAdbLocation()); 109 } 110 111 public void stopDebugBridge() { 112 DeviceBridge.terminate(); 113 } 114 115 public void populateDeviceSelectionModel() { 116 IDevice[] devices = DeviceBridge.getDevices(); 117 for (IDevice device : devices) { 118 deviceConnected(device); 119 } 120 } 121 122 public void startListenForDevices() { 123 DeviceBridge.startListenForDevices(this); 124 } 125 126 public void stopListenForDevices() { 127 DeviceBridge.stopListenForDevices(this); 128 } 129 130 public abstract void executeInBackground(String taskName, Runnable task); 131 132 public void deviceConnected(final IDevice device) { 133 executeInBackground("Connecting device", new Runnable() { 134 public void run() { 135 if (DeviceSelectionModel.getModel().containsDevice(device)) { 136 windowsChanged(device); 137 } else if (device.isOnline()) { 138 DeviceBridge.setupDeviceForward(device); 139 if (!DeviceBridge.isViewServerRunning(device)) { 140 if (!DeviceBridge.startViewServer(device)) { 141 // Let's do something interesting here... Try again 142 // in 2 seconds. 143 try { 144 Thread.sleep(2000); 145 } catch (InterruptedException e) { 146 } 147 if (!DeviceBridge.startViewServer(device)) { 148 Log.e(TAG, "Unable to debug device " + device); 149 DeviceBridge.removeDeviceForward(device); 150 } else { 151 loadViewServerInfoAndWindows(device); 152 } 153 return; 154 } 155 } 156 loadViewServerInfoAndWindows(device); 157 } 158 } 159 }); 160 } 161 162 private void loadViewServerInfoAndWindows(final IDevice device) { 163 164 ViewServerInfo viewServerInfo = DeviceBridge.loadViewServerInfo(device); 165 if (viewServerInfo == null) { 166 return; 167 } 168 Window[] windows = DeviceBridge.loadWindows(device); 169 DeviceSelectionModel.getModel().addDevice(device, windows, viewServerInfo); 170 if (viewServerInfo.protocolVersion >= 3) { 171 WindowUpdater.startListenForWindowChanges(HierarchyViewerDirector.this, device); 172 focusChanged(device); 173 } 174 175 } 176 177 public void deviceDisconnected(final IDevice device) { 178 executeInBackground("Disconnecting device", new Runnable() { 179 public void run() { 180 ViewServerInfo viewServerInfo = DeviceBridge.getViewServerInfo(device); 181 if (viewServerInfo != null && viewServerInfo.protocolVersion >= 3) { 182 WindowUpdater.stopListenForWindowChanges(HierarchyViewerDirector.this, device); 183 } 184 DeviceBridge.removeDeviceForward(device); 185 DeviceBridge.removeViewServerInfo(device); 186 DeviceSelectionModel.getModel().removeDevice(device); 187 if (PixelPerfectModel.getModel().getDevice() == device) { 188 PixelPerfectModel.getModel().setData(null, null, null); 189 } 190 Window treeViewWindow = TreeViewModel.getModel().getWindow(); 191 if (treeViewWindow != null && treeViewWindow.getDevice() == device) { 192 TreeViewModel.getModel().setData(null, null); 193 mFilterText = ""; //$NON-NLS-1$ 194 } 195 } 196 }); 197 } 198 199 public void deviceChanged(IDevice device, int changeMask) { 200 if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) { 201 deviceConnected(device); 202 } 203 } 204 205 public void windowsChanged(final IDevice device) { 206 executeInBackground("Refreshing windows", new Runnable() { 207 public void run() { 208 if (!DeviceBridge.isViewServerRunning(device)) { 209 if (!DeviceBridge.startViewServer(device)) { 210 Log.e(TAG, "Unable to debug device " + device); 211 return; 212 } 213 } 214 Window[] windows = DeviceBridge.loadWindows(device); 215 DeviceSelectionModel.getModel().updateDevice(device, windows); 216 } 217 }); 218 } 219 220 public void focusChanged(final IDevice device) { 221 executeInBackground("Updating focus", new Runnable() { 222 public void run() { 223 int focusedWindow = DeviceBridge.getFocusedWindow(device); 224 DeviceSelectionModel.getModel().updateFocusedWindow(device, focusedWindow); 225 } 226 }); 227 } 228 229 public void refreshPixelPerfect() { 230 final IDevice device = PixelPerfectModel.getModel().getDevice(); 231 if (device != null) { 232 // Some interesting logic here. We don't want to refresh the pixel 233 // perfect view 1000 times in a row if the focus keeps changing. We 234 // just 235 // want it to refresh following the last focus change. 236 boolean proceed = false; 237 synchronized (this) { 238 if (mPixelPerfectRefreshesInProgress <= 1) { 239 proceed = true; 240 mPixelPerfectRefreshesInProgress++; 241 } 242 } 243 if (proceed) { 244 executeInBackground("Refreshing pixel perfect screenshot", new Runnable() { 245 public void run() { 246 Image screenshotImage = getScreenshotImage(device); 247 if (screenshotImage != null) { 248 PixelPerfectModel.getModel().setImage(screenshotImage); 249 } 250 synchronized (HierarchyViewerDirector.this) { 251 mPixelPerfectRefreshesInProgress--; 252 } 253 } 254 255 }); 256 } 257 } 258 } 259 260 public void refreshPixelPerfectTree() { 261 final IDevice device = PixelPerfectModel.getModel().getDevice(); 262 if (device != null) { 263 executeInBackground("Refreshing pixel perfect tree", new Runnable() { 264 public void run() { 265 ViewNode viewNode = 266 DeviceBridge.loadWindowData(Window.getFocusedWindow(device)); 267 if (viewNode != null) { 268 PixelPerfectModel.getModel().setTree(viewNode); 269 } 270 } 271 272 }); 273 } 274 } 275 276 public void loadPixelPerfectData(final IDevice device) { 277 executeInBackground("Loading pixel perfect data", new Runnable() { 278 public void run() { 279 Image screenshotImage = getScreenshotImage(device); 280 if (screenshotImage != null) { 281 ViewNode viewNode = 282 DeviceBridge.loadWindowData(Window.getFocusedWindow(device)); 283 if (viewNode != null) { 284 PixelPerfectModel.getModel().setData(device, screenshotImage, viewNode); 285 } 286 } 287 } 288 }); 289 } 290 291 private Image getScreenshotImage(IDevice device) { 292 try { 293 final RawImage screenshot = device.getScreenshot(); 294 if (screenshot == null) { 295 return null; 296 } 297 class ImageContainer { 298 public Image image; 299 } 300 final ImageContainer imageContainer = new ImageContainer(); 301 Display.getDefault().syncExec(new Runnable() { 302 public void run() { 303 ImageData imageData = 304 new ImageData(screenshot.width, screenshot.height, screenshot.bpp, 305 new PaletteData(screenshot.getRedMask(), screenshot 306 .getGreenMask(), screenshot.getBlueMask()), 1, 307 screenshot.data); 308 imageContainer.image = new Image(Display.getDefault(), imageData); 309 } 310 }); 311 return imageContainer.image; 312 } catch (IOException e) { 313 Log.e(TAG, "Unable to load screenshot from device " + device); 314 } catch (TimeoutException e) { 315 Log.e(TAG, "Timeout loading screenshot from device " + device); 316 } catch (AdbCommandRejectedException e) { 317 Log.e(TAG, "Adb rejected command to load screenshot from device " + device); 318 } 319 return null; 320 } 321 322 public void loadViewTreeData(final Window window) { 323 executeInBackground("Loading view hierarchy", new Runnable() { 324 public void run() { 325 326 mFilterText = ""; //$NON-NLS-1$ 327 328 ViewNode viewNode = DeviceBridge.loadWindowData(window); 329 if (viewNode != null) { 330 DeviceBridge.loadProfileData(window, viewNode); 331 viewNode.setViewCount(); 332 TreeViewModel.getModel().setData(window, viewNode); 333 } 334 } 335 }); 336 } 337 338 public void loadOverlay(final Shell shell) { 339 Display.getDefault().syncExec(new Runnable() { 340 public void run() { 341 FileDialog fileDialog = new FileDialog(shell, SWT.OPEN); 342 fileDialog.setFilterExtensions(new String[] { 343 "*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$ 344 }); 345 fileDialog.setFilterNames(new String[] { 346 "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)" 347 }); 348 fileDialog.setText("Choose an overlay image"); 349 String fileName = fileDialog.open(); 350 if (fileName != null) { 351 try { 352 Image image = new Image(Display.getDefault(), fileName); 353 PixelPerfectModel.getModel().setOverlayImage(image); 354 } catch (SWTException e) { 355 Log.e(TAG, "Unable to load image from " + fileName); 356 } 357 } 358 } 359 }); 360 } 361 362 public void showCapture(final Shell shell, final ViewNode viewNode) { 363 executeInBackground("Capturing node", new Runnable() { 364 public void run() { 365 final Image image = loadCapture(viewNode); 366 if (image != null) { 367 368 Display.getDefault().syncExec(new Runnable() { 369 public void run() { 370 CaptureDisplay.show(shell, viewNode, image); 371 } 372 }); 373 } 374 } 375 }); 376 } 377 378 public Image loadCapture(ViewNode viewNode) { 379 final Image image = DeviceBridge.loadCapture(viewNode.window, viewNode); 380 if (image != null) { 381 viewNode.image = image; 382 383 // Force the layout viewer to redraw. 384 TreeViewModel.getModel().notifySelectionChanged(); 385 } 386 return image; 387 } 388 389 public void loadCaptureInBackground(final ViewNode viewNode) { 390 executeInBackground("Capturing node", new Runnable() { 391 public void run() { 392 loadCapture(viewNode); 393 } 394 }); 395 } 396 397 public void showCapture(Shell shell) { 398 DrawableViewNode viewNode = TreeViewModel.getModel().getSelection(); 399 if (viewNode != null) { 400 showCapture(shell, viewNode.viewNode); 401 } 402 } 403 404 public void refreshWindows() { 405 executeInBackground("Refreshing windows", new Runnable() { 406 public void run() { 407 IDevice[] devicesA = DeviceSelectionModel.getModel().getDevices(); 408 IDevice[] devicesB = DeviceBridge.getDevices(); 409 HashSet<IDevice> deviceSet = new HashSet<IDevice>(); 410 for (int i = 0; i < devicesB.length; i++) { 411 deviceSet.add(devicesB[i]); 412 } 413 for (int i = 0; i < devicesA.length; i++) { 414 if (deviceSet.contains(devicesA[i])) { 415 windowsChanged(devicesA[i]); 416 deviceSet.remove(devicesA[i]); 417 } else { 418 deviceDisconnected(devicesA[i]); 419 } 420 } 421 for (IDevice device : deviceSet) { 422 deviceConnected(device); 423 } 424 } 425 }); 426 } 427 428 public void loadViewHierarchy() { 429 Window window = DeviceSelectionModel.getModel().getSelectedWindow(); 430 if (window != null) { 431 loadViewTreeData(window); 432 } 433 } 434 435 public void inspectScreenshot() { 436 IDevice device = DeviceSelectionModel.getModel().getSelectedDevice(); 437 if (device != null) { 438 loadPixelPerfectData(device); 439 } 440 } 441 442 public void saveTreeView(final Shell shell) { 443 Display.getDefault().syncExec(new Runnable() { 444 public void run() { 445 final DrawableViewNode viewNode = TreeViewModel.getModel().getTree(); 446 if (viewNode != null) { 447 FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); 448 fileDialog.setFilterExtensions(new String[] { 449 "*.png" //$NON-NLS-1$ 450 }); 451 fileDialog.setFilterNames(new String[] { 452 "Portable Network Graphics File (*.png)" 453 }); 454 fileDialog.setText("Choose where to save the tree image"); 455 final String fileName = fileDialog.open(); 456 if (fileName != null) { 457 executeInBackground("Saving tree view", new Runnable() { 458 public void run() { 459 Image image = TreeView.paintToImage(viewNode); 460 ImageLoader imageLoader = new ImageLoader(); 461 imageLoader.data = new ImageData[] { 462 image.getImageData() 463 }; 464 String extensionedFileName = fileName; 465 if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ 466 extensionedFileName += ".png"; //$NON-NLS-1$ 467 } 468 try { 469 imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); 470 } catch (SWTException e) { 471 Log.e(TAG, "Unable to save tree view as a PNG image at " 472 + fileName); 473 } 474 image.dispose(); 475 } 476 }); 477 } 478 } 479 } 480 }); 481 } 482 483 public void savePixelPerfect(final Shell shell) { 484 Display.getDefault().syncExec(new Runnable() { 485 public void run() { 486 Image untouchableImage = PixelPerfectModel.getModel().getImage(); 487 if (untouchableImage != null) { 488 final ImageData imageData = untouchableImage.getImageData(); 489 FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); 490 fileDialog.setFilterExtensions(new String[] { 491 "*.png" //$NON-NLS-1$ 492 }); 493 fileDialog.setFilterNames(new String[] { 494 "Portable Network Graphics File (*.png)" 495 }); 496 fileDialog.setText("Choose where to save the screenshot"); 497 final String fileName = fileDialog.open(); 498 if (fileName != null) { 499 executeInBackground("Saving pixel perfect", new Runnable() { 500 public void run() { 501 ImageLoader imageLoader = new ImageLoader(); 502 imageLoader.data = new ImageData[] { 503 imageData 504 }; 505 String extensionedFileName = fileName; 506 if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ 507 extensionedFileName += ".png"; //$NON-NLS-1$ 508 } 509 try { 510 imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); 511 } catch (SWTException e) { 512 Log.e(TAG, "Unable to save tree view as a PNG image at " 513 + fileName); 514 } 515 } 516 }); 517 } 518 } 519 } 520 }); 521 } 522 523 public void capturePSD(final Shell shell) { 524 Display.getDefault().syncExec(new Runnable() { 525 public void run() { 526 final Window window = TreeViewModel.getModel().getWindow(); 527 if (window != null) { 528 FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); 529 fileDialog.setFilterExtensions(new String[] { 530 "*.psd" //$NON-NLS-1$ 531 }); 532 fileDialog.setFilterNames(new String[] { 533 "Photoshop Document (*.psd)" 534 }); 535 fileDialog.setText("Choose where to save the window layers"); 536 final String fileName = fileDialog.open(); 537 if (fileName != null) { 538 executeInBackground("Saving window layers", new Runnable() { 539 public void run() { 540 PsdFile psdFile = DeviceBridge.captureLayers(window); 541 if (psdFile != null) { 542 String extensionedFileName = fileName; 543 if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$ 544 extensionedFileName += ".psd"; //$NON-NLS-1$ 545 } 546 try { 547 psdFile.write(new FileOutputStream(extensionedFileName)); 548 } catch (FileNotFoundException e) { 549 Log.e(TAG, "Unable to write to file " + fileName); 550 } 551 } 552 } 553 }); 554 } 555 } 556 } 557 }); 558 } 559 560 public void reloadViewHierarchy() { 561 Window window = TreeViewModel.getModel().getWindow(); 562 if (window != null) { 563 loadViewTreeData(window); 564 } 565 } 566 567 public void invalidateCurrentNode() { 568 final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); 569 if (selectedNode != null) { 570 executeInBackground("Invalidating view", new Runnable() { 571 public void run() { 572 DeviceBridge.invalidateView(selectedNode.viewNode); 573 } 574 }); 575 } 576 } 577 578 public void relayoutCurrentNode() { 579 final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); 580 if (selectedNode != null) { 581 executeInBackground("Request layout", new Runnable() { 582 public void run() { 583 DeviceBridge.requestLayout(selectedNode.viewNode); 584 } 585 }); 586 } 587 } 588 589 public void dumpDisplayListForCurrentNode() { 590 final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); 591 if (selectedNode != null) { 592 executeInBackground("Dump displaylist", new Runnable() { 593 public void run() { 594 DeviceBridge.outputDisplayList(selectedNode.viewNode); 595 } 596 }); 597 } 598 } 599 600 public void loadAllViews() { 601 executeInBackground("Loading all views", new Runnable() { 602 public void run() { 603 DrawableViewNode tree = TreeViewModel.getModel().getTree(); 604 if (tree != null) { 605 loadViewRecursive(tree.viewNode); 606 // Force the layout viewer to redraw. 607 TreeViewModel.getModel().notifySelectionChanged(); 608 } 609 } 610 }); 611 } 612 613 private void loadViewRecursive(ViewNode viewNode) { 614 Image image = DeviceBridge.loadCapture(viewNode.window, viewNode); 615 if (image == null) { 616 return; 617 } 618 viewNode.image = image; 619 final int N = viewNode.children.size(); 620 for (int i = 0; i < N; i++) { 621 loadViewRecursive(viewNode.children.get(i)); 622 } 623 } 624 625 public void filterNodes(String filterText) { 626 this.mFilterText = filterText; 627 DrawableViewNode tree = TreeViewModel.getModel().getTree(); 628 if (tree != null) { 629 tree.viewNode.filter(filterText); 630 // Force redraw 631 TreeViewModel.getModel().notifySelectionChanged(); 632 } 633 } 634 635 public String getFilterText() { 636 return mFilterText; 637 } 638 639 private static class PixelPerfectAutoRefreshTask extends TimerTask { 640 @Override 641 public void run() { 642 HierarchyViewerDirector.getDirector().refreshPixelPerfect(); 643 } 644 }; 645 646 public void setPixelPerfectAutoRefresh(boolean value) { 647 synchronized (mPixelPerfectRefreshTimer) { 648 if (value == mAutoRefresh) { 649 return; 650 } 651 mAutoRefresh = value; 652 if (mAutoRefresh) { 653 mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); 654 mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, 655 mPixelPerfectAutoRefreshInterval * 1000, 656 mPixelPerfectAutoRefreshInterval * 1000); 657 } else { 658 mCurrentAutoRefreshTask.cancel(); 659 mCurrentAutoRefreshTask = null; 660 } 661 } 662 } 663 664 public void setPixelPerfectAutoRefreshInterval(int value) { 665 synchronized (mPixelPerfectRefreshTimer) { 666 if (mPixelPerfectAutoRefreshInterval == value) { 667 return; 668 } 669 mPixelPerfectAutoRefreshInterval = value; 670 if (mAutoRefresh) { 671 mCurrentAutoRefreshTask.cancel(); 672 long timeLeft = 673 Math.max(0, mPixelPerfectAutoRefreshInterval 674 * 1000 675 - (System.currentTimeMillis() - mCurrentAutoRefreshTask 676 .scheduledExecutionTime())); 677 mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); 678 mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft, 679 mPixelPerfectAutoRefreshInterval * 1000); 680 } 681 } 682 } 683 684 public int getPixelPerfectAutoRefreshInverval() { 685 return mPixelPerfectAutoRefreshInterval; 686 } 687 } 688