Home | History | Annotate | Download | only in device
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.hierarchyviewerlib.device;
     18 
     19 import com.android.ddmlib.AdbCommandRejectedException;
     20 import com.android.ddmlib.AndroidDebugBridge;
     21 import com.android.ddmlib.IDevice;
     22 import com.android.ddmlib.Log;
     23 import com.android.ddmlib.MultiLineReceiver;
     24 import com.android.ddmlib.ShellCommandUnresponsiveException;
     25 import com.android.ddmlib.TimeoutException;
     26 import com.android.hierarchyviewerlib.ui.util.PsdFile;
     27 
     28 import org.eclipse.swt.graphics.Image;
     29 import org.eclipse.swt.widgets.Display;
     30 
     31 import java.awt.Graphics2D;
     32 import java.awt.Point;
     33 import java.awt.image.BufferedImage;
     34 import java.io.BufferedInputStream;
     35 import java.io.BufferedReader;
     36 import java.io.ByteArrayInputStream;
     37 import java.io.DataInputStream;
     38 import java.io.IOException;
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 import java.util.regex.Matcher;
     42 import java.util.regex.Pattern;
     43 
     44 import javax.imageio.ImageIO;
     45 
     46 /**
     47  * A bridge to the device.
     48  */
     49 public class DeviceBridge {
     50 
     51     public static final String TAG = "hierarchyviewer";
     52 
     53     private static final int DEFAULT_SERVER_PORT = 4939;
     54 
     55     // These codes must match the auto-generated codes in IWindowManager.java
     56     // See IWindowManager.aidl as well
     57     private static final int SERVICE_CODE_START_SERVER = 1;
     58 
     59     private static final int SERVICE_CODE_STOP_SERVER = 2;
     60 
     61     private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3;
     62 
     63     private static AndroidDebugBridge sBridge;
     64 
     65     private static final HashMap<IDevice, Integer> sDevicePortMap = new HashMap<IDevice, Integer>();
     66 
     67     private static final HashMap<IDevice, ViewServerInfo> sViewServerInfo =
     68             new HashMap<IDevice, ViewServerInfo>();
     69 
     70     private static int sNextLocalPort = DEFAULT_SERVER_PORT;
     71 
     72     public static class ViewServerInfo {
     73         public final int protocolVersion;
     74 
     75         public final int serverVersion;
     76 
     77         ViewServerInfo(int serverVersion, int protocolVersion) {
     78             this.protocolVersion = protocolVersion;
     79             this.serverVersion = serverVersion;
     80         }
     81     }
     82 
     83     /**
     84      * Init the DeviceBridge with an existing {@link AndroidDebugBridge}.
     85      * @param bridge the bridge object to use
     86      */
     87     public static void acquireBridge(AndroidDebugBridge bridge) {
     88         sBridge = bridge;
     89     }
     90 
     91     /**
     92      * Creates an {@link AndroidDebugBridge} connected to adb at the given location.
     93      *
     94      * If a bridge is already running, this disconnects it and creates a new one.
     95      *
     96      * @param adbLocation the location to adb.
     97      */
     98     public static void initDebugBridge(String adbLocation) {
     99         if (sBridge == null) {
    100             AndroidDebugBridge.init(false /* debugger support */);
    101         }
    102         if (sBridge == null || !sBridge.isConnected()) {
    103             sBridge = AndroidDebugBridge.createBridge(adbLocation, true);
    104         }
    105     }
    106 
    107     /** Disconnects the current {@link AndroidDebugBridge}. */
    108     public static void terminate() {
    109         AndroidDebugBridge.terminate();
    110     }
    111 
    112     public static IDevice[] getDevices() {
    113         if (sBridge == null) {
    114             return new IDevice[0];
    115         }
    116         return sBridge.getDevices();
    117     }
    118 
    119     /*
    120      * This adds a listener to the debug bridge. The listener is notified of
    121      * connecting/disconnecting devices, devices coming online, etc.
    122      */
    123     public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
    124         AndroidDebugBridge.addDeviceChangeListener(listener);
    125     }
    126 
    127     public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
    128         AndroidDebugBridge.removeDeviceChangeListener(listener);
    129     }
    130 
    131     /**
    132      * Sets up a just-connected device to work with the view server.
    133      * <p/>
    134      * This starts a port forwarding between a local port and a port on the
    135      * device.
    136      *
    137      * @param device
    138      */
    139     public static void setupDeviceForward(IDevice device) {
    140         synchronized (sDevicePortMap) {
    141             if (device.getState() == IDevice.DeviceState.ONLINE) {
    142                 int localPort = sNextLocalPort++;
    143                 try {
    144                     device.createForward(localPort, DEFAULT_SERVER_PORT);
    145                     sDevicePortMap.put(device, localPort);
    146                 } catch (TimeoutException e) {
    147                     Log.e(TAG, "Timeout setting up port forwarding for " + device);
    148                 } catch (AdbCommandRejectedException e) {
    149                     Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s",
    150                             device, e.getMessage()));
    151                 } catch (IOException e) {
    152                     Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s",
    153                             device, e.getMessage()));
    154                 }
    155             }
    156         }
    157     }
    158 
    159     public static void removeDeviceForward(IDevice device) {
    160         synchronized (sDevicePortMap) {
    161             final Integer localPort = sDevicePortMap.get(device);
    162             if (localPort != null) {
    163                 try {
    164                     device.removeForward(localPort, DEFAULT_SERVER_PORT);
    165                     sDevicePortMap.remove(device);
    166                 } catch (TimeoutException e) {
    167                     Log.e(TAG, "Timeout removing port forwarding for " + device);
    168                 } catch (AdbCommandRejectedException e) {
    169                     // In this case, we want to fail silently.
    170                 } catch (IOException e) {
    171                     Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s",
    172                             device, e.getMessage()));
    173                 }
    174             }
    175         }
    176     }
    177 
    178     public static int getDeviceLocalPort(IDevice device) {
    179         synchronized (sDevicePortMap) {
    180             Integer port = sDevicePortMap.get(device);
    181             if (port != null) {
    182                 return port;
    183             }
    184 
    185             Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber());
    186             return -1;
    187         }
    188 
    189     }
    190 
    191     public static boolean isViewServerRunning(IDevice device) {
    192         final boolean[] result = new boolean[1];
    193         try {
    194             if (device.isOnline()) {
    195                 device.executeShellCommand(buildIsServerRunningShellCommand(),
    196                         new BooleanResultReader(result));
    197                 if (!result[0]) {
    198                     ViewServerInfo serverInfo = loadViewServerInfo(device);
    199                     if (serverInfo != null && serverInfo.protocolVersion > 2) {
    200                         result[0] = true;
    201                     }
    202                 }
    203             }
    204         } catch (TimeoutException e) {
    205             Log.e(TAG, "Timeout checking status of view server on device " + device);
    206         } catch (IOException e) {
    207             Log.e(TAG, "Unable to check status of view server on device " + device);
    208         } catch (AdbCommandRejectedException e) {
    209             Log.e(TAG, "Adb rejected command to check status of view server on device " + device);
    210         } catch (ShellCommandUnresponsiveException e) {
    211             Log.e(TAG, "Unable to execute command to check status of view server on device "
    212                     + device);
    213         }
    214         return result[0];
    215     }
    216 
    217     public static boolean startViewServer(IDevice device) {
    218         return startViewServer(device, DEFAULT_SERVER_PORT);
    219     }
    220 
    221     public static boolean startViewServer(IDevice device, int port) {
    222         final boolean[] result = new boolean[1];
    223         try {
    224             if (device.isOnline()) {
    225                 device.executeShellCommand(buildStartServerShellCommand(port),
    226                         new BooleanResultReader(result));
    227             }
    228         } catch (TimeoutException e) {
    229             Log.e(TAG, "Timeout starting view server on device " + device);
    230         } catch (IOException e) {
    231             Log.e(TAG, "Unable to start view server on device " + device);
    232         } catch (AdbCommandRejectedException e) {
    233             Log.e(TAG, "Adb rejected command to start view server on device " + device);
    234         } catch (ShellCommandUnresponsiveException e) {
    235             Log.e(TAG, "Unable to execute command to start view server on device " + device);
    236         }
    237         return result[0];
    238     }
    239 
    240     public static boolean stopViewServer(IDevice device) {
    241         final boolean[] result = new boolean[1];
    242         try {
    243             if (device.isOnline()) {
    244                 device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader(
    245                         result));
    246             }
    247         } catch (TimeoutException e) {
    248             Log.e(TAG, "Timeout stopping view server on device " + device);
    249         } catch (IOException e) {
    250             Log.e(TAG, "Unable to stop view server on device " + device);
    251         } catch (AdbCommandRejectedException e) {
    252             Log.e(TAG, "Adb rejected command to stop view server on device " + device);
    253         } catch (ShellCommandUnresponsiveException e) {
    254             Log.e(TAG, "Unable to execute command to stop view server on device " + device);
    255         }
    256         return result[0];
    257     }
    258 
    259     private static String buildStartServerShellCommand(int port) {
    260         return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$
    261     }
    262 
    263     private static String buildStopServerShellCommand() {
    264         return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$
    265     }
    266 
    267     private static String buildIsServerRunningShellCommand() {
    268         return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$
    269     }
    270 
    271     private static class BooleanResultReader extends MultiLineReceiver {
    272         private final boolean[] mResult;
    273 
    274         public BooleanResultReader(boolean[] result) {
    275             mResult = result;
    276         }
    277 
    278         @Override
    279         public void processNewLines(String[] strings) {
    280             if (strings.length > 0) {
    281                 Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$
    282                 Matcher matcher = pattern.matcher(strings[0]);
    283                 if (matcher.matches()) {
    284                     if (Integer.parseInt(matcher.group(1)) == 1) {
    285                         mResult[0] = true;
    286                     }
    287                 }
    288             }
    289         }
    290 
    291         @Override
    292         public boolean isCancelled() {
    293             return false;
    294         }
    295     }
    296 
    297     public static ViewServerInfo loadViewServerInfo(IDevice device) {
    298         int server = -1;
    299         int protocol = -1;
    300         DeviceConnection connection = null;
    301         try {
    302             connection = new DeviceConnection(device);
    303             connection.sendCommand("SERVER"); //$NON-NLS-1$
    304             String line = connection.getInputStream().readLine();
    305             if (line != null) {
    306                 server = Integer.parseInt(line);
    307             }
    308         } catch (Exception e) {
    309             Log.e(TAG, "Unable to get view server version from device " + device);
    310         } finally {
    311             if (connection != null) {
    312                 connection.close();
    313             }
    314         }
    315         connection = null;
    316         try {
    317             connection = new DeviceConnection(device);
    318             connection.sendCommand("PROTOCOL"); //$NON-NLS-1$
    319             String line = connection.getInputStream().readLine();
    320             if (line != null) {
    321                 protocol = Integer.parseInt(line);
    322             }
    323         } catch (Exception e) {
    324             Log.e(TAG, "Unable to get view server protocol version from device " + device);
    325         } finally {
    326             if (connection != null) {
    327                 connection.close();
    328             }
    329         }
    330         if (server == -1 || protocol == -1) {
    331             return null;
    332         }
    333         ViewServerInfo returnValue = new ViewServerInfo(server, protocol);
    334         synchronized (sViewServerInfo) {
    335             sViewServerInfo.put(device, returnValue);
    336         }
    337         return returnValue;
    338     }
    339 
    340     public static ViewServerInfo getViewServerInfo(IDevice device) {
    341         synchronized (sViewServerInfo) {
    342             return sViewServerInfo.get(device);
    343         }
    344     }
    345 
    346     public static void removeViewServerInfo(IDevice device) {
    347         synchronized (sViewServerInfo) {
    348             sViewServerInfo.remove(device);
    349         }
    350     }
    351 
    352     /*
    353      * This loads the list of windows from the specified device. The format is:
    354      * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE.
    355      */
    356     public static Window[] loadWindows(IDevice device) {
    357         ArrayList<Window> windows = new ArrayList<Window>();
    358         DeviceConnection connection = null;
    359         ViewServerInfo serverInfo = getViewServerInfo(device);
    360         try {
    361             connection = new DeviceConnection(device);
    362             connection.sendCommand("LIST"); //$NON-NLS-1$
    363             BufferedReader in = connection.getInputStream();
    364             String line;
    365             while ((line = in.readLine()) != null) {
    366                 if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$
    367                     break;
    368                 }
    369 
    370                 int index = line.indexOf(' ');
    371                 if (index != -1) {
    372                     String windowId = line.substring(0, index);
    373 
    374                     int id;
    375                     if (serverInfo.serverVersion > 2) {
    376                         id = (int) Long.parseLong(windowId, 16);
    377                     } else {
    378                         id = Integer.parseInt(windowId, 16);
    379                     }
    380 
    381                     Window w = new Window(device, line.substring(index + 1), id);
    382                     windows.add(w);
    383                 }
    384             }
    385             // Automatic refreshing of windows was added in protocol version 3.
    386             // Before, the user needed to specify explicitly that he wants to
    387             // get the focused window, which was done using a special type of
    388             // window with hash code -1.
    389             if (serverInfo.protocolVersion < 3) {
    390                 windows.add(Window.getFocusedWindow(device));
    391             }
    392         } catch (Exception e) {
    393             Log.e(TAG, "Unable to load the window list from device " + device);
    394         } finally {
    395             if (connection != null) {
    396                 connection.close();
    397             }
    398         }
    399         // The server returns the list of windows from the window at the bottom
    400         // to the top. We want the reverse order to put the top window on top of
    401         // the list.
    402         Window[] returnValue = new Window[windows.size()];
    403         for (int i = windows.size() - 1; i >= 0; i--) {
    404             returnValue[returnValue.length - i - 1] = windows.get(i);
    405         }
    406         return returnValue;
    407     }
    408 
    409     /*
    410      * This gets the hash code of the window that has focus. Only works with
    411      * protocol version 3 and above.
    412      */
    413     public static int getFocusedWindow(IDevice device) {
    414         DeviceConnection connection = null;
    415         try {
    416             connection = new DeviceConnection(device);
    417             connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$
    418             String line = connection.getInputStream().readLine();
    419             if (line == null || line.length() == 0) {
    420                 return -1;
    421             }
    422             return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16);
    423         } catch (Exception e) {
    424             Log.e(TAG, "Unable to get the focused window from device " + device);
    425         } finally {
    426             if (connection != null) {
    427                 connection.close();
    428             }
    429         }
    430         return -1;
    431     }
    432 
    433     public static ViewNode loadWindowData(Window window) {
    434         DeviceConnection connection = null;
    435         try {
    436             connection = new DeviceConnection(window.getDevice());
    437             connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$
    438             BufferedReader in = connection.getInputStream();
    439             ViewNode currentNode = null;
    440             int currentDepth = -1;
    441             String line;
    442             while ((line = in.readLine()) != null) {
    443                 if ("DONE.".equalsIgnoreCase(line)) {
    444                     break;
    445                 }
    446                 int depth = 0;
    447                 while (line.charAt(depth) == ' ') {
    448                     depth++;
    449                 }
    450                 while (depth <= currentDepth) {
    451                     currentNode = currentNode.parent;
    452                     currentDepth--;
    453                 }
    454                 currentNode = new ViewNode(window, currentNode, line.substring(depth));
    455                 currentDepth = depth;
    456             }
    457             if (currentNode == null) {
    458                 return null;
    459             }
    460             while (currentNode.parent != null) {
    461                 currentNode = currentNode.parent;
    462             }
    463             ViewServerInfo serverInfo = getViewServerInfo(window.getDevice());
    464             if (serverInfo != null) {
    465                 currentNode.protocolVersion = serverInfo.protocolVersion;
    466             }
    467             return currentNode;
    468         } catch (Exception e) {
    469             Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device "
    470                     + window.getDevice());
    471             Log.e(TAG, e.getMessage());
    472         } finally {
    473             if (connection != null) {
    474                 connection.close();
    475             }
    476         }
    477         return null;
    478     }
    479 
    480     public static boolean loadProfileData(Window window, ViewNode viewNode) {
    481         DeviceConnection connection = null;
    482         try {
    483             connection = new DeviceConnection(window.getDevice());
    484             connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    485             BufferedReader in = connection.getInputStream();
    486             int protocol;
    487             synchronized (sViewServerInfo) {
    488                 protocol = sViewServerInfo.get(window.getDevice()).protocolVersion;
    489             }
    490             if (protocol < 3) {
    491                 return loadProfileData(viewNode, in);
    492             } else {
    493                 boolean ret = loadProfileDataRecursive(viewNode, in);
    494                 if (ret) {
    495                     viewNode.setProfileRatings();
    496                 }
    497                 return ret;
    498             }
    499         } catch (Exception e) {
    500             Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
    501                     + " on device " + window.getDevice());
    502         } finally {
    503             if (connection != null) {
    504                 connection.close();
    505             }
    506         }
    507         return false;
    508     }
    509 
    510     private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException {
    511         String line;
    512         if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$
    513                 || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$
    514             return false;
    515         }
    516         String[] data = line.split(" ");
    517         node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0;
    518         node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0;
    519         node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0;
    520         return true;
    521     }
    522 
    523     private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in)
    524             throws IOException {
    525         if (!loadProfileData(node, in)) {
    526             return false;
    527         }
    528         for (int i = 0; i < node.children.size(); i++) {
    529             if (!loadProfileDataRecursive(node.children.get(i), in)) {
    530                 return false;
    531             }
    532         }
    533         return true;
    534     }
    535 
    536     public static Image loadCapture(Window window, ViewNode viewNode) {
    537         DeviceConnection connection = null;
    538         try {
    539             connection = new DeviceConnection(window.getDevice());
    540             connection.getSocket().setSoTimeout(5000);
    541             connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    542             return new Image(Display.getDefault(), connection.getSocket().getInputStream());
    543         } catch (Exception e) {
    544             Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
    545                     + window.getTitle() + " on device " + window.getDevice());
    546         } finally {
    547             if (connection != null) {
    548                 connection.close();
    549             }
    550         }
    551         return null;
    552     }
    553 
    554     public static PsdFile captureLayers(Window window) {
    555         DeviceConnection connection = null;
    556         DataInputStream in = null;
    557 
    558         try {
    559             connection = new DeviceConnection(window.getDevice());
    560 
    561             connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$
    562 
    563             in =
    564                     new DataInputStream(new BufferedInputStream(connection.getSocket()
    565                             .getInputStream()));
    566 
    567             int width = in.readInt();
    568             int height = in.readInt();
    569 
    570             PsdFile psd = new PsdFile(width, height);
    571 
    572             while (readLayer(in, psd)) {
    573             }
    574 
    575             return psd;
    576         } catch (Exception e) {
    577             Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device "
    578                     + window.getDevice());
    579         } finally {
    580             if (in != null) {
    581                 try {
    582                     in.close();
    583                 } catch (Exception ex) {
    584                 }
    585             }
    586             connection.close();
    587         }
    588 
    589         return null;
    590     }
    591 
    592     private static boolean readLayer(DataInputStream in, PsdFile psd) {
    593         try {
    594             if (in.read() == 2) {
    595                 return false;
    596             }
    597             String name = in.readUTF();
    598             boolean visible = in.read() == 1;
    599             int x = in.readInt();
    600             int y = in.readInt();
    601             int dataSize = in.readInt();
    602 
    603             byte[] data = new byte[dataSize];
    604             int read = 0;
    605             while (read < dataSize) {
    606                 read += in.read(data, read, dataSize - read);
    607             }
    608 
    609             ByteArrayInputStream arrayIn = new ByteArrayInputStream(data);
    610             BufferedImage chunk = ImageIO.read(arrayIn);
    611 
    612             // Ensure the image is in the right format
    613             BufferedImage image =
    614                     new BufferedImage(chunk.getWidth(), chunk.getHeight(),
    615                             BufferedImage.TYPE_INT_ARGB);
    616             Graphics2D g = image.createGraphics();
    617             g.drawImage(chunk, null, 0, 0);
    618             g.dispose();
    619 
    620             psd.addLayer(name, image, new Point(x, y), visible);
    621 
    622             return true;
    623         } catch (Exception e) {
    624             return false;
    625         }
    626     }
    627 
    628     public static void invalidateView(ViewNode viewNode) {
    629         DeviceConnection connection = null;
    630         try {
    631             connection = new DeviceConnection(viewNode.window.getDevice());
    632             connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    633         } catch (Exception e) {
    634             Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window
    635                     + " on device " + viewNode.window.getDevice());
    636         } finally {
    637             connection.close();
    638         }
    639     }
    640 
    641     public static void requestLayout(ViewNode viewNode) {
    642         DeviceConnection connection = null;
    643         try {
    644             connection = new DeviceConnection(viewNode.window.getDevice());
    645             connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    646         } catch (Exception e) {
    647             Log.e(TAG, "Unable to request layout for node " + viewNode + " in window "
    648                     + viewNode.window + " on device " + viewNode.window.getDevice());
    649         } finally {
    650             connection.close();
    651         }
    652     }
    653 
    654     public static void outputDisplayList(ViewNode viewNode) {
    655         DeviceConnection connection = null;
    656         try {
    657             connection = new DeviceConnection(viewNode.window.getDevice());
    658             connection.sendCommand("OUTPUT_DISPLAYLIST " +
    659                     viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    660         } catch (Exception e) {
    661             Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window "
    662                     + viewNode.window + " on device " + viewNode.window.getDevice());
    663         } finally {
    664             connection.close();
    665         }
    666     }
    667 
    668 }
    669