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         } finally {
    472             if (connection != null) {
    473                 connection.close();
    474             }
    475         }
    476         return null;
    477     }
    478 
    479     public static boolean loadProfileData(Window window, ViewNode viewNode) {
    480         DeviceConnection connection = null;
    481         try {
    482             connection = new DeviceConnection(window.getDevice());
    483             connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    484             BufferedReader in = connection.getInputStream();
    485             int protocol;
    486             synchronized (sViewServerInfo) {
    487                 protocol = sViewServerInfo.get(window.getDevice()).protocolVersion;
    488             }
    489             if (protocol < 3) {
    490                 return loadProfileData(viewNode, in);
    491             } else {
    492                 boolean ret = loadProfileDataRecursive(viewNode, in);
    493                 if (ret) {
    494                     viewNode.setProfileRatings();
    495                 }
    496                 return ret;
    497             }
    498         } catch (Exception e) {
    499             Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
    500                     + " on device " + window.getDevice());
    501         } finally {
    502             if (connection != null) {
    503                 connection.close();
    504             }
    505         }
    506         return false;
    507     }
    508 
    509     private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException {
    510         String line;
    511         if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$
    512                 || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$
    513             return false;
    514         }
    515         String[] data = line.split(" ");
    516         node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0;
    517         node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0;
    518         node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0;
    519         return true;
    520     }
    521 
    522     private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in)
    523             throws IOException {
    524         if (!loadProfileData(node, in)) {
    525             return false;
    526         }
    527         for (int i = 0; i < node.children.size(); i++) {
    528             if (!loadProfileDataRecursive(node.children.get(i), in)) {
    529                 return false;
    530             }
    531         }
    532         return true;
    533     }
    534 
    535     public static Image loadCapture(Window window, ViewNode viewNode) {
    536         DeviceConnection connection = null;
    537         try {
    538             connection = new DeviceConnection(window.getDevice());
    539             connection.getSocket().setSoTimeout(5000);
    540             connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    541             return new Image(Display.getDefault(), connection.getSocket().getInputStream());
    542         } catch (Exception e) {
    543             Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
    544                     + window.getTitle() + " on device " + window.getDevice());
    545         } finally {
    546             if (connection != null) {
    547                 connection.close();
    548             }
    549         }
    550         return null;
    551     }
    552 
    553     public static PsdFile captureLayers(Window window) {
    554         DeviceConnection connection = null;
    555         DataInputStream in = null;
    556 
    557         try {
    558             connection = new DeviceConnection(window.getDevice());
    559 
    560             connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$
    561 
    562             in =
    563                     new DataInputStream(new BufferedInputStream(connection.getSocket()
    564                             .getInputStream()));
    565 
    566             int width = in.readInt();
    567             int height = in.readInt();
    568 
    569             PsdFile psd = new PsdFile(width, height);
    570 
    571             while (readLayer(in, psd)) {
    572             }
    573 
    574             return psd;
    575         } catch (Exception e) {
    576             Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device "
    577                     + window.getDevice());
    578         } finally {
    579             if (in != null) {
    580                 try {
    581                     in.close();
    582                 } catch (Exception ex) {
    583                 }
    584             }
    585             connection.close();
    586         }
    587 
    588         return null;
    589     }
    590 
    591     private static boolean readLayer(DataInputStream in, PsdFile psd) {
    592         try {
    593             if (in.read() == 2) {
    594                 return false;
    595             }
    596             String name = in.readUTF();
    597             boolean visible = in.read() == 1;
    598             int x = in.readInt();
    599             int y = in.readInt();
    600             int dataSize = in.readInt();
    601 
    602             byte[] data = new byte[dataSize];
    603             int read = 0;
    604             while (read < dataSize) {
    605                 read += in.read(data, read, dataSize - read);
    606             }
    607 
    608             ByteArrayInputStream arrayIn = new ByteArrayInputStream(data);
    609             BufferedImage chunk = ImageIO.read(arrayIn);
    610 
    611             // Ensure the image is in the right format
    612             BufferedImage image =
    613                     new BufferedImage(chunk.getWidth(), chunk.getHeight(),
    614                             BufferedImage.TYPE_INT_ARGB);
    615             Graphics2D g = image.createGraphics();
    616             g.drawImage(chunk, null, 0, 0);
    617             g.dispose();
    618 
    619             psd.addLayer(name, image, new Point(x, y), visible);
    620 
    621             return true;
    622         } catch (Exception e) {
    623             return false;
    624         }
    625     }
    626 
    627     public static void invalidateView(ViewNode viewNode) {
    628         DeviceConnection connection = null;
    629         try {
    630             connection = new DeviceConnection(viewNode.window.getDevice());
    631             connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    632         } catch (Exception e) {
    633             Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window
    634                     + " on device " + viewNode.window.getDevice());
    635         } finally {
    636             connection.close();
    637         }
    638     }
    639 
    640     public static void requestLayout(ViewNode viewNode) {
    641         DeviceConnection connection = null;
    642         try {
    643             connection = new DeviceConnection(viewNode.window.getDevice());
    644             connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    645         } catch (Exception e) {
    646             Log.e(TAG, "Unable to request layout for node " + viewNode + " in window "
    647                     + viewNode.window + " on device " + viewNode.window.getDevice());
    648         } finally {
    649             connection.close();
    650         }
    651     }
    652 
    653     public static void outputDisplayList(ViewNode viewNode) {
    654         DeviceConnection connection = null;
    655         try {
    656             connection = new DeviceConnection(viewNode.window.getDevice());
    657             connection.sendCommand("OUTPUT_DISPLAYLIST " +
    658                     viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    659         } catch (Exception e) {
    660             Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window "
    661                     + viewNode.window + " on device " + viewNode.window.getDevice());
    662         } finally {
    663             connection.close();
    664         }
    665     }
    666 
    667 }
    668