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.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