Home | History | Annotate | Download | only in hierarchyviewerlib
      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