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         public boolean isCancelled() {
    292             return false;
    293         }
    294     }
    295 
    296     public static ViewServerInfo loadViewServerInfo(IDevice device) {
    297         int server = -1;
    298         int protocol = -1;
    299         DeviceConnection connection = null;
    300         try {
    301             connection = new DeviceConnection(device);
    302             connection.sendCommand("SERVER"); //$NON-NLS-1$
    303             String line = connection.getInputStream().readLine();
    304             if (line != null) {
    305                 server = Integer.parseInt(line);
    306             }
    307         } catch (Exception e) {
    308             Log.e(TAG, "Unable to get view server version from device " + device);
    309         } finally {
    310             if (connection != null) {
    311                 connection.close();
    312             }
    313         }
    314         connection = null;
    315         try {
    316             connection = new DeviceConnection(device);
    317             connection.sendCommand("PROTOCOL"); //$NON-NLS-1$
    318             String line = connection.getInputStream().readLine();
    319             if (line != null) {
    320                 protocol = Integer.parseInt(line);
    321             }
    322         } catch (Exception e) {
    323             Log.e(TAG, "Unable to get view server protocol version from device " + device);
    324         } finally {
    325             if (connection != null) {
    326                 connection.close();
    327             }
    328         }
    329         if (server == -1 || protocol == -1) {
    330             return null;
    331         }
    332         ViewServerInfo returnValue = new ViewServerInfo(server, protocol);
    333         synchronized (sViewServerInfo) {
    334             sViewServerInfo.put(device, returnValue);
    335         }
    336         return returnValue;
    337     }
    338 
    339     public static ViewServerInfo getViewServerInfo(IDevice device) {
    340         synchronized (sViewServerInfo) {
    341             return sViewServerInfo.get(device);
    342         }
    343     }
    344 
    345     public static void removeViewServerInfo(IDevice device) {
    346         synchronized (sViewServerInfo) {
    347             sViewServerInfo.remove(device);
    348         }
    349     }
    350 
    351     /*
    352      * This loads the list of windows from the specified device. The format is:
    353      * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE.
    354      */
    355     public static Window[] loadWindows(IDevice device) {
    356         ArrayList<Window> windows = new ArrayList<Window>();
    357         DeviceConnection connection = null;
    358         ViewServerInfo serverInfo = getViewServerInfo(device);
    359         try {
    360             connection = new DeviceConnection(device);
    361             connection.sendCommand("LIST"); //$NON-NLS-1$
    362             BufferedReader in = connection.getInputStream();
    363             String line;
    364             while ((line = in.readLine()) != null) {
    365                 if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$
    366                     break;
    367                 }
    368 
    369                 int index = line.indexOf(' ');
    370                 if (index != -1) {
    371                     String windowId = line.substring(0, index);
    372 
    373                     int id;
    374                     if (serverInfo.serverVersion > 2) {
    375                         id = (int) Long.parseLong(windowId, 16);
    376                     } else {
    377                         id = Integer.parseInt(windowId, 16);
    378                     }
    379 
    380                     Window w = new Window(device, line.substring(index + 1), id);
    381                     windows.add(w);
    382                 }
    383             }
    384             // Automatic refreshing of windows was added in protocol version 3.
    385             // Before, the user needed to specify explicitly that he wants to
    386             // get the focused window, which was done using a special type of
    387             // window with hash code -1.
    388             if (serverInfo.protocolVersion < 3) {
    389                 windows.add(Window.getFocusedWindow(device));
    390             }
    391         } catch (Exception e) {
    392             Log.e(TAG, "Unable to load the window list from device " + device);
    393         } finally {
    394             if (connection != null) {
    395                 connection.close();
    396             }
    397         }
    398         // The server returns the list of windows from the window at the bottom
    399         // to the top. We want the reverse order to put the top window on top of
    400         // the list.
    401         Window[] returnValue = new Window[windows.size()];
    402         for (int i = windows.size() - 1; i >= 0; i--) {
    403             returnValue[returnValue.length - i - 1] = windows.get(i);
    404         }
    405         return returnValue;
    406     }
    407 
    408     /*
    409      * This gets the hash code of the window that has focus. Only works with
    410      * protocol version 3 and above.
    411      */
    412     public static int getFocusedWindow(IDevice device) {
    413         DeviceConnection connection = null;
    414         try {
    415             connection = new DeviceConnection(device);
    416             connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$
    417             String line = connection.getInputStream().readLine();
    418             if (line == null || line.length() == 0) {
    419                 return -1;
    420             }
    421             return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16);
    422         } catch (Exception e) {
    423             Log.e(TAG, "Unable to get the focused window from device " + device);
    424         } finally {
    425             if (connection != null) {
    426                 connection.close();
    427             }
    428         }
    429         return -1;
    430     }
    431 
    432     public static ViewNode loadWindowData(Window window) {
    433         DeviceConnection connection = null;
    434         try {
    435             connection = new DeviceConnection(window.getDevice());
    436             connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$
    437             BufferedReader in = connection.getInputStream();
    438             ViewNode currentNode = null;
    439             int currentDepth = -1;
    440             String line;
    441             while ((line = in.readLine()) != null) {
    442                 if ("DONE.".equalsIgnoreCase(line)) {
    443                     break;
    444                 }
    445                 int depth = 0;
    446                 while (line.charAt(depth) == ' ') {
    447                     depth++;
    448                 }
    449                 while (depth <= currentDepth) {
    450                     currentNode = currentNode.parent;
    451                     currentDepth--;
    452                 }
    453                 currentNode = new ViewNode(window, currentNode, line.substring(depth));
    454                 currentDepth = depth;
    455             }
    456             if (currentNode == null) {
    457                 return null;
    458             }
    459             while (currentNode.parent != null) {
    460                 currentNode = currentNode.parent;
    461             }
    462             ViewServerInfo serverInfo = getViewServerInfo(window.getDevice());
    463             if (serverInfo != null) {
    464                 currentNode.protocolVersion = serverInfo.protocolVersion;
    465             }
    466             return currentNode;
    467         } catch (Exception e) {
    468             Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device "
    469                     + window.getDevice());
    470         } finally {
    471             if (connection != null) {
    472                 connection.close();
    473             }
    474         }
    475         return null;
    476     }
    477 
    478     public static boolean loadProfileData(Window window, ViewNode viewNode) {
    479         DeviceConnection connection = null;
    480         try {
    481             connection = new DeviceConnection(window.getDevice());
    482             connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    483             BufferedReader in = connection.getInputStream();
    484             int protocol;
    485             synchronized (sViewServerInfo) {
    486                 protocol = sViewServerInfo.get(window.getDevice()).protocolVersion;
    487             }
    488             if (protocol < 3) {
    489                 return loadProfileData(viewNode, in);
    490             } else {
    491                 boolean ret = loadProfileDataRecursive(viewNode, in);
    492                 if (ret) {
    493                     viewNode.setProfileRatings();
    494                 }
    495                 return ret;
    496             }
    497         } catch (Exception e) {
    498             Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
    499                     + " on device " + window.getDevice());
    500         } finally {
    501             if (connection != null) {
    502                 connection.close();
    503             }
    504         }
    505         return false;
    506     }
    507 
    508     private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException {
    509         String line;
    510         if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$
    511                 || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$
    512             return false;
    513         }
    514         String[] data = line.split(" ");
    515         node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0;
    516         node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0;
    517         node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0;
    518         return true;
    519     }
    520 
    521     private static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in)
    522             throws IOException {
    523         if (!loadProfileData(node, in)) {
    524             return false;
    525         }
    526         for (int i = 0; i < node.children.size(); i++) {
    527             if (!loadProfileDataRecursive(node.children.get(i), in)) {
    528                 return false;
    529             }
    530         }
    531         return true;
    532     }
    533 
    534     public static Image loadCapture(Window window, ViewNode viewNode) {
    535         DeviceConnection connection = null;
    536         try {
    537             connection = new DeviceConnection(window.getDevice());
    538             connection.getSocket().setSoTimeout(5000);
    539             connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
    540             return new Image(Display.getDefault(), connection.getSocket().getInputStream());
    541         } catch (Exception e) {
    542             Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
    543                     + window.getTitle() + " on device " + window.getDevice());
    544         } finally {
    545             if (connection != null) {
    546                 connection.close();
    547             }
    548         }
    549         return null;
    550     }
    551 
    552     public static PsdFile captureLayers(Window window) {
    553         DeviceConnection connection = null;
    554         DataInputStream in = null;
    555 
    556         try {
    557             connection = new DeviceConnection(window.getDevice());
    558 
    559             connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$
    560 
    561             in =
    562                     new DataInputStream(new BufferedInputStream(connection.getSocket()
    563                             .getInputStream()));
    564 
    565             int width = in.readInt();
    566             int height = in.readInt();
    567 
    568             PsdFile psd = new PsdFile(width, height);
    569 
    570             while (readLayer(in, psd)) {
    571             }
    572 
    573             return psd;
    574         } catch (Exception e) {
    575             Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device "
    576                     + window.getDevice());
    577         } finally {
    578             if (in != null) {
    579                 try {
    580                     in.close();
    581                 } catch (Exception ex) {
    582                 }
    583             }
    584             connection.close();
    585         }
    586 
    587         return null;
    588     }
    589 
    590     private static boolean readLayer(DataInputStream in, PsdFile psd) {
    591         try {
    592             if (in.read() == 2) {
    593                 return false;
    594             }
    595             String name = in.readUTF();
    596             boolean visible = in.read() == 1;
    597             int x = in.readInt();
    598             int y = in.readInt();
    599             int dataSize = in.readInt();
    600 
    601             byte[] data = new byte[dataSize];
    602             int read = 0;
    603             while (read < dataSize) {
    604                 read += in.read(data, read, dataSize - read);
    605             }
    606 
    607             ByteArrayInputStream arrayIn = new ByteArrayInputStream(data);
    608             BufferedImage chunk = ImageIO.read(arrayIn);
    609 
    610             // Ensure the image is in the right format
    611             BufferedImage image =
    612                     new BufferedImage(chunk.getWidth(), chunk.getHeight(),
    613                             BufferedImage.TYPE_INT_ARGB);
    614             Graphics2D g = image.createGraphics();
    615             g.drawImage(chunk, null, 0, 0);
    616             g.dispose();
    617 
    618             psd.addLayer(name, image, new Point(x, y), visible);
    619 
    620             return true;
    621         } catch (Exception e) {
    622             return false;
    623         }
    624     }
    625 
    626     public static void invalidateView(ViewNode viewNode) {
    627         DeviceConnection connection = null;
    628         try {
    629             connection = new DeviceConnection(viewNode.window.getDevice());
    630             connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    631         } catch (Exception e) {
    632             Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window
    633                     + " on device " + viewNode.window.getDevice());
    634         } finally {
    635             connection.close();
    636         }
    637     }
    638 
    639     public static void requestLayout(ViewNode viewNode) {
    640         DeviceConnection connection = null;
    641         try {
    642             connection = new DeviceConnection(viewNode.window.getDevice());
    643             connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    644         } catch (Exception e) {
    645             Log.e(TAG, "Unable to request layout for node " + viewNode + " in window "
    646                     + viewNode.window + " on device " + viewNode.window.getDevice());
    647         } finally {
    648             connection.close();
    649         }
    650     }
    651 
    652     public static void outputDisplayList(ViewNode viewNode) {
    653         DeviceConnection connection = null;
    654         try {
    655             connection = new DeviceConnection(viewNode.window.getDevice());
    656             connection.sendCommand("OUTPUT_DISPLAYLIST " +
    657                     viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
    658         } catch (Exception e) {
    659             Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window "
    660                     + viewNode.window + " on device " + viewNode.window.getDevice());
    661         } finally {
    662             connection.close();
    663         }
    664     }
    665 
    666 }
    667