Home | History | Annotate | Download | only in ddmuilib
      1 /*
      2  * Copyright (C) 2007 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.ddmuilib;
     18 
     19 import com.android.ddmlib.AndroidDebugBridge;
     20 import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
     21 import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
     22 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
     23 import com.android.ddmlib.Client;
     24 import com.android.ddmlib.ClientData;
     25 import com.android.ddmlib.ClientData.DebuggerStatus;
     26 import com.android.ddmlib.DdmPreferences;
     27 import com.android.ddmlib.IDevice;
     28 import com.android.ddmlib.IDevice.DeviceState;
     29 
     30 import org.eclipse.jface.preference.IPreferenceStore;
     31 import org.eclipse.jface.viewers.ILabelProviderListener;
     32 import org.eclipse.jface.viewers.ITableLabelProvider;
     33 import org.eclipse.jface.viewers.ITreeContentProvider;
     34 import org.eclipse.jface.viewers.TreePath;
     35 import org.eclipse.jface.viewers.TreeSelection;
     36 import org.eclipse.jface.viewers.TreeViewer;
     37 import org.eclipse.jface.viewers.Viewer;
     38 import org.eclipse.swt.SWT;
     39 import org.eclipse.swt.SWTException;
     40 import org.eclipse.swt.events.SelectionAdapter;
     41 import org.eclipse.swt.events.SelectionEvent;
     42 import org.eclipse.swt.graphics.Image;
     43 import org.eclipse.swt.layout.FillLayout;
     44 import org.eclipse.swt.widgets.Composite;
     45 import org.eclipse.swt.widgets.Control;
     46 import org.eclipse.swt.widgets.Display;
     47 import org.eclipse.swt.widgets.Tree;
     48 import org.eclipse.swt.widgets.TreeColumn;
     49 import org.eclipse.swt.widgets.TreeItem;
     50 
     51 import java.util.ArrayList;
     52 
     53 /**
     54  * A display of both the devices and their clients.
     55  */
     56 public final class DevicePanel extends Panel implements IDebugBridgeChangeListener,
     57         IDeviceChangeListener, IClientChangeListener {
     58 
     59     private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$
     60     private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$
     61     private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$
     62 
     63     private final static int DEVICE_COL_SERIAL = 0;
     64     private final static int DEVICE_COL_STATE = 1;
     65     // col 2, 3 not used.
     66     private final static int DEVICE_COL_BUILD = 4;
     67 
     68     private final static int CLIENT_COL_NAME = 0;
     69     private final static int CLIENT_COL_PID = 1;
     70     private final static int CLIENT_COL_THREAD = 2;
     71     private final static int CLIENT_COL_HEAP = 3;
     72     private final static int CLIENT_COL_PORT = 4;
     73 
     74     public final static int ICON_WIDTH = 16;
     75     public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$
     76     public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$
     77     public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$
     78     public final static String ICON_GC = "gc.png"; //$NON-NLS-1$
     79     public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$
     80     public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$
     81     public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$
     82 
     83     private IDevice mCurrentDevice;
     84     private Client mCurrentClient;
     85 
     86     private Tree mTree;
     87     private TreeViewer mTreeViewer;
     88 
     89     private Image mDeviceImage;
     90     private Image mEmulatorImage;
     91 
     92     private Image mThreadImage;
     93     private Image mHeapImage;
     94     private Image mWaitingImage;
     95     private Image mDebuggerImage;
     96     private Image mDebugErrorImage;
     97 
     98     private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>();
     99 
    100     private final ArrayList<IDevice> mDevicesToExpand = new ArrayList<IDevice>();
    101 
    102     private boolean mAdvancedPortSupport;
    103 
    104     /**
    105      * A Content provider for the {@link TreeViewer}.
    106      * <p/>
    107      * The input is a {@link AndroidDebugBridge}. First level elements are {@link IDevice} objects,
    108      * and second level elements are {@link Client} object.
    109      */
    110     private class ContentProvider implements ITreeContentProvider {
    111         @Override
    112         public Object[] getChildren(Object parentElement) {
    113             if (parentElement instanceof IDevice) {
    114                 return ((IDevice)parentElement).getClients();
    115             }
    116             return new Object[0];
    117         }
    118 
    119         @Override
    120         public Object getParent(Object element) {
    121             if (element instanceof Client) {
    122                 return ((Client)element).getDevice();
    123             }
    124             return null;
    125         }
    126 
    127         @Override
    128         public boolean hasChildren(Object element) {
    129             if (element instanceof IDevice) {
    130                 return ((IDevice)element).hasClients();
    131             }
    132 
    133             // Clients never have children.
    134             return false;
    135         }
    136 
    137         @Override
    138         public Object[] getElements(Object inputElement) {
    139             if (inputElement instanceof AndroidDebugBridge) {
    140                 return ((AndroidDebugBridge)inputElement).getDevices();
    141             }
    142             return new Object[0];
    143         }
    144 
    145         @Override
    146         public void dispose() {
    147             // pass
    148         }
    149 
    150         @Override
    151         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
    152             // pass
    153         }
    154     }
    155 
    156     /**
    157      * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides
    158      * labels and images for {@link IDevice} and {@link Client} objects.
    159      */
    160     private class LabelProvider implements ITableLabelProvider {
    161         private static final String DEVICE_MODEL_PROPERTY = "ro.product.model"; //$NON-NLS-1$
    162         private static final String DEVICE_MANUFACTURER_PROPERTY = "ro.product.manufacturer"; //$NON-NLS-1$
    163 
    164         @Override
    165         public Image getColumnImage(Object element, int columnIndex) {
    166             if (columnIndex == DEVICE_COL_SERIAL && element instanceof IDevice) {
    167                 IDevice device = (IDevice)element;
    168                 if (device.isEmulator()) {
    169                     return mEmulatorImage;
    170                 }
    171 
    172                 return mDeviceImage;
    173             } else if (element instanceof Client) {
    174                 Client client = (Client)element;
    175                 ClientData cd = client.getClientData();
    176 
    177                 switch (columnIndex) {
    178                     case CLIENT_COL_NAME:
    179                         switch (cd.getDebuggerConnectionStatus()) {
    180                             case DEFAULT:
    181                                 return null;
    182                             case WAITING:
    183                                 return mWaitingImage;
    184                             case ATTACHED:
    185                                 return mDebuggerImage;
    186                             case ERROR:
    187                                 return mDebugErrorImage;
    188                         }
    189                         return null;
    190                     case CLIENT_COL_THREAD:
    191                         if (client.isThreadUpdateEnabled()) {
    192                             return mThreadImage;
    193                         }
    194                         return null;
    195                     case CLIENT_COL_HEAP:
    196                         if (client.isHeapUpdateEnabled()) {
    197                             return mHeapImage;
    198                         }
    199                         return null;
    200                 }
    201             }
    202             return null;
    203         }
    204 
    205         @Override
    206         public String getColumnText(Object element, int columnIndex) {
    207             if (element instanceof IDevice) {
    208                 IDevice device = (IDevice)element;
    209                 switch (columnIndex) {
    210                     case DEVICE_COL_SERIAL:
    211                         return getDeviceName(device);
    212                     case DEVICE_COL_STATE:
    213                         return getStateString(device);
    214                     case DEVICE_COL_BUILD: {
    215                         String version = device.getProperty(IDevice.PROP_BUILD_VERSION);
    216                         if (version != null) {
    217                             String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
    218                             if (device.isEmulator()) {
    219                                 String avdName = device.getAvdName();
    220                                 if (avdName == null) {
    221                                     avdName = "?"; // the device is probably not online yet, so
    222                                                    // we don't know its AVD name just yet.
    223                                 }
    224                                 if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
    225                                     return String.format("%1$s [%2$s, debug]", avdName,
    226                                             version);
    227                                 } else {
    228                                     return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$
    229                                 }
    230                             } else {
    231                                 if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
    232                                     return String.format("%1$s, debug", version);
    233                                 } else {
    234                                     return String.format("%1$s", version); //$NON-NLS-1$
    235                                 }
    236                             }
    237                         } else {
    238                             return "unknown";
    239                         }
    240                     }
    241                 }
    242             } else if (element instanceof Client) {
    243                 Client client = (Client)element;
    244                 ClientData cd = client.getClientData();
    245 
    246                 switch (columnIndex) {
    247                     case CLIENT_COL_NAME:
    248                         String name = cd.getClientDescription();
    249                         if (name != null) {
    250                             return name;
    251                         }
    252                         return "?";
    253                     case CLIENT_COL_PID:
    254                         return Integer.toString(cd.getPid());
    255                     case CLIENT_COL_PORT:
    256                         if (mAdvancedPortSupport) {
    257                             int port = client.getDebuggerListenPort();
    258                             String portString = "?";
    259                             if (port != 0) {
    260                                 portString = Integer.toString(port);
    261                             }
    262                             if (client.isSelectedClient()) {
    263                                 return String.format("%1$s / %2$d", portString, //$NON-NLS-1$
    264                                         DdmPreferences.getSelectedDebugPort());
    265                             }
    266 
    267                             return portString;
    268                         }
    269                 }
    270             }
    271             return null;
    272         }
    273 
    274         private String getDeviceName(IDevice device) {
    275             StringBuilder sb = new StringBuilder(20);
    276             sb.append(device.getSerialNumber());
    277 
    278             if (device.isEmulator()) {
    279                 sb.append(String.format(" [%s]", device.getAvdName()));
    280             } else {
    281                 String manufacturer = device.getProperty(DEVICE_MANUFACTURER_PROPERTY);
    282                 manufacturer = cleanupStringForDisplay(manufacturer);
    283 
    284                 String model = device.getProperty(DEVICE_MODEL_PROPERTY);
    285                 model = cleanupStringForDisplay(model);
    286 
    287                 boolean hasManufacturer = manufacturer.length() > 0;
    288                 boolean hasModel = model.length() > 0;
    289                 if (hasManufacturer || hasModel) {
    290                     sb.append(" [");                        //$NON-NLS-1$
    291                     sb.append(manufacturer);
    292 
    293                     if (hasManufacturer && hasModel) {
    294                         sb.append(':');
    295                     }
    296 
    297                     sb.append(model);
    298                     sb.append(']');
    299                 }
    300             }
    301 
    302             return sb.toString();
    303         }
    304 
    305         private String cleanupStringForDisplay(String s) {
    306             if (s == null) {
    307                 return "";
    308             }
    309 
    310             StringBuilder sb = new StringBuilder(s.length());
    311             for (int i = 0; i < s.length(); i++) {
    312                 char c = s.charAt(i);
    313 
    314                 if (Character.isLetterOrDigit(c)) {
    315                     sb.append(c);
    316                 }
    317             }
    318 
    319             return sb.toString();
    320         }
    321 
    322         @Override
    323         public void addListener(ILabelProviderListener listener) {
    324             // pass
    325         }
    326 
    327         @Override
    328         public void dispose() {
    329             // pass
    330         }
    331 
    332         @Override
    333         public boolean isLabelProperty(Object element, String property) {
    334             // pass
    335             return false;
    336         }
    337 
    338         @Override
    339         public void removeListener(ILabelProviderListener listener) {
    340             // pass
    341         }
    342     }
    343 
    344     /**
    345      * Classes which implement this interface provide methods that deals
    346      * with {@link IDevice} and {@link Client} selection changes coming from the ui.
    347      */
    348     public interface IUiSelectionListener {
    349         /**
    350          * Sent when a new {@link IDevice} and {@link Client} are selected.
    351          * @param selectedDevice the selected device. If null, no devices are selected.
    352          * @param selectedClient The selected client. If null, no clients are selected.
    353          */
    354         public void selectionChanged(IDevice selectedDevice, Client selectedClient);
    355     }
    356 
    357     /**
    358      * Creates the {@link DevicePanel} object.
    359      * @param loader
    360      * @param advancedPortSupport if true the device panel will add support for selected client port
    361      * and display the ports in the ui.
    362      */
    363     public DevicePanel(boolean advancedPortSupport) {
    364         mAdvancedPortSupport = advancedPortSupport;
    365     }
    366 
    367     public void addSelectionListener(IUiSelectionListener listener) {
    368         mListeners.add(listener);
    369     }
    370 
    371     public void removeSelectionListener(IUiSelectionListener listener) {
    372         mListeners.remove(listener);
    373     }
    374 
    375     @Override
    376     protected Control createControl(Composite parent) {
    377         loadImages(parent.getDisplay());
    378 
    379         parent.setLayout(new FillLayout());
    380 
    381         // create the tree and its column
    382         mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION);
    383         mTree.setHeaderVisible(true);
    384         mTree.setLinesVisible(true);
    385 
    386         IPreferenceStore store = DdmUiPreferences.getStore();
    387 
    388         TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
    389                 "com.android.home", //$NON-NLS-1$
    390                 PREFS_COL_NAME_SERIAL, store);
    391         TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
    392                 "Offline", //$NON-NLS-1$
    393                 PREFS_COL_PID_STATE, store);
    394 
    395         TreeColumn col = new TreeColumn(mTree, SWT.NONE);
    396         col.setWidth(ICON_WIDTH + 8);
    397         col.setResizable(false);
    398         col = new TreeColumn(mTree, SWT.NONE);
    399         col.setWidth(ICON_WIDTH + 8);
    400         col.setResizable(false);
    401 
    402         TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
    403                 "9999-9999", //$NON-NLS-1$
    404                 PREFS_COL_PORT_BUILD, store);
    405 
    406         // create the tree viewer
    407         mTreeViewer = new TreeViewer(mTree);
    408 
    409         // make the device auto expanded.
    410         mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
    411 
    412         // set up the content and label providers.
    413         mTreeViewer.setContentProvider(new ContentProvider());
    414         mTreeViewer.setLabelProvider(new LabelProvider());
    415 
    416         mTree.addSelectionListener(new SelectionAdapter() {
    417             @Override
    418             public void widgetSelected(SelectionEvent e) {
    419                 notifyListeners();
    420             }
    421         });
    422 
    423         return mTree;
    424     }
    425 
    426     /**
    427      * Sets the focus to the proper control inside the panel.
    428      */
    429     @Override
    430     public void setFocus() {
    431         mTree.setFocus();
    432     }
    433 
    434     @Override
    435     protected void postCreation() {
    436         // ask for notification of changes in AndroidDebugBridge (a new one is created when
    437         // adb is restarted from a different location), IDevice and Client objects.
    438         AndroidDebugBridge.addDebugBridgeChangeListener(this);
    439         AndroidDebugBridge.addDeviceChangeListener(this);
    440         AndroidDebugBridge.addClientChangeListener(this);
    441     }
    442 
    443     public void dispose() {
    444         AndroidDebugBridge.removeDebugBridgeChangeListener(this);
    445         AndroidDebugBridge.removeDeviceChangeListener(this);
    446         AndroidDebugBridge.removeClientChangeListener(this);
    447     }
    448 
    449     /**
    450      * Returns the selected {@link Client}. May be null.
    451      */
    452     public Client getSelectedClient() {
    453         return mCurrentClient;
    454     }
    455 
    456     /**
    457      * Returns the selected {@link IDevice}. If a {@link Client} is selected, it returns the
    458      * IDevice object containing the client.
    459      */
    460     public IDevice getSelectedDevice() {
    461         return mCurrentDevice;
    462     }
    463 
    464     /**
    465      * Kills the selected {@link Client} by sending its VM a halt command.
    466      */
    467     public void killSelectedClient() {
    468         if (mCurrentClient != null) {
    469             Client client = mCurrentClient;
    470 
    471             // reset the selection to the device.
    472             TreePath treePath = new TreePath(new Object[] { mCurrentDevice });
    473             TreeSelection treeSelection = new TreeSelection(treePath);
    474             mTreeViewer.setSelection(treeSelection);
    475 
    476             client.kill();
    477         }
    478     }
    479 
    480     /**
    481      * Forces a GC on the selected {@link Client}.
    482      */
    483     public void forceGcOnSelectedClient() {
    484         if (mCurrentClient != null) {
    485             mCurrentClient.executeGarbageCollector();
    486         }
    487     }
    488 
    489     public void dumpHprof() {
    490         if (mCurrentClient != null) {
    491             mCurrentClient.dumpHprof();
    492         }
    493     }
    494 
    495     public void toggleMethodProfiling() {
    496         if (mCurrentClient != null) {
    497             mCurrentClient.toggleMethodProfiling();
    498         }
    499     }
    500 
    501     public void setEnabledHeapOnSelectedClient(boolean enable) {
    502         if (mCurrentClient != null) {
    503             mCurrentClient.setHeapUpdateEnabled(enable);
    504         }
    505     }
    506 
    507     public void setEnabledThreadOnSelectedClient(boolean enable) {
    508         if (mCurrentClient != null) {
    509             mCurrentClient.setThreadUpdateEnabled(enable);
    510         }
    511     }
    512 
    513     /**
    514      * Sent when a new {@link AndroidDebugBridge} is started.
    515      * <p/>
    516      * This is sent from a non UI thread.
    517      * @param bridge the new {@link AndroidDebugBridge} object.
    518      *
    519      * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge)
    520      */
    521     @Override
    522     public void bridgeChanged(final AndroidDebugBridge bridge) {
    523         if (mTree.isDisposed() == false) {
    524             exec(new Runnable() {
    525                 @Override
    526                 public void run() {
    527                     if (mTree.isDisposed() == false) {
    528                         // set up the data source.
    529                         mTreeViewer.setInput(bridge);
    530 
    531                         // notify the listener of a possible selection change.
    532                         notifyListeners();
    533                     } else {
    534                         // tree is disposed, we need to do something.
    535                         // lets remove ourselves from the listener.
    536                         AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
    537                         AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
    538                         AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
    539                     }
    540                 }
    541             });
    542         }
    543 
    544         // all current devices are obsolete
    545         synchronized (mDevicesToExpand) {
    546             mDevicesToExpand.clear();
    547         }
    548     }
    549 
    550     /**
    551      * Sent when the a device is connected to the {@link AndroidDebugBridge}.
    552      * <p/>
    553      * This is sent from a non UI thread.
    554      * @param device the new device.
    555      *
    556      * @see IDeviceChangeListener#deviceConnected(IDevice)
    557      */
    558     @Override
    559     public void deviceConnected(IDevice device) {
    560         exec(new Runnable() {
    561             @Override
    562             public void run() {
    563                 if (mTree.isDisposed() == false) {
    564                     // refresh all
    565                     mTreeViewer.refresh();
    566 
    567                     // notify the listener of a possible selection change.
    568                     notifyListeners();
    569                 } else {
    570                     // tree is disposed, we need to do something.
    571                     // lets remove ourselves from the listener.
    572                     AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
    573                     AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
    574                     AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
    575                 }
    576             }
    577         });
    578 
    579         // if it doesn't have clients yet, it'll need to be manually expanded when it gets them.
    580         if (device.hasClients() == false) {
    581             synchronized (mDevicesToExpand) {
    582                 mDevicesToExpand.add(device);
    583             }
    584         }
    585     }
    586 
    587     /**
    588      * Sent when the a device is connected to the {@link AndroidDebugBridge}.
    589      * <p/>
    590      * This is sent from a non UI thread.
    591      * @param device the new device.
    592      *
    593      * @see IDeviceChangeListener#deviceDisconnected(IDevice)
    594      */
    595     @Override
    596     public void deviceDisconnected(IDevice device) {
    597         deviceConnected(device);
    598 
    599         // just in case, we remove it from the list of devices to expand.
    600         synchronized (mDevicesToExpand) {
    601             mDevicesToExpand.remove(device);
    602         }
    603     }
    604 
    605     /**
    606      * Sent when a device data changed, or when clients are started/terminated on the device.
    607      * <p/>
    608      * This is sent from a non UI thread.
    609      * @param device the device that was updated.
    610      * @param changeMask the mask indicating what changed.
    611      *
    612      * @see IDeviceChangeListener#deviceChanged(IDevice)
    613      */
    614     @Override
    615     public void deviceChanged(final IDevice device, int changeMask) {
    616         boolean expand = false;
    617         synchronized (mDevicesToExpand) {
    618             int index = mDevicesToExpand.indexOf(device);
    619             if (device.hasClients() && index != -1) {
    620                 mDevicesToExpand.remove(index);
    621                 expand = true;
    622             }
    623         }
    624 
    625         final boolean finalExpand = expand;
    626 
    627         exec(new Runnable() {
    628             @Override
    629             public void run() {
    630                 if (mTree.isDisposed() == false) {
    631                     // look if the current device is selected. This is done in case the current
    632                     // client of this particular device was killed. In this case, we'll need to
    633                     // manually reselect the device.
    634 
    635                     IDevice selectedDevice = getSelectedDevice();
    636 
    637                     // refresh the device
    638                     mTreeViewer.refresh(device);
    639 
    640                     // if the selected device was the changed device and the new selection is
    641                     // empty, we reselect the device.
    642                     if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) {
    643                         mTreeViewer.setSelection(new TreeSelection(new TreePath(
    644                                 new Object[] { device })));
    645                     }
    646 
    647                     // notify the listener of a possible selection change.
    648                     notifyListeners();
    649 
    650                     if (finalExpand) {
    651                         mTreeViewer.setExpandedState(device, true);
    652                     }
    653                 } else {
    654                     // tree is disposed, we need to do something.
    655                     // lets remove ourselves from the listener.
    656                     AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
    657                     AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
    658                     AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
    659                 }
    660             }
    661         });
    662     }
    663 
    664     /**
    665      * Sent when an existing client information changed.
    666      * <p/>
    667      * This is sent from a non UI thread.
    668      * @param client the updated client.
    669      * @param changeMask the bit mask describing the changed properties. It can contain
    670      * any of the following values: {@link Client#CHANGE_INFO},
    671      * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
    672      * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
    673      * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
    674      *
    675      * @see IClientChangeListener#clientChanged(Client, int)
    676      */
    677     @Override
    678     public void clientChanged(final Client client, final int changeMask) {
    679         exec(new Runnable() {
    680             @Override
    681             public void run() {
    682                 if (mTree.isDisposed() == false) {
    683                     // refresh the client
    684                     mTreeViewer.refresh(client);
    685 
    686                     if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) ==
    687                             Client.CHANGE_DEBUGGER_STATUS &&
    688                             client.getClientData().getDebuggerConnectionStatus() ==
    689                                 DebuggerStatus.WAITING) {
    690                         // make sure the device is expanded. Normally the setSelection below
    691                         // will auto expand, but the children of device may not already exist
    692                         // at this time. Forcing an expand will make the TreeViewer create them.
    693                         IDevice device = client.getDevice();
    694                         if (mTreeViewer.getExpandedState(device) == false) {
    695                             mTreeViewer.setExpandedState(device, true);
    696                         }
    697 
    698                         // create and set the selection
    699                         TreePath treePath = new TreePath(new Object[] { device, client});
    700                         TreeSelection treeSelection = new TreeSelection(treePath);
    701                         mTreeViewer.setSelection(treeSelection);
    702 
    703                         if (mAdvancedPortSupport) {
    704                             client.setAsSelectedClient();
    705                         }
    706 
    707                         // notify the listener of a possible selection change.
    708                         notifyListeners(device, client);
    709                     }
    710                 } else {
    711                     // tree is disposed, we need to do something.
    712                     // lets remove ourselves from the listener.
    713                     AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
    714                     AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
    715                     AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
    716                 }
    717             }
    718         });
    719     }
    720 
    721     private void loadImages(Display display) {
    722         ImageLoader loader = ImageLoader.getDdmUiLibLoader();
    723 
    724         if (mDeviceImage == null) {
    725             mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$
    726                     ICON_WIDTH, ICON_WIDTH,
    727                     display.getSystemColor(SWT.COLOR_RED));
    728         }
    729         if (mEmulatorImage == null) {
    730             mEmulatorImage = loader.loadImage(display,
    731                     "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
    732                     display.getSystemColor(SWT.COLOR_BLUE));
    733         }
    734         if (mThreadImage == null) {
    735             mThreadImage = loader.loadImage(display, ICON_THREAD,
    736                     ICON_WIDTH, ICON_WIDTH,
    737                     display.getSystemColor(SWT.COLOR_YELLOW));
    738         }
    739         if (mHeapImage == null) {
    740             mHeapImage = loader.loadImage(display, ICON_HEAP,
    741                     ICON_WIDTH, ICON_WIDTH,
    742                     display.getSystemColor(SWT.COLOR_BLUE));
    743         }
    744         if (mWaitingImage == null) {
    745             mWaitingImage = loader.loadImage(display,
    746                     "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
    747                     display.getSystemColor(SWT.COLOR_RED));
    748         }
    749         if (mDebuggerImage == null) {
    750             mDebuggerImage = loader.loadImage(display,
    751                     "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
    752                     display.getSystemColor(SWT.COLOR_GREEN));
    753         }
    754         if (mDebugErrorImage == null) {
    755             mDebugErrorImage = loader.loadImage(display,
    756                     "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
    757                     display.getSystemColor(SWT.COLOR_RED));
    758         }
    759     }
    760 
    761     /**
    762      * Returns a display string representing the state of the device.
    763      * @param d the device
    764      */
    765     private static String getStateString(IDevice d) {
    766         DeviceState deviceState = d.getState();
    767         if (deviceState == DeviceState.ONLINE) {
    768             return "Online";
    769         } else if (deviceState == DeviceState.OFFLINE) {
    770             return "Offline";
    771         } else if (deviceState == DeviceState.BOOTLOADER) {
    772             return "Bootloader";
    773         }
    774 
    775         return "??";
    776     }
    777 
    778     /**
    779      * Executes the {@link Runnable} in the UI thread.
    780      * @param runnable the runnable to execute.
    781      */
    782     private void exec(Runnable runnable) {
    783         try {
    784             Display display = mTree.getDisplay();
    785             display.asyncExec(runnable);
    786         } catch (SWTException e) {
    787             // tree is disposed, we need to do something. lets remove ourselves from the listener.
    788             AndroidDebugBridge.removeDebugBridgeChangeListener(this);
    789             AndroidDebugBridge.removeDeviceChangeListener(this);
    790             AndroidDebugBridge.removeClientChangeListener(this);
    791         }
    792     }
    793 
    794     private void notifyListeners() {
    795         // get the selection
    796         TreeItem[] items = mTree.getSelection();
    797 
    798         Client client = null;
    799         IDevice device = null;
    800 
    801         if (items.length == 1) {
    802             Object object = items[0].getData();
    803             if (object instanceof Client) {
    804                 client = (Client)object;
    805                 device = client.getDevice();
    806             } else if (object instanceof IDevice) {
    807                 device = (IDevice)object;
    808             }
    809         }
    810 
    811         notifyListeners(device, client);
    812     }
    813 
    814     private void notifyListeners(IDevice selectedDevice, Client selectedClient) {
    815         if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) {
    816             mCurrentDevice = selectedDevice;
    817             mCurrentClient = selectedClient;
    818 
    819             for (IUiSelectionListener listener : mListeners) {
    820                 // notify the listener with a try/catch-all to make sure this thread won't die
    821                 // because of an uncaught exception before all the listeners were notified.
    822                 try {
    823                     listener.selectionChanged(selectedDevice, selectedClient);
    824                 } catch (Exception e) {
    825                 }
    826             }
    827         }
    828     }
    829 
    830 }
    831