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