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