• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ddms;
18 
19 import com.android.ddmlib.AndroidDebugBridge;
20 import com.android.ddmlib.Client;
21 import com.android.ddmlib.ClientData;
22 import com.android.ddmlib.IDevice;
23 import com.android.ddmlib.Log;
24 import com.android.ddmlib.SyncException;
25 import com.android.ddmlib.SyncService;
26 import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
27 import com.android.ddmlib.ClientData.IHprofDumpHandler;
28 import com.android.ddmlib.ClientData.MethodProfilingStatus;
29 import com.android.ddmlib.Log.ILogOutput;
30 import com.android.ddmlib.Log.LogLevel;
31 import com.android.ddmuilib.AllocationPanel;
32 import com.android.ddmuilib.DdmUiPreferences;
33 import com.android.ddmuilib.DevicePanel;
34 import com.android.ddmuilib.EmulatorControlPanel;
35 import com.android.ddmuilib.HeapPanel;
36 import com.android.ddmuilib.ITableFocusListener;
37 import com.android.ddmuilib.ImageLoader;
38 import com.android.ddmuilib.InfoPanel;
39 import com.android.ddmuilib.NativeHeapPanel;
40 import com.android.ddmuilib.ScreenShotDialog;
41 import com.android.ddmuilib.SysinfoPanel;
42 import com.android.ddmuilib.TablePanel;
43 import com.android.ddmuilib.ThreadPanel;
44 import com.android.ddmuilib.DevicePanel.IUiSelectionListener;
45 import com.android.ddmuilib.actions.ToolItemAction;
46 import com.android.ddmuilib.explorer.DeviceExplorer;
47 import com.android.ddmuilib.handler.BaseFileHandler;
48 import com.android.ddmuilib.handler.MethodProfilingHandler;
49 import com.android.ddmuilib.log.event.EventLogPanel;
50 import com.android.ddmuilib.logcat.LogCatPanel;
51 import com.android.ddmuilib.logcat.LogCatReceiver;
52 import com.android.ddmuilib.logcat.LogColors;
53 import com.android.ddmuilib.logcat.LogFilter;
54 import com.android.ddmuilib.logcat.LogPanel;
55 import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager;
56 import com.android.menubar.IMenuBarCallback;
57 import com.android.menubar.IMenuBarEnhancer;
58 import com.android.menubar.IMenuBarEnhancer.MenuBarMode;
59 import com.android.menubar.MenuBarEnhancer;
60 
61 import org.eclipse.jface.dialogs.MessageDialog;
62 import org.eclipse.jface.preference.IPreferenceStore;
63 import org.eclipse.jface.preference.PreferenceStore;
64 import org.eclipse.swt.SWT;
65 import org.eclipse.swt.SWTError;
66 import org.eclipse.swt.SWTException;
67 import org.eclipse.swt.dnd.Clipboard;
68 import org.eclipse.swt.events.ControlEvent;
69 import org.eclipse.swt.events.ControlListener;
70 import org.eclipse.swt.events.MenuAdapter;
71 import org.eclipse.swt.events.MenuEvent;
72 import org.eclipse.swt.events.SelectionAdapter;
73 import org.eclipse.swt.events.SelectionEvent;
74 import org.eclipse.swt.events.ShellEvent;
75 import org.eclipse.swt.events.ShellListener;
76 import org.eclipse.swt.graphics.Color;
77 import org.eclipse.swt.graphics.Font;
78 import org.eclipse.swt.graphics.FontData;
79 import org.eclipse.swt.graphics.Image;
80 import org.eclipse.swt.graphics.Rectangle;
81 import org.eclipse.swt.layout.FillLayout;
82 import org.eclipse.swt.layout.FormAttachment;
83 import org.eclipse.swt.layout.FormData;
84 import org.eclipse.swt.layout.FormLayout;
85 import org.eclipse.swt.layout.GridData;
86 import org.eclipse.swt.layout.GridLayout;
87 import org.eclipse.swt.widgets.Composite;
88 import org.eclipse.swt.widgets.Display;
89 import org.eclipse.swt.widgets.Event;
90 import org.eclipse.swt.widgets.Label;
91 import org.eclipse.swt.widgets.Listener;
92 import org.eclipse.swt.widgets.Menu;
93 import org.eclipse.swt.widgets.MenuItem;
94 import org.eclipse.swt.widgets.Sash;
95 import org.eclipse.swt.widgets.Shell;
96 import org.eclipse.swt.widgets.TabFolder;
97 import org.eclipse.swt.widgets.TabItem;
98 import org.eclipse.swt.widgets.ToolBar;
99 import org.eclipse.swt.widgets.ToolItem;
100 
101 import java.io.File;
102 import java.util.ArrayList;
103 
104 /**
105  * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an
106  * SWT application. So this class mainly builds the ui, and manages communication between the panels
107  * when {@link IDevice} / {@link Client} selection changes.
108  */
109 public class UIThread implements IUiSelectionListener, IClientChangeListener {
110     private static final String APP_NAME = "DDMS";
111 
112     /*
113      * UI tab panel definitions. The constants here must match up with the array
114      * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing
115      * the client list.
116      */
117     public static final int PANEL_CLIENT_LIST = -1;
118 
119     public static final int PANEL_INFO = 0;
120 
121     public static final int PANEL_THREAD = 1;
122 
123     public static final int PANEL_HEAP = 2;
124 
125     private static final int PANEL_NATIVE_HEAP = 3;
126 
127     private static final int PANEL_ALLOCATIONS = 4;
128 
129     private static final int PANEL_SYSINFO = 5;
130 
131     private static final int PANEL_COUNT = 6;
132 
133     /** Content is setup in the constructor */
134     private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT];
135 
136     private static final String[] mPanelNames = new String[] {
137             "Info", "Threads", "VM Heap", "Native Heap",
138             "Allocation Tracker", "Sysinfo"
139     };
140 
141     private static final String[] mPanelTips = new String[] {
142             "Client information", "Thread status", "VM heap status",
143             "Native heap status", "Allocation Tracker", "Sysinfo graphs"
144     };
145 
146     private static final String PREFERENCE_LOGSASH =
147         "logSashLocation"; //$NON-NLS-1$
148     private static final String PREFERENCE_SASH =
149         "sashLocation"; //$NON-NLS-1$
150 
151     private static final String PREFS_COL_TIME =
152         "logcat.time"; //$NON-NLS-1$
153     private static final String PREFS_COL_LEVEL =
154         "logcat.level"; //$NON-NLS-1$
155     private static final String PREFS_COL_PID =
156         "logcat.pid"; //$NON-NLS-1$
157     private static final String PREFS_COL_TAG =
158         "logcat.tag"; //$NON-NLS-1$
159     private static final String PREFS_COL_MESSAGE =
160         "logcat.message"; //$NON-NLS-1$
161 
162     private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$
163 
164     // singleton instance
165     private static UIThread mInstance = new UIThread();
166 
167     // our display
168     private Display mDisplay;
169 
170     // the table we show in the left-hand pane
171     private DevicePanel mDevicePanel;
172 
173     private IDevice mCurrentDevice = null;
174     private Client mCurrentClient = null;
175 
176     // status line at the bottom of the app window
177     private Label mStatusLine;
178 
179     // some toolbar items we need to update
180     private ToolItem mTBShowThreadUpdates;
181     private ToolItem mTBShowHeapUpdates;
182     private ToolItem mTBHalt;
183     private ToolItem mTBCauseGc;
184     private ToolItem mTBDumpHprof;
185     private ToolItem mTBProfiling;
186 
187     private final class FilterStorage implements ILogFilterStorageManager {
188 
getFilterFromStore()189         public LogFilter[] getFilterFromStore() {
190             String filterPrefs = PrefsDialog.getStore().getString(
191                     PREFS_FILTERS);
192 
193             // split in a string per filter
194             String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$
195 
196             ArrayList<LogFilter> list =
197                 new ArrayList<LogFilter>(filters.length);
198 
199             for (String f : filters) {
200                 if (f.length() > 0) {
201                     LogFilter logFilter = new LogFilter();
202                     if (logFilter.loadFromString(f)) {
203                         list.add(logFilter);
204                     }
205                 }
206             }
207 
208             return list.toArray(new LogFilter[list.size()]);
209         }
210 
saveFilters(LogFilter[] filters)211         public void saveFilters(LogFilter[] filters) {
212             StringBuilder sb = new StringBuilder();
213             for (LogFilter f : filters) {
214                 String filterString = f.toString();
215                 sb.append(filterString);
216                 sb.append('|');
217             }
218 
219             PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString());
220         }
221 
requiresDefaultFilter()222         public boolean requiresDefaultFilter() {
223             return true;
224         }
225     }
226 
227 
228     /**
229      * Flag to indicate whether to use the old or the new logcat view. This is a
230      * temporary workaround that will be removed once the new view is complete.
231      */
232     private static final String USE_OLD_LOGCAT_VIEW =
233             System.getenv("ANDROID_USE_OLD_LOGCAT_VIEW");
useOldLogCatView()234     public static boolean useOldLogCatView() {
235         return USE_OLD_LOGCAT_VIEW != null;
236     }
237 
238     private LogPanel mLogPanel; /* only valid when useOldLogCatView() == true */
239     private LogCatPanel mLogCatPanel; /* only valid when useOldLogCatView() == false */
240 
241     private ToolItemAction mCreateFilterAction;
242     private ToolItemAction mDeleteFilterAction;
243     private ToolItemAction mEditFilterAction;
244     private ToolItemAction mExportAction;
245     private ToolItemAction mClearAction;
246 
247     private ToolItemAction[] mLogLevelActions;
248     private String[] mLogLevelIcons = {
249             "v.png", //$NON-NLS-1S
250             "d.png", //$NON-NLS-1S
251             "i.png", //$NON-NLS-1S
252             "w.png", //$NON-NLS-1S
253             "e.png", //$NON-NLS-1S
254     };
255 
256     protected Clipboard mClipboard;
257 
258     private MenuItem mCopyMenuItem;
259 
260     private MenuItem mSelectAllMenuItem;
261 
262     private TableFocusListener mTableListener;
263 
264     private DeviceExplorer mExplorer = null;
265     private Shell mExplorerShell = null;
266 
267     private EmulatorControlPanel mEmulatorPanel;
268 
269     private EventLogPanel mEventLogPanel;
270 
271     private Image mTracingStartImage;
272 
273     private Image mTracingStopImage;
274 
275     private ImageLoader mDdmUiLibLoader;
276 
277     private class TableFocusListener implements ITableFocusListener {
278 
279         private IFocusedTableActivator mCurrentActivator;
280 
focusGained(IFocusedTableActivator activator)281         public void focusGained(IFocusedTableActivator activator) {
282             mCurrentActivator = activator;
283             if (mCopyMenuItem.isDisposed() == false) {
284                 mCopyMenuItem.setEnabled(true);
285                 mSelectAllMenuItem.setEnabled(true);
286             }
287         }
288 
focusLost(IFocusedTableActivator activator)289         public void focusLost(IFocusedTableActivator activator) {
290             // if we move from one table to another, it's unclear
291             // if the old table lose its focus before the new
292             // one gets the focus, so we need to check.
293             if (activator == mCurrentActivator) {
294                 activator = null;
295                 if (mCopyMenuItem.isDisposed() == false) {
296                     mCopyMenuItem.setEnabled(false);
297                     mSelectAllMenuItem.setEnabled(false);
298                 }
299             }
300         }
301 
copy(Clipboard clipboard)302         public void copy(Clipboard clipboard) {
303             if (mCurrentActivator != null) {
304                 mCurrentActivator.copy(clipboard);
305             }
306         }
307 
selectAll()308         public void selectAll() {
309             if (mCurrentActivator != null) {
310                 mCurrentActivator.selectAll();
311             }
312         }
313     }
314 
315     /**
316      * Handler for HPROF dumps.
317      * This will always prompt the user to save the HPROF file.
318      */
319     private class HProfHandler extends BaseFileHandler implements IHprofDumpHandler {
320 
HProfHandler(Shell parentShell)321         public HProfHandler(Shell parentShell) {
322             super(parentShell);
323         }
324 
onEndFailure(final Client client, final String message)325         public void onEndFailure(final Client client, final String message) {
326             mDisplay.asyncExec(new Runnable() {
327                 public void run() {
328                     try {
329                         displayErrorFromUiThread(
330                                 "Unable to create HPROF file for application '%1$s'\n\n%2$s" +
331                                 "Check logcat for more information.",
332                                 client.getClientData().getClientDescription(),
333                                 message != null ? message + "\n\n" : "");
334                     } finally {
335                         // this will make sure the dump hprof button is re-enabled for the
336                         // current selection. as the client is finished dumping an hprof file
337                         enableButtons();
338                     }
339                 }
340             });
341         }
342 
onSuccess(final String remoteFilePath, final Client client)343         public void onSuccess(final String remoteFilePath, final Client client) {
344             mDisplay.asyncExec(new Runnable() {
345                 public void run() {
346                     final IDevice device = client.getDevice();
347                     try {
348                         // get the sync service to pull the HPROF file
349                         final SyncService sync = client.getDevice().getSyncService();
350                         if (sync != null) {
351                             promptAndPull(sync,
352                                     client.getClientData().getClientDescription() + ".hprof",
353                                     remoteFilePath, "Save HPROF file");
354                         } else {
355                             displayErrorFromUiThread(
356                                     "Unable to download HPROF file from device '%1$s'.",
357                                     device.getSerialNumber());
358                         }
359                     } catch (SyncException e) {
360                         if (e.wasCanceled() == false) {
361                             displayErrorFromUiThread(
362                                     "Unable to download HPROF file from device '%1$s'.\n\n%2$s",
363                                     device.getSerialNumber(), e.getMessage());
364                         }
365                     } catch (Exception e) {
366                         displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.",
367                                 device.getSerialNumber());
368 
369                     } finally {
370                         // this will make sure the dump hprof button is re-enabled for the
371                         // current selection. as the client is finished dumping an hprof file
372                         enableButtons();
373                     }
374                 }
375             });
376         }
377 
onSuccess(final byte[] data, final Client client)378         public void onSuccess(final byte[] data, final Client client) {
379             mDisplay.asyncExec(new Runnable() {
380                 public void run() {
381                     promptAndSave(client.getClientData().getClientDescription() + ".hprof", data,
382                             "Save HPROF file");
383                 }
384             });
385         }
386 
387         @Override
getDialogTitle()388         protected String getDialogTitle() {
389             return "HPROF Error";
390         }
391     }
392 
393 
394     /**
395      * Generic constructor.
396      */
UIThread()397     private UIThread() {
398         mPanels[PANEL_INFO] = new InfoPanel();
399         mPanels[PANEL_THREAD] = new ThreadPanel();
400         mPanels[PANEL_HEAP] = new HeapPanel();
401         if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) {
402             if (System.getenv("ANDROID_DDMS_OLD_HEAP_PANEL") != null) {
403                 mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel();
404             } else {
405                 mPanels[PANEL_NATIVE_HEAP] =
406                         new com.android.ddmuilib.heap.NativeHeapPanel(getStore());
407             }
408         } else {
409             mPanels[PANEL_NATIVE_HEAP] = null;
410         }
411         mPanels[PANEL_ALLOCATIONS] = new AllocationPanel();
412         mPanels[PANEL_SYSINFO] = new SysinfoPanel();
413     }
414 
415     /**
416      * Get singleton instance of the UI thread.
417      */
getInstance()418     public static UIThread getInstance() {
419         return mInstance;
420     }
421 
422     /**
423      * Return the Display. Don't try this unless you're in the UI thread.
424      */
getDisplay()425     public Display getDisplay() {
426         return mDisplay;
427     }
428 
asyncExec(Runnable r)429     public void asyncExec(Runnable r) {
430         if (mDisplay != null && mDisplay.isDisposed() == false) {
431             mDisplay.asyncExec(r);
432         }
433     }
434 
435     /** returns the IPreferenceStore */
getStore()436     public IPreferenceStore getStore() {
437         return PrefsDialog.getStore();
438     }
439 
440     /**
441      * Create SWT objects and drive the user interface event loop.
442      * @param ddmsParentLocation location of the folder that contains ddms.
443      */
runUI(String ddmsParentLocation)444     public void runUI(String ddmsParentLocation) {
445         Display.setAppName(APP_NAME);
446         mDisplay = Display.getDefault();
447         final Shell shell = new Shell(mDisplay);
448 
449         // create the image loaders for DDMS and DDMUILIB
450         mDdmUiLibLoader = ImageLoader.getDdmUiLibLoader();
451 
452         shell.setImage(ImageLoader.getLoader(this.getClass()).loadImage(mDisplay,
453                 "ddms-128.png", //$NON-NLS-1$
454                 100, 50, null));
455 
456         Log.setLogOutput(new ILogOutput() {
457             public void printAndPromptLog(final LogLevel logLevel, final String tag,
458                     final String message) {
459                 Log.printLog(logLevel, tag, message);
460                 // dialog box only run in UI thread..
461                 mDisplay.asyncExec(new Runnable() {
462                     public void run() {
463                         Shell activeShell = mDisplay.getActiveShell();
464                         if (logLevel == LogLevel.ERROR) {
465                             MessageDialog.openError(activeShell, tag, message);
466                         } else {
467                             MessageDialog.openWarning(activeShell, tag, message);
468                         }
469                     }
470                 });
471             }
472 
473             public void printLog(LogLevel logLevel, String tag, String message) {
474                 Log.printLog(logLevel, tag, message);
475             }
476         });
477 
478         // set the handler for hprof dump
479         ClientData.setHprofDumpHandler(new HProfHandler(shell));
480         ClientData.setMethodProfilingHandler(new MethodProfilingHandler(shell));
481 
482         // [try to] ensure ADB is running
483         // in the new SDK, adb is in the platform-tools, but when run from the command line
484         // in the Android source tree, then adb is next to ddms.
485         String adbLocation;
486         if (ddmsParentLocation != null && ddmsParentLocation.length() != 0) {
487             // check if there's a platform-tools folder
488             File platformTools = new File(new File(ddmsParentLocation).getParent(),
489                     "platform-tools");  //$NON-NLS-1$
490             if (platformTools.isDirectory()) {
491                 adbLocation = platformTools.getAbsolutePath() + File.separator + "adb"; //$NON-NLS-1$
492             } else {
493                 adbLocation = ddmsParentLocation + File.separator + "adb"; //$NON-NLS-1$
494             }
495         } else {
496             adbLocation = "adb"; //$NON-NLS-1$
497         }
498 
499         AndroidDebugBridge.init(true /* debugger support */);
500         AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */);
501 
502         // we need to listen to client change to be notified of client status (profiling) change
503         AndroidDebugBridge.addClientChangeListener(this);
504 
505         shell.setText("Dalvik Debug Monitor");
506         setConfirmClose(shell);
507         createMenus(shell);
508         createWidgets(shell);
509 
510         shell.pack();
511         setSizeAndPosition(shell);
512         shell.open();
513 
514         Log.d("ddms", "UI is up");
515 
516         while (!shell.isDisposed()) {
517             if (!mDisplay.readAndDispatch())
518                 mDisplay.sleep();
519         }
520         if (useOldLogCatView()) {
521             mLogPanel.stopLogCat(true);
522         }
523 
524         mDevicePanel.dispose();
525         for (TablePanel panel : mPanels) {
526             if (panel != null) {
527                 panel.dispose();
528             }
529         }
530 
531         ImageLoader.dispose();
532 
533         mDisplay.dispose();
534         Log.d("ddms", "UI is down");
535     }
536 
537     /**
538      * Set the size and position of the main window from the preference, and
539      * setup listeners for control events (resize/move of the window)
540      */
setSizeAndPosition(final Shell shell)541     private void setSizeAndPosition(final Shell shell) {
542         shell.setMinimumSize(400, 200);
543 
544         // get the x/y and w/h from the prefs
545         PreferenceStore prefs = PrefsDialog.getStore();
546         int x = prefs.getInt(PrefsDialog.SHELL_X);
547         int y = prefs.getInt(PrefsDialog.SHELL_Y);
548         int w = prefs.getInt(PrefsDialog.SHELL_WIDTH);
549         int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT);
550 
551         // check that we're not out of the display area
552         Rectangle rect = mDisplay.getClientArea();
553         // first check the width/height
554         if (w > rect.width) {
555             w = rect.width;
556             prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width);
557         }
558         if (h > rect.height) {
559             h = rect.height;
560             prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height);
561         }
562         // then check x. Make sure the left corner is in the screen
563         if (x < rect.x) {
564             x = rect.x;
565             prefs.setValue(PrefsDialog.SHELL_X, rect.x);
566         } else if (x >= rect.x + rect.width) {
567             x = rect.x + rect.width - w;
568             prefs.setValue(PrefsDialog.SHELL_X, rect.x);
569         }
570         // then check y. Make sure the left corner is in the screen
571         if (y < rect.y) {
572             y = rect.y;
573             prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
574         } else if (y >= rect.y + rect.height) {
575             y = rect.y + rect.height - h;
576             prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
577         }
578 
579         // now we can set the location/size
580         shell.setBounds(x, y, w, h);
581 
582         // add listener for resize/move
583         shell.addControlListener(new ControlListener() {
584             public void controlMoved(ControlEvent e) {
585                 // get the new x/y
586                 Rectangle controlBounds = shell.getBounds();
587                 // store in pref file
588                 PreferenceStore currentPrefs = PrefsDialog.getStore();
589                 currentPrefs.setValue(PrefsDialog.SHELL_X, controlBounds.x);
590                 currentPrefs.setValue(PrefsDialog.SHELL_Y, controlBounds.y);
591             }
592 
593             public void controlResized(ControlEvent e) {
594                 // get the new w/h
595                 Rectangle controlBounds = shell.getBounds();
596                 // store in pref file
597                 PreferenceStore currentPrefs = PrefsDialog.getStore();
598                 currentPrefs.setValue(PrefsDialog.SHELL_WIDTH, controlBounds.width);
599                 currentPrefs.setValue(PrefsDialog.SHELL_HEIGHT, controlBounds.height);
600             }
601         });
602     }
603 
604     /**
605      * Set the size and position of the file explorer window from the
606      * preference, and setup listeners for control events (resize/move of
607      * the window)
608      */
setExplorerSizeAndPosition(final Shell shell)609     private void setExplorerSizeAndPosition(final Shell shell) {
610         shell.setMinimumSize(400, 200);
611 
612         // get the x/y and w/h from the prefs
613         PreferenceStore prefs = PrefsDialog.getStore();
614         int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X);
615         int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y);
616         int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH);
617         int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT);
618 
619         // check that we're not out of the display area
620         Rectangle rect = mDisplay.getClientArea();
621         // first check the width/height
622         if (w > rect.width) {
623             w = rect.width;
624             prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width);
625         }
626         if (h > rect.height) {
627             h = rect.height;
628             prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height);
629         }
630         // then check x. Make sure the left corner is in the screen
631         if (x < rect.x) {
632             x = rect.x;
633             prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
634         } else if (x >= rect.x + rect.width) {
635             x = rect.x + rect.width - w;
636             prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
637         }
638         // then check y. Make sure the left corner is in the screen
639         if (y < rect.y) {
640             y = rect.y;
641             prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
642         } else if (y >= rect.y + rect.height) {
643             y = rect.y + rect.height - h;
644             prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
645         }
646 
647         // now we can set the location/size
648         shell.setBounds(x, y, w, h);
649 
650         // add listener for resize/move
651         shell.addControlListener(new ControlListener() {
652             public void controlMoved(ControlEvent e) {
653                 // get the new x/y
654                 Rectangle controlBounds = shell.getBounds();
655                 // store in pref file
656                 PreferenceStore currentPrefs = PrefsDialog.getStore();
657                 currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_X, controlBounds.x);
658                 currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, controlBounds.y);
659             }
660 
661             public void controlResized(ControlEvent e) {
662                 // get the new w/h
663                 Rectangle controlBounds = shell.getBounds();
664                 // store in pref file
665                 PreferenceStore currentPrefs = PrefsDialog.getStore();
666                 currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, controlBounds.width);
667                 currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, controlBounds.height);
668             }
669         });
670     }
671 
672     /*
673      * Set the confirm-before-close dialog.
674      */
setConfirmClose(final Shell shell)675     private void setConfirmClose(final Shell shell) {
676         // Note: there was some commented out code to display a confirmation box
677         // when closing. The feature seems unnecessary and the code was not being
678         // used, so it has been removed.
679     }
680 
681     /*
682      * Create the menu bar and items.
683      */
createMenus(final Shell shell)684     private void createMenus(final Shell shell) {
685         // create menu bar
686         Menu menuBar = new Menu(shell, SWT.BAR);
687 
688         // create top-level items
689         MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE);
690         fileItem.setText("&File");
691         MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE);
692         editItem.setText("&Edit");
693         MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE);
694         actionItem.setText("&Actions");
695         MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE);
696         deviceItem.setText("&Device");
697 
698         // create top-level menus
699         Menu fileMenu = new Menu(menuBar);
700         fileItem.setMenu(fileMenu);
701         Menu editMenu = new Menu(menuBar);
702         editItem.setMenu(editMenu);
703         Menu actionMenu = new Menu(menuBar);
704         actionItem.setMenu(actionMenu);
705         Menu deviceMenu = new Menu(menuBar);
706         deviceItem.setMenu(deviceMenu);
707 
708         MenuItem item;
709 
710         // create File menu items
711         item = new MenuItem(fileMenu, SWT.NONE);
712         item.setText("&Static Port Configuration...");
713         item.addSelectionListener(new SelectionAdapter() {
714             @Override
715             public void widgetSelected(SelectionEvent e) {
716                 StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell);
717                 dlg.open();
718             }
719         });
720 
721         IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenu(APP_NAME, fileMenu,
722                 new IMenuBarCallback() {
723             public void printError(String format, Object... args) {
724                 Log.e("DDMS Menu Bar", String.format(format, args));
725             }
726 
727             public void onPreferencesMenuSelected() {
728                 PrefsDialog.run(shell);
729             }
730 
731             public void onAboutMenuSelected() {
732                 AboutDialog dlg = new AboutDialog(shell);
733                 dlg.open();
734             }
735         });
736 
737         if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
738             new MenuItem(fileMenu, SWT.SEPARATOR);
739 
740             item = new MenuItem(fileMenu, SWT.NONE);
741             item.setText("E&xit\tCtrl-Q");
742             item.setAccelerator('Q' | SWT.MOD1);
743             item.addSelectionListener(new SelectionAdapter() {
744                 @Override
745                 public void widgetSelected(SelectionEvent e) {
746                     shell.close();
747                 }
748             });
749         }
750 
751         // create edit menu items
752         mCopyMenuItem = new MenuItem(editMenu, SWT.NONE);
753         mCopyMenuItem.setText("&Copy\tCtrl-C");
754         mCopyMenuItem.setAccelerator('C' | SWT.MOD1);
755         mCopyMenuItem.addSelectionListener(new SelectionAdapter() {
756             @Override
757             public void widgetSelected(SelectionEvent e) {
758                 mTableListener.copy(mClipboard);
759             }
760         });
761 
762         new MenuItem(editMenu, SWT.SEPARATOR);
763 
764         mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE);
765         mSelectAllMenuItem.setText("Select &All\tCtrl-A");
766         mSelectAllMenuItem.setAccelerator('A' | SWT.MOD1);
767         mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() {
768             @Override
769             public void widgetSelected(SelectionEvent e) {
770                 mTableListener.selectAll();
771             }
772         });
773 
774         // create Action menu items
775         // TODO: this should come with a confirmation dialog
776         final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE);
777         actionHaltItem.setText("&Halt VM");
778         actionHaltItem.addSelectionListener(new SelectionAdapter() {
779             @Override
780             public void widgetSelected(SelectionEvent e) {
781                 mDevicePanel.killSelectedClient();
782             }
783         });
784 
785         final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE);
786         actionCauseGcItem.setText("Cause &GC");
787         actionCauseGcItem.addSelectionListener(new SelectionAdapter() {
788             @Override
789             public void widgetSelected(SelectionEvent e) {
790                 mDevicePanel.forceGcOnSelectedClient();
791             }
792         });
793 
794         final MenuItem actionResetAdb = new MenuItem(actionMenu, SWT.NONE);
795         actionResetAdb.setText("&Reset adb");
796         actionResetAdb.addSelectionListener(new SelectionAdapter() {
797             @Override
798             public void widgetSelected(SelectionEvent e) {
799                 AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
800                 if (bridge != null) {
801                     bridge.restart();
802                 }
803             }
804         });
805 
806         // configure Action items based on current state
807         actionMenu.addMenuListener(new MenuAdapter() {
808             @Override
809             public void menuShown(MenuEvent e) {
810                 actionHaltItem.setEnabled(mTBHalt.getEnabled() && mCurrentClient != null);
811                 actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled() && mCurrentClient != null);
812                 actionResetAdb.setEnabled(true);
813             }
814         });
815 
816         // create Device menu items
817         final MenuItem screenShotItem = new MenuItem(deviceMenu, SWT.NONE);
818 
819         // The \tCtrl-S "keybinding text" here isn't right for the Mac - but
820         // it's stripped out and replaced by the proper keyboard accelerator
821         // text (e.g. the unicode symbol for the command key + S) anyway
822         // so it's fine to leave it there for the other platforms.
823         screenShotItem.setText("&Screen capture...\tCtrl-S");
824         screenShotItem.setAccelerator('S' | SWT.MOD1);
825         screenShotItem.addSelectionListener(new SelectionAdapter() {
826             @Override
827             public void widgetSelected(SelectionEvent e) {
828                 if (mCurrentDevice != null) {
829                     ScreenShotDialog dlg = new ScreenShotDialog(shell);
830                     dlg.open(mCurrentDevice);
831                 }
832             }
833         });
834 
835         new MenuItem(deviceMenu, SWT.SEPARATOR);
836 
837         final MenuItem explorerItem = new MenuItem(deviceMenu, SWT.NONE);
838         explorerItem.setText("File Explorer...");
839         explorerItem.addSelectionListener(new SelectionAdapter() {
840             @Override
841             public void widgetSelected(SelectionEvent e) {
842                 createFileExplorer();
843             }
844         });
845 
846         new MenuItem(deviceMenu, SWT.SEPARATOR);
847 
848         final MenuItem processItem = new MenuItem(deviceMenu, SWT.NONE);
849         processItem.setText("Show &process status...");
850         processItem.addSelectionListener(new SelectionAdapter() {
851             @Override
852             public void widgetSelected(SelectionEvent e) {
853                 DeviceCommandDialog dlg;
854                 dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell);
855                 dlg.open(mCurrentDevice);
856             }
857         });
858 
859         final MenuItem deviceStateItem = new MenuItem(deviceMenu, SWT.NONE);
860         deviceStateItem.setText("Dump &device state...");
861         deviceStateItem.addSelectionListener(new SelectionAdapter() {
862             @Override
863             public void widgetSelected(SelectionEvent e) {
864                 DeviceCommandDialog dlg;
865                 dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0",
866                         "device-state.txt", shell);
867                 dlg.open(mCurrentDevice);
868             }
869         });
870 
871         final MenuItem appStateItem = new MenuItem(deviceMenu, SWT.NONE);
872         appStateItem.setText("Dump &app state...");
873         appStateItem.setEnabled(false);
874         appStateItem.addSelectionListener(new SelectionAdapter() {
875             @Override
876             public void widgetSelected(SelectionEvent e) {
877                 DeviceCommandDialog dlg;
878                 dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell);
879                 dlg.open(mCurrentDevice);
880             }
881         });
882 
883         final MenuItem radioStateItem = new MenuItem(deviceMenu, SWT.NONE);
884         radioStateItem.setText("Dump &radio state...");
885         radioStateItem.addSelectionListener(new SelectionAdapter() {
886             @Override
887             public void widgetSelected(SelectionEvent e) {
888                 DeviceCommandDialog dlg;
889                 dlg = new DeviceCommandDialog(
890                         "cat /data/logs/radio.4 /data/logs/radio.3"
891                         + " /data/logs/radio.2 /data/logs/radio.1"
892                         + " /data/logs/radio",
893                         "radio-state.txt", shell);
894                 dlg.open(mCurrentDevice);
895             }
896         });
897 
898         final MenuItem logCatItem = new MenuItem(deviceMenu, SWT.NONE);
899         logCatItem.setText("Run &logcat...");
900         logCatItem.addSelectionListener(new SelectionAdapter() {
901             @Override
902             public void widgetSelected(SelectionEvent e) {
903                 DeviceCommandDialog dlg;
904                 dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt",
905                         shell);
906                 dlg.open(mCurrentDevice);
907             }
908         });
909 
910         // configure Action items based on current state
911         deviceMenu.addMenuListener(new MenuAdapter() {
912             @Override
913             public void menuShown(MenuEvent e) {
914                 boolean deviceEnabled = mCurrentDevice != null;
915                 screenShotItem.setEnabled(deviceEnabled);
916                 explorerItem.setEnabled(deviceEnabled);
917                 processItem.setEnabled(deviceEnabled);
918                 deviceStateItem.setEnabled(deviceEnabled);
919                 appStateItem.setEnabled(deviceEnabled);
920                 radioStateItem.setEnabled(deviceEnabled);
921                 logCatItem.setEnabled(deviceEnabled);
922             }
923         });
924 
925         // tell the shell to use this menu
926         shell.setMenuBar(menuBar);
927     }
928 
929     /*
930      * Create the widgets in the main application window. The basic layout is a
931      * two-panel sash, with a scrolling list of VMs on the left and detailed
932      * output for a single VM on the right.
933      */
createWidgets(final Shell shell)934     private void createWidgets(final Shell shell) {
935         Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
936 
937         /*
938          * Create three areas: tool bar, split panels, status line
939          */
940         shell.setLayout(new GridLayout(1, false));
941 
942         // 1. panel area
943         final Composite panelArea = new Composite(shell, SWT.BORDER);
944 
945         // make the panel area absorb all space
946         panelArea.setLayoutData(new GridData(GridData.FILL_BOTH));
947 
948         // 2. status line.
949         mStatusLine = new Label(shell, SWT.NONE);
950 
951         // make status line extend all the way across
952         mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
953 
954         mStatusLine.setText("Initializing...");
955 
956         /*
957          * Configure the split-panel area.
958          */
959         final PreferenceStore prefs = PrefsDialog.getStore();
960 
961         Composite topPanel = new Composite(panelArea, SWT.NONE);
962         final Sash sash = new Sash(panelArea, SWT.HORIZONTAL);
963         sash.setBackground(darkGray);
964         Composite bottomPanel = new Composite(panelArea, SWT.NONE);
965 
966         panelArea.setLayout(new FormLayout());
967 
968         createTopPanel(topPanel, darkGray);
969 
970         mClipboard = new Clipboard(panelArea.getDisplay());
971         if (useOldLogCatView()) {
972             createBottomPanel(bottomPanel);
973         } else {
974             createLogCatView(bottomPanel);
975         }
976 
977         // form layout data
978         FormData data = new FormData();
979         data.top = new FormAttachment(0, 0);
980         data.bottom = new FormAttachment(sash, 0);
981         data.left = new FormAttachment(0, 0);
982         data.right = new FormAttachment(100, 0);
983         topPanel.setLayoutData(data);
984 
985         final FormData sashData = new FormData();
986         if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) {
987             sashData.top = new FormAttachment(0, prefs.getInt(
988                     PREFERENCE_LOGSASH));
989         } else {
990             sashData.top = new FormAttachment(50,0); // 50% across
991         }
992         sashData.left = new FormAttachment(0, 0);
993         sashData.right = new FormAttachment(100, 0);
994         sash.setLayoutData(sashData);
995 
996         data = new FormData();
997         data.top = new FormAttachment(sash, 0);
998         data.bottom = new FormAttachment(100, 0);
999         data.left = new FormAttachment(0, 0);
1000         data.right = new FormAttachment(100, 0);
1001         bottomPanel.setLayoutData(data);
1002 
1003         // allow resizes, but cap at minPanelWidth
1004         sash.addListener(SWT.Selection, new Listener() {
1005             public void handleEvent(Event e) {
1006                 Rectangle sashRect = sash.getBounds();
1007                 Rectangle panelRect = panelArea.getClientArea();
1008                 int bottom = panelRect.height - sashRect.height - 100;
1009                 e.y = Math.max(Math.min(e.y, bottom), 100);
1010                 if (e.y != sashRect.y) {
1011                     sashData.top = new FormAttachment(0, e.y);
1012                     if (prefs != null) {
1013                         prefs.setValue(PREFERENCE_LOGSASH, e.y);
1014                     }
1015                     panelArea.layout();
1016                 }
1017             }
1018         });
1019 
1020         // add a global focus listener for all the tables
1021         mTableListener = new TableFocusListener();
1022 
1023         // now set up the listener in the various panels
1024         if (useOldLogCatView()) {
1025             mLogPanel.setTableFocusListener(mTableListener);
1026         } else {
1027             mLogCatPanel.setTableFocusListener(mTableListener);
1028         }
1029         mEventLogPanel.setTableFocusListener(mTableListener);
1030         for (TablePanel p : mPanels) {
1031             if (p != null) {
1032                 p.setTableFocusListener(mTableListener);
1033             }
1034         }
1035 
1036         mStatusLine.setText("");
1037     }
1038 
1039     /*
1040      * Populate the tool bar.
1041      */
createDevicePanelToolBar(ToolBar toolBar)1042     private void createDevicePanelToolBar(ToolBar toolBar) {
1043         Display display = toolBar.getDisplay();
1044 
1045         // add "show heap updates" button
1046         mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK);
1047         mTBShowHeapUpdates.setImage(mDdmUiLibLoader.loadImage(display,
1048                 DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1049         mTBShowHeapUpdates.setToolTipText("Show heap updates");
1050         mTBShowHeapUpdates.setEnabled(false);
1051         mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() {
1052             @Override
1053             public void widgetSelected(SelectionEvent e) {
1054                 if (mCurrentClient != null) {
1055                     // boolean status = ((ToolItem)e.item).getSelection();
1056                     // invert previous state
1057                     boolean enable = !mCurrentClient.isHeapUpdateEnabled();
1058                     mCurrentClient.setHeapUpdateEnabled(enable);
1059                 } else {
1060                     e.doit = false; // this has no effect?
1061                 }
1062             }
1063         });
1064 
1065         // add "dump HPROF" button
1066         mTBDumpHprof = new ToolItem(toolBar, SWT.PUSH);
1067         mTBDumpHprof.setToolTipText("Dump HPROF file");
1068         mTBDumpHprof.setEnabled(false);
1069         mTBDumpHprof.setImage(mDdmUiLibLoader.loadImage(display,
1070                 DevicePanel.ICON_HPROF, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1071         mTBDumpHprof.addSelectionListener(new SelectionAdapter() {
1072             @Override
1073             public void widgetSelected(SelectionEvent e) {
1074                 mDevicePanel.dumpHprof();
1075 
1076                 // this will make sure the dump hprof button is disabled for the current selection
1077                 // as the client is already dumping an hprof file
1078                 enableButtons();
1079             }
1080         });
1081 
1082         // add "cause GC" button
1083         mTBCauseGc = new ToolItem(toolBar, SWT.PUSH);
1084         mTBCauseGc.setToolTipText("Cause an immediate GC");
1085         mTBCauseGc.setEnabled(false);
1086         mTBCauseGc.setImage(mDdmUiLibLoader.loadImage(display,
1087                 DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1088         mTBCauseGc.addSelectionListener(new SelectionAdapter() {
1089             @Override
1090             public void widgetSelected(SelectionEvent e) {
1091                 mDevicePanel.forceGcOnSelectedClient();
1092             }
1093         });
1094 
1095         new ToolItem(toolBar, SWT.SEPARATOR);
1096 
1097         // add "show thread updates" button
1098         mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK);
1099         mTBShowThreadUpdates.setImage(mDdmUiLibLoader.loadImage(display,
1100                 DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1101         mTBShowThreadUpdates.setToolTipText("Show thread updates");
1102         mTBShowThreadUpdates.setEnabled(false);
1103         mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() {
1104             @Override
1105             public void widgetSelected(SelectionEvent e) {
1106                 if (mCurrentClient != null) {
1107                     // boolean status = ((ToolItem)e.item).getSelection();
1108                     // invert previous state
1109                     boolean enable = !mCurrentClient.isThreadUpdateEnabled();
1110 
1111                     mCurrentClient.setThreadUpdateEnabled(enable);
1112                 } else {
1113                     e.doit = false; // this has no effect?
1114                 }
1115             }
1116         });
1117 
1118         // add a start/stop method tracing
1119         mTracingStartImage = mDdmUiLibLoader.loadImage(display,
1120                 DevicePanel.ICON_TRACING_START,
1121                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
1122         mTracingStopImage = mDdmUiLibLoader.loadImage(display,
1123                 DevicePanel.ICON_TRACING_STOP,
1124                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
1125         mTBProfiling = new ToolItem(toolBar, SWT.PUSH);
1126         mTBProfiling.setToolTipText("Start Method Profiling");
1127         mTBProfiling.setEnabled(false);
1128         mTBProfiling.setImage(mTracingStartImage);
1129         mTBProfiling.addSelectionListener(new SelectionAdapter() {
1130             @Override
1131             public void widgetSelected(SelectionEvent e) {
1132                 mDevicePanel.toggleMethodProfiling();
1133             }
1134         });
1135 
1136         new ToolItem(toolBar, SWT.SEPARATOR);
1137 
1138         // add "kill VM" button; need to make this visually distinct from
1139         // the status update buttons
1140         mTBHalt = new ToolItem(toolBar, SWT.PUSH);
1141         mTBHalt.setToolTipText("Halt the target VM");
1142         mTBHalt.setEnabled(false);
1143         mTBHalt.setImage(mDdmUiLibLoader.loadImage(display,
1144                 DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1145         mTBHalt.addSelectionListener(new SelectionAdapter() {
1146             @Override
1147             public void widgetSelected(SelectionEvent e) {
1148                 mDevicePanel.killSelectedClient();
1149             }
1150         });
1151 
1152        toolBar.pack();
1153     }
1154 
createTopPanel(final Composite comp, Color darkGray)1155     private void createTopPanel(final Composite comp, Color darkGray) {
1156         final PreferenceStore prefs = PrefsDialog.getStore();
1157 
1158         comp.setLayout(new FormLayout());
1159 
1160         Composite leftPanel = new Composite(comp, SWT.NONE);
1161         final Sash sash = new Sash(comp, SWT.VERTICAL);
1162         sash.setBackground(darkGray);
1163         Composite rightPanel = new Composite(comp, SWT.NONE);
1164 
1165         createLeftPanel(leftPanel);
1166         createRightPanel(rightPanel);
1167 
1168         FormData data = new FormData();
1169         data.top = new FormAttachment(0, 0);
1170         data.bottom = new FormAttachment(100, 0);
1171         data.left = new FormAttachment(0, 0);
1172         data.right = new FormAttachment(sash, 0);
1173         leftPanel.setLayoutData(data);
1174 
1175         final FormData sashData = new FormData();
1176         sashData.top = new FormAttachment(0, 0);
1177         sashData.bottom = new FormAttachment(100, 0);
1178         if (prefs != null && prefs.contains(PREFERENCE_SASH)) {
1179             sashData.left = new FormAttachment(0, prefs.getInt(
1180                     PREFERENCE_SASH));
1181         } else {
1182             // position the sash 380 from the right instead of x% (done by using
1183             // FormAttachment(x, 0)) in order to keep the sash at the same
1184             // position
1185             // from the left when the window is resized.
1186             // 380px is just enough to display the left table with no horizontal
1187             // scrollbar with the default font.
1188             sashData.left = new FormAttachment(0, 380);
1189         }
1190         sash.setLayoutData(sashData);
1191 
1192         data = new FormData();
1193         data.top = new FormAttachment(0, 0);
1194         data.bottom = new FormAttachment(100, 0);
1195         data.left = new FormAttachment(sash, 0);
1196         data.right = new FormAttachment(100, 0);
1197         rightPanel.setLayoutData(data);
1198 
1199         final int minPanelWidth = 60;
1200 
1201         // allow resizes, but cap at minPanelWidth
1202         sash.addListener(SWT.Selection, new Listener() {
1203             public void handleEvent(Event e) {
1204                 Rectangle sashRect = sash.getBounds();
1205                 Rectangle panelRect = comp.getClientArea();
1206                 int right = panelRect.width - sashRect.width - minPanelWidth;
1207                 e.x = Math.max(Math.min(e.x, right), minPanelWidth);
1208                 if (e.x != sashRect.x) {
1209                     sashData.left = new FormAttachment(0, e.x);
1210                     if (prefs != null) {
1211                         prefs.setValue(PREFERENCE_SASH, e.x);
1212                     }
1213                     comp.layout();
1214                 }
1215             }
1216         });
1217     }
1218 
createBottomPanel(final Composite comp)1219     private void createBottomPanel(final Composite comp) {
1220         final PreferenceStore prefs = PrefsDialog.getStore();
1221 
1222         // create clipboard
1223         Display display = comp.getDisplay();
1224 
1225         LogColors colors = new LogColors();
1226 
1227         colors.infoColor = new Color(display, 0, 127, 0);
1228         colors.debugColor = new Color(display, 0, 0, 127);
1229         colors.errorColor = new Color(display, 255, 0, 0);
1230         colors.warningColor = new Color(display, 255, 127, 0);
1231         colors.verboseColor = new Color(display, 0, 0, 0);
1232 
1233         // set the preferences names
1234         LogPanel.PREFS_TIME = PREFS_COL_TIME;
1235         LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL;
1236         LogPanel.PREFS_PID = PREFS_COL_PID;
1237         LogPanel.PREFS_TAG = PREFS_COL_TAG;
1238         LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE;
1239 
1240         comp.setLayout(new GridLayout(1, false));
1241 
1242         ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL);
1243 
1244         mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
1245         mCreateFilterAction.item.setToolTipText("Create Filter");
1246         mCreateFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1247                 "add.png", //$NON-NLS-1$
1248                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1249         mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() {
1250             @Override
1251             public void widgetSelected(SelectionEvent e) {
1252                 mLogPanel.addFilter();
1253             }
1254         });
1255 
1256         mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
1257         mEditFilterAction.item.setToolTipText("Edit Filter");
1258         mEditFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1259                 "edit.png", //$NON-NLS-1$
1260                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1261         mEditFilterAction.item.addSelectionListener(new SelectionAdapter() {
1262             @Override
1263             public void widgetSelected(SelectionEvent e) {
1264                 mLogPanel.editFilter();
1265             }
1266         });
1267 
1268         mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
1269         mDeleteFilterAction.item.setToolTipText("Delete Filter");
1270         mDeleteFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1271                 "delete.png", //$NON-NLS-1$
1272                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1273         mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() {
1274             @Override
1275             public void widgetSelected(SelectionEvent e) {
1276                 mLogPanel.deleteFilter();
1277             }
1278         });
1279 
1280 
1281         new ToolItem(toolBar, SWT.SEPARATOR);
1282 
1283         LogLevel[] levels = LogLevel.values();
1284         mLogLevelActions = new ToolItemAction[mLogLevelIcons.length];
1285         for (int i = 0 ; i < mLogLevelActions.length; i++) {
1286             String name = levels[i].getStringValue();
1287             final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK);
1288             mLogLevelActions[i] = newAction;
1289             //newAction.item.setText(name);
1290             newAction.item.addSelectionListener(new SelectionAdapter() {
1291                 @Override
1292                 public void widgetSelected(SelectionEvent e) {
1293                     // disable the other actions and record current index
1294                     for (int k = 0 ; k < mLogLevelActions.length; k++) {
1295                         ToolItemAction a = mLogLevelActions[k];
1296                         if (a == newAction) {
1297                             a.setChecked(true);
1298 
1299                             // set the log level
1300                             mLogPanel.setCurrentFilterLogLevel(k+2);
1301                         } else {
1302                             a.setChecked(false);
1303                         }
1304                     }
1305                 }
1306             });
1307 
1308             newAction.item.setToolTipText(name);
1309             newAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1310                     mLogLevelIcons[i],
1311                     DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1312         }
1313 
1314         new ToolItem(toolBar, SWT.SEPARATOR);
1315 
1316         mClearAction = new ToolItemAction(toolBar, SWT.PUSH);
1317         mClearAction.item.setToolTipText("Clear Log");
1318 
1319         mClearAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1320                 "clear.png", //$NON-NLS-1$
1321                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1322         mClearAction.item.addSelectionListener(new SelectionAdapter() {
1323             @Override
1324             public void widgetSelected(SelectionEvent e) {
1325                 mLogPanel.clear();
1326             }
1327         });
1328 
1329         new ToolItem(toolBar, SWT.SEPARATOR);
1330 
1331         mExportAction = new ToolItemAction(toolBar, SWT.PUSH);
1332         mExportAction.item.setToolTipText("Export Selection As Text...");
1333         mExportAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
1334                 "save.png", //$NON-NLS-1$
1335                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1336         mExportAction.item.addSelectionListener(new SelectionAdapter() {
1337             @Override
1338             public void widgetSelected(SelectionEvent e) {
1339                 mLogPanel.save();
1340             }
1341         });
1342 
1343 
1344         toolBar.pack();
1345 
1346         // now create the log view
1347         mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL);
1348 
1349         mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions);
1350 
1351         String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE);
1352         if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) {
1353             mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO);
1354         }
1355 
1356         String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT);
1357         if (fontStr != null) {
1358             try {
1359                 FontData fdat = new FontData(fontStr);
1360                 mLogPanel.setFont(new Font(display, fdat));
1361             } catch (IllegalArgumentException e) {
1362                 // Looks like fontStr isn't a valid font representation.
1363                 // We do nothing in this case, the logcat view will use the default font.
1364             } catch (SWTError e2) {
1365                 // Looks like the Font() constructor failed.
1366                 // We do nothing in this case, the logcat view will use the default font.
1367             }
1368         }
1369 
1370         mLogPanel.createPanel(comp);
1371 
1372         // and start the logcat
1373         mLogPanel.startLogCat(mCurrentDevice);
1374     }
1375 
createLogCatView(Composite parent)1376     private void createLogCatView(Composite parent) {
1377         IPreferenceStore prefStore = DdmUiPreferences.getStore();
1378         mLogCatPanel = new LogCatPanel(prefStore);
1379         mLogCatPanel.createPanel(parent);
1380 
1381         if (mCurrentDevice != null) {
1382             mLogCatPanel.deviceSelected(mCurrentDevice);
1383         }
1384     }
1385 
1386     /*
1387      * Create the contents of the left panel: a table of VMs.
1388      */
createLeftPanel(final Composite comp)1389     private void createLeftPanel(final Composite comp) {
1390         comp.setLayout(new GridLayout(1, false));
1391         ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP);
1392         toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
1393         createDevicePanelToolBar(toolBar);
1394 
1395         Composite c = new Composite(comp, SWT.NONE);
1396         c.setLayoutData(new GridData(GridData.FILL_BOTH));
1397 
1398         mDevicePanel = new DevicePanel(true /* showPorts */);
1399         mDevicePanel.createPanel(c);
1400 
1401         // add ourselves to the device panel selection listener
1402         mDevicePanel.addSelectionListener(this);
1403     }
1404 
1405     /*
1406      * Create the contents of the right panel: tabs with VM information.
1407      */
createRightPanel(final Composite comp)1408     private void createRightPanel(final Composite comp) {
1409         TabItem item;
1410         TabFolder tabFolder;
1411 
1412         comp.setLayout(new FillLayout());
1413 
1414         tabFolder = new TabFolder(comp, SWT.NONE);
1415 
1416         for (int i = 0; i < mPanels.length; i++) {
1417             if (mPanels[i] != null) {
1418                 item = new TabItem(tabFolder, SWT.NONE);
1419                 item.setText(mPanelNames[i]);
1420                 item.setToolTipText(mPanelTips[i]);
1421                 item.setControl(mPanels[i].createPanel(tabFolder));
1422             }
1423         }
1424 
1425         // add the emulator control panel to the folders.
1426         item = new TabItem(tabFolder, SWT.NONE);
1427         item.setText("Emulator Control");
1428         item.setToolTipText("Emulator Control Panel");
1429         mEmulatorPanel = new EmulatorControlPanel();
1430         item.setControl(mEmulatorPanel.createPanel(tabFolder));
1431 
1432         // add the event log panel to the folders.
1433         item = new TabItem(tabFolder, SWT.NONE);
1434         item.setText("Event Log");
1435         item.setToolTipText("Event Log");
1436 
1437         // create the composite that will hold the toolbar and the event log panel.
1438         Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE);
1439         item.setControl(eventLogTopComposite);
1440         eventLogTopComposite.setLayout(new GridLayout(1, false));
1441 
1442         // create the toolbar and the actions
1443         ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL);
1444         toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
1445 
1446         ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH);
1447         optionsAction.item.setToolTipText("Opens the options panel");
1448         optionsAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
1449                 "edit.png", //$NON-NLS-1$
1450                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1451 
1452         ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH);
1453         clearAction.item.setToolTipText("Clears the event log");
1454         clearAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
1455                 "clear.png", //$NON-NLS-1$
1456                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1457 
1458         new ToolItem(toolbar, SWT.SEPARATOR);
1459 
1460         ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH);
1461         saveAction.item.setToolTipText("Saves the event log");
1462         saveAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
1463                 "save.png", //$NON-NLS-1$
1464                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1465 
1466         ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH);
1467         loadAction.item.setToolTipText("Loads an event log");
1468         loadAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
1469                 "load.png", //$NON-NLS-1$
1470                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1471 
1472         ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH);
1473         importBugAction.item.setToolTipText("Imports a bug report");
1474         importBugAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
1475                 "importBug.png", //$NON-NLS-1$
1476                 DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
1477 
1478         // create the event log panel
1479         mEventLogPanel = new EventLogPanel();
1480 
1481         // set the external actions
1482         mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction,
1483                 importBugAction);
1484 
1485         // create the panel
1486         mEventLogPanel.createPanel(eventLogTopComposite);
1487     }
1488 
createFileExplorer()1489     private void createFileExplorer() {
1490         if (mExplorer == null) {
1491             mExplorerShell = new Shell(mDisplay);
1492 
1493             // create the ui
1494             mExplorerShell.setLayout(new GridLayout(1, false));
1495 
1496             // toolbar + action
1497             ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL);
1498             toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
1499 
1500             ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH);
1501             pullAction.item.setToolTipText("Pull File from Device");
1502             Image image = mDdmUiLibLoader.loadImage("pull.png", mDisplay); //$NON-NLS-1$
1503             if (image != null) {
1504                 pullAction.item.setImage(image);
1505             } else {
1506                 // this is for debugging purpose when the icon is missing
1507                 pullAction.item.setText("Pull"); //$NON-NLS-1$
1508             }
1509 
1510             ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH);
1511             pushAction.item.setToolTipText("Push file onto Device");
1512             image = mDdmUiLibLoader.loadImage("push.png", mDisplay); //$NON-NLS-1$
1513             if (image != null) {
1514                 pushAction.item.setImage(image);
1515             } else {
1516                 // this is for debugging purpose when the icon is missing
1517                 pushAction.item.setText("Push"); //$NON-NLS-1$
1518             }
1519 
1520             ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH);
1521             deleteAction.item.setToolTipText("Delete");
1522             image = mDdmUiLibLoader.loadImage("delete.png", mDisplay); //$NON-NLS-1$
1523             if (image != null) {
1524                 deleteAction.item.setImage(image);
1525             } else {
1526                 // this is for debugging purpose when the icon is missing
1527                 deleteAction.item.setText("Delete"); //$NON-NLS-1$
1528             }
1529 
1530             ToolItemAction createNewFolderAction = new ToolItemAction(toolBar, SWT.PUSH);
1531             createNewFolderAction.item.setToolTipText("New Folder");
1532             image = mDdmUiLibLoader.loadImage("add.png", mDisplay); //$NON-NLS-1$
1533             if (image != null) {
1534                 createNewFolderAction.item.setImage(image);
1535             } else {
1536                 // this is for debugging purpose when the icon is missing
1537                 createNewFolderAction.item.setText("New Folder"); //$NON-NLS-1$
1538             }
1539 
1540             // device explorer
1541             mExplorer = new DeviceExplorer();
1542             mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction);
1543 
1544             pullAction.item.addSelectionListener(new SelectionAdapter() {
1545                 @Override
1546                 public void widgetSelected(SelectionEvent e) {
1547                     mExplorer.pullSelection();
1548                 }
1549             });
1550             pullAction.setEnabled(false);
1551 
1552             pushAction.item.addSelectionListener(new SelectionAdapter() {
1553                 @Override
1554                 public void widgetSelected(SelectionEvent e) {
1555                     mExplorer.pushIntoSelection();
1556                 }
1557             });
1558             pushAction.setEnabled(false);
1559 
1560             deleteAction.item.addSelectionListener(new SelectionAdapter() {
1561                 @Override
1562                 public void widgetSelected(SelectionEvent e) {
1563                     mExplorer.deleteSelection();
1564                 }
1565             });
1566             deleteAction.setEnabled(false);
1567 
1568             createNewFolderAction.item.addSelectionListener(new SelectionAdapter() {
1569                 @Override
1570                 public void widgetSelected(SelectionEvent e) {
1571                     mExplorer.createNewFolderInSelection();
1572                 }
1573             });
1574             createNewFolderAction.setEnabled(false);
1575 
1576             Composite parent = new Composite(mExplorerShell, SWT.NONE);
1577             parent.setLayoutData(new GridData(GridData.FILL_BOTH));
1578 
1579             mExplorer.createPanel(parent);
1580             mExplorer.switchDevice(mCurrentDevice);
1581 
1582             mExplorerShell.addShellListener(new ShellListener() {
1583                 public void shellActivated(ShellEvent e) {
1584                     // pass
1585                 }
1586 
1587                 public void shellClosed(ShellEvent e) {
1588                     mExplorer = null;
1589                     mExplorerShell = null;
1590                 }
1591 
1592                 public void shellDeactivated(ShellEvent e) {
1593                     // pass
1594                 }
1595 
1596                 public void shellDeiconified(ShellEvent e) {
1597                     // pass
1598                 }
1599 
1600                 public void shellIconified(ShellEvent e) {
1601                     // pass
1602                 }
1603             });
1604 
1605             mExplorerShell.pack();
1606             setExplorerSizeAndPosition(mExplorerShell);
1607             mExplorerShell.open();
1608         } else {
1609             if (mExplorerShell != null) {
1610                 mExplorerShell.forceActive();
1611             }
1612         }
1613     }
1614 
1615     /**
1616      * Set the status line. TODO: make this a stack, so we can safely have
1617      * multiple things trying to set it all at once. Also specify an expiration?
1618      */
setStatusLine(final String str)1619     public void setStatusLine(final String str) {
1620         try {
1621             mDisplay.asyncExec(new Runnable() {
1622                 public void run() {
1623                     doSetStatusLine(str);
1624                 }
1625             });
1626         } catch (SWTException swte) {
1627             if (!mDisplay.isDisposed())
1628                 throw swte;
1629         }
1630     }
1631 
doSetStatusLine(String str)1632     private void doSetStatusLine(String str) {
1633         if (mStatusLine.isDisposed())
1634             return;
1635 
1636         if (!mStatusLine.getText().equals(str)) {
1637             mStatusLine.setText(str);
1638 
1639             // try { Thread.sleep(100); }
1640             // catch (InterruptedException ie) {}
1641         }
1642     }
1643 
displayError(final String msg)1644     public void displayError(final String msg) {
1645         try {
1646             mDisplay.syncExec(new Runnable() {
1647                 public void run() {
1648                     MessageDialog.openError(mDisplay.getActiveShell(), "Error",
1649                             msg);
1650                 }
1651             });
1652         } catch (SWTException swte) {
1653             if (!mDisplay.isDisposed())
1654                 throw swte;
1655         }
1656     }
1657 
enableButtons()1658     private void enableButtons() {
1659         if (mCurrentClient != null) {
1660             mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled());
1661             mTBShowThreadUpdates.setEnabled(true);
1662             mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled());
1663             mTBShowHeapUpdates.setEnabled(true);
1664             mTBHalt.setEnabled(true);
1665             mTBCauseGc.setEnabled(true);
1666 
1667             ClientData data = mCurrentClient.getClientData();
1668 
1669             if (data.hasFeature(ClientData.FEATURE_HPROF)) {
1670                 mTBDumpHprof.setEnabled(data.hasPendingHprofDump() == false);
1671                 mTBDumpHprof.setToolTipText("Dump HPROF file");
1672             } else {
1673                 mTBDumpHprof.setEnabled(false);
1674                 mTBDumpHprof.setToolTipText("Dump HPROF file (not supported by this VM)");
1675             }
1676 
1677             if (data.hasFeature(ClientData.FEATURE_PROFILING)) {
1678                 mTBProfiling.setEnabled(true);
1679                 if (data.getMethodProfilingStatus() == MethodProfilingStatus.ON) {
1680                     mTBProfiling.setToolTipText("Stop Method Profiling");
1681                     mTBProfiling.setImage(mTracingStopImage);
1682                 } else {
1683                     mTBProfiling.setToolTipText("Start Method Profiling");
1684                     mTBProfiling.setImage(mTracingStartImage);
1685                 }
1686             } else {
1687                 mTBProfiling.setEnabled(false);
1688                 mTBProfiling.setImage(mTracingStartImage);
1689                 mTBProfiling.setToolTipText("Start Method Profiling (not supported by this VM)");
1690             }
1691         } else {
1692             // list is empty, disable these
1693             mTBShowThreadUpdates.setSelection(false);
1694             mTBShowThreadUpdates.setEnabled(false);
1695             mTBShowHeapUpdates.setSelection(false);
1696             mTBShowHeapUpdates.setEnabled(false);
1697             mTBHalt.setEnabled(false);
1698             mTBCauseGc.setEnabled(false);
1699 
1700             mTBDumpHprof.setEnabled(false);
1701             mTBDumpHprof.setToolTipText("Dump HPROF file");
1702 
1703             mTBProfiling.setEnabled(false);
1704             mTBProfiling.setImage(mTracingStartImage);
1705             mTBProfiling.setToolTipText("Start Method Profiling");
1706         }
1707     }
1708 
1709     /**
1710      * Sent when a new {@link IDevice} and {@link Client} are selected.
1711      * @param selectedDevice the selected device. If null, no devices are selected.
1712      * @param selectedClient The selected client. If null, no clients are selected.
1713      *
1714      * @see IUiSelectionListener
1715      */
selectionChanged(IDevice selectedDevice, Client selectedClient)1716     public void selectionChanged(IDevice selectedDevice, Client selectedClient) {
1717         if (mCurrentDevice != selectedDevice) {
1718             mCurrentDevice = selectedDevice;
1719             for (TablePanel panel : mPanels) {
1720                 if (panel != null) {
1721                     panel.deviceSelected(mCurrentDevice);
1722                 }
1723             }
1724 
1725             mEmulatorPanel.deviceSelected(mCurrentDevice);
1726             if (useOldLogCatView()) {
1727                 mLogPanel.deviceSelected(mCurrentDevice);
1728             } else {
1729                 mLogCatPanel.deviceSelected(mCurrentDevice);
1730             }
1731             if (mEventLogPanel != null) {
1732                 mEventLogPanel.deviceSelected(mCurrentDevice);
1733             }
1734 
1735             if (mExplorer != null) {
1736                 mExplorer.switchDevice(mCurrentDevice);
1737             }
1738         }
1739 
1740         if (mCurrentClient != selectedClient) {
1741             AndroidDebugBridge.getBridge().setSelectedClient(selectedClient);
1742             mCurrentClient = selectedClient;
1743             for (TablePanel panel : mPanels) {
1744                 if (panel != null) {
1745                     panel.clientSelected(mCurrentClient);
1746                 }
1747             }
1748 
1749             enableButtons();
1750         }
1751     }
1752 
clientChanged(Client client, int changeMask)1753     public void clientChanged(Client client, int changeMask) {
1754         if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) ==
1755                 Client.CHANGE_METHOD_PROFILING_STATUS) {
1756             if (mCurrentClient == client) {
1757                 mDisplay.asyncExec(new Runnable() {
1758                     public void run() {
1759                         // force refresh of the button enabled state.
1760                         enableButtons();
1761                     }
1762                 });
1763             }
1764         }
1765     }
1766 }
1767