• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.sdkuilib.internal.widgets;
18 
19 import com.android.prefs.AndroidLocation.AndroidLocationException;
20 import com.android.sdklib.IAndroidTarget;
21 import com.android.sdklib.ISdkLog;
22 import com.android.sdklib.NullSdkLog;
23 import com.android.sdklib.SdkConstants;
24 import com.android.sdklib.internal.avd.AvdManager;
25 import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
26 import com.android.sdklib.internal.avd.AvdManager.AvdInfo.AvdStatus;
27 import com.android.sdklib.internal.repository.ITask;
28 import com.android.sdklib.internal.repository.ITaskMonitor;
29 import com.android.sdkuilib.internal.repository.SettingsController;
30 import com.android.sdkuilib.internal.repository.icons.ImageFactory;
31 import com.android.sdkuilib.internal.tasks.ProgressTask;
32 import com.android.sdkuilib.repository.UpdaterWindow;
33 
34 import org.eclipse.jface.dialogs.MessageDialog;
35 import org.eclipse.jface.window.Window;
36 import org.eclipse.swt.SWT;
37 import org.eclipse.swt.events.ControlAdapter;
38 import org.eclipse.swt.events.ControlEvent;
39 import org.eclipse.swt.events.DisposeEvent;
40 import org.eclipse.swt.events.DisposeListener;
41 import org.eclipse.swt.events.SelectionAdapter;
42 import org.eclipse.swt.events.SelectionEvent;
43 import org.eclipse.swt.events.SelectionListener;
44 import org.eclipse.swt.graphics.Image;
45 import org.eclipse.swt.graphics.Rectangle;
46 import org.eclipse.swt.layout.GridData;
47 import org.eclipse.swt.layout.GridLayout;
48 import org.eclipse.swt.widgets.Button;
49 import org.eclipse.swt.widgets.Composite;
50 import org.eclipse.swt.widgets.Display;
51 import org.eclipse.swt.widgets.Label;
52 import org.eclipse.swt.widgets.Shell;
53 import org.eclipse.swt.widgets.Table;
54 import org.eclipse.swt.widgets.TableColumn;
55 import org.eclipse.swt.widgets.TableItem;
56 
57 import java.io.BufferedReader;
58 import java.io.File;
59 import java.io.IOException;
60 import java.io.InputStreamReader;
61 import java.util.ArrayList;
62 
63 
64 /**
65  * The AVD selector is a table that is added to the given parent composite.
66  * <p/>
67  * After using one of the constructors, call {@link #setSelection(AvdInfo)},
68  * {@link #setSelectionListener(SelectionListener)} and finally use
69  * {@link #getSelected()} to retrieve the selection.
70  */
71 public final class AvdSelector {
72     private static int NUM_COL = 2;
73 
74     private final DisplayMode mDisplayMode;
75 
76     private AvdManager mAvdManager;
77     private final String mOsSdkPath;
78 
79     private Table mTable;
80     private Button mDeleteButton;
81     private Button mDetailsButton;
82     private Button mNewButton;
83     private Button mRefreshButton;
84     private Button mManagerButton;
85     private Button mRepairButton;
86     private Button mStartButton;
87 
88     private SelectionListener mSelectionListener;
89     private IAvdFilter mTargetFilter;
90 
91     /** Defaults to true. Changed by the {@link #setEnabled(boolean)} method to represent the
92      * "global" enabled state on this composite. */
93     private boolean mIsEnabled = true;
94 
95     private ImageFactory mImageFactory;
96     private Image mOkImage;
97     private Image mBrokenImage;
98 
99     private SettingsController mController;
100 
101 
102     /**
103      * The display mode of the AVD Selector.
104      */
105     public static enum DisplayMode {
106         /**
107          * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs
108          */
109         MANAGER,
110 
111         /**
112          * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but
113          * there is a button to open the AVD Manager.
114          * In the "check" selection mode, checkboxes are displayed on each line
115          * and {@link AvdSelector#getSelected()} returns the line that is checked
116          * even if it is not the currently selected line. Only one line can
117          * be checked at once.
118          */
119         SIMPLE_CHECK,
120 
121         /**
122          * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but
123          * there is a button to open the AVD Manager.
124          * In the "select" selection mode, there are no checkboxes and
125          * {@link AvdSelector#getSelected()} returns the line currently selected.
126          * Only one line can be selected at once.
127          */
128         SIMPLE_SELECTION,
129     }
130 
131     /**
132      * A filter to control the whether or not an AVD should be displayed by the AVD Selector.
133      */
134     public interface IAvdFilter {
135         /**
136          * Called before {@link #accept(AvdInfo)} is called for any AVD.
137          */
prepare()138         void prepare();
139 
140         /**
141          * Called to decided whether an AVD should be displayed.
142          * @param avd the AVD to test.
143          * @return true if the AVD should be displayed.
144          */
accept(AvdInfo avd)145         boolean accept(AvdInfo avd);
146 
147         /**
148          * Called after {@link #accept(AvdInfo)} has been called on all the AVDs.
149          */
cleanup()150         void cleanup();
151     }
152 
153     /**
154      * Internal implementation of {@link IAvdFilter} to filter out the AVDs that are not
155      * running an image compatible with a specific target.
156      */
157     private final static class TargetBasedFilter implements IAvdFilter {
158         private final IAndroidTarget mTarget;
159 
TargetBasedFilter(IAndroidTarget target)160         TargetBasedFilter(IAndroidTarget target) {
161             mTarget = target;
162         }
163 
prepare()164         public void prepare() {
165             // nothing to prepare
166         }
167 
accept(AvdInfo avd)168         public boolean accept(AvdInfo avd) {
169             if (avd != null) {
170                 return mTarget.isCompatibleBaseFor(avd.getTarget());
171             }
172 
173             return false;
174         }
175 
cleanup()176         public void cleanup() {
177             // nothing to clean up
178         }
179     }
180 
181     /**
182      * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered
183      * by a {@link IAndroidTarget}.
184      * <p/>Only the {@link AvdInfo} able to run application developed for the given
185      * {@link IAndroidTarget} will be displayed.
186      *
187      * @param parent The parent composite where the selector will be added.
188      * @param osSdkPath The SDK root path. When not null, enables the start button to start
189      *                  an emulator on a given AVD.
190      * @param manager the AVD manager.
191      * @param filter When non-null, will allow filtering the AVDs to display.
192      * @param displayMode The display mode ({@link DisplayMode}).
193      *
194      * TODO: pass an ISdkLog and use it when reloading, starting the emulator, etc.
195      */
AvdSelector(Composite parent, String osSdkPath, AvdManager manager, IAvdFilter filter, DisplayMode displayMode)196     public AvdSelector(Composite parent,
197             String osSdkPath,
198             AvdManager manager,
199             IAvdFilter filter,
200             DisplayMode displayMode) {
201         mOsSdkPath = osSdkPath;
202         mAvdManager = manager;
203         mTargetFilter = filter;
204         mDisplayMode = displayMode;
205 
206         // get some bitmaps.
207         mImageFactory = new ImageFactory(parent.getDisplay());
208         mOkImage = mImageFactory.getImageByName("accept_icon16.png");
209         mBrokenImage = mImageFactory.getImageByName("reject_icon16.png");
210 
211         // Layout has 2 columns
212         Composite group = new Composite(parent, SWT.NONE);
213         GridLayout gl;
214         group.setLayout(gl = new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/));
215         gl.marginHeight = gl.marginWidth = 0;
216         group.setLayoutData(new GridData(GridData.FILL_BOTH));
217         group.setFont(parent.getFont());
218         group.addDisposeListener(new DisposeListener() {
219             public void widgetDisposed(DisposeEvent arg0) {
220                 mImageFactory.dispose();
221             }
222         });
223 
224         int style = SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER;
225         if (displayMode == DisplayMode.SIMPLE_CHECK) {
226             style |= SWT.CHECK;
227         }
228         mTable = new Table(group, style);
229         mTable.setHeaderVisible(true);
230         mTable.setLinesVisible(false);
231         setTableHeightHint(0);
232 
233         Composite buttons = new Composite(group, SWT.NONE);
234         buttons.setLayout(gl = new GridLayout(1, false /*makeColumnsEqualWidth*/));
235         gl.marginHeight = gl.marginWidth = 0;
236         buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
237         buttons.setFont(group.getFont());
238 
239         if (displayMode == DisplayMode.MANAGER) {
240             mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
241             mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
242             mNewButton.setText("New...");
243             mNewButton.setToolTipText("Creates a new AVD.");
244             mNewButton.addSelectionListener(new SelectionAdapter() {
245                 @Override
246                 public void widgetSelected(SelectionEvent arg0) {
247                     onNew();
248                 }
249             });
250 
251             mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
252             mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
253             mDeleteButton.setText("Delete...");
254             mDeleteButton.setToolTipText("Deletes the selected AVD.");
255             mDeleteButton.addSelectionListener(new SelectionAdapter() {
256                 @Override
257                 public void widgetSelected(SelectionEvent arg0) {
258                     onDelete();
259                 }
260             });
261 
262             mRepairButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
263             mRepairButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
264             mRepairButton.setText("Repair...");
265             mRepairButton.setToolTipText("Repairs the selected AVD.");
266             mRepairButton.addSelectionListener(new SelectionAdapter() {
267                 @Override
268                 public void widgetSelected(SelectionEvent arg0) {
269                     onRepair();
270                 }
271             });
272 
273             Label l = new Label(buttons, SWT.SEPARATOR | SWT.HORIZONTAL);
274             l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
275         }
276 
277         mDetailsButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
278         mDetailsButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
279         mDetailsButton.setText("Details...");
280         mDetailsButton.setToolTipText("Diplays details of the selected AVD.");
281         mDetailsButton.addSelectionListener(new SelectionAdapter() {
282             @Override
283             public void widgetSelected(SelectionEvent arg0) {
284                 onDetails();
285             }
286         });
287 
288         mStartButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
289         mStartButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
290         mStartButton.setText("Start...");
291         mStartButton.setToolTipText("Starts the selected AVD.");
292         mStartButton.addSelectionListener(new SelectionAdapter() {
293             @Override
294             public void widgetSelected(SelectionEvent arg0) {
295                 onStart();
296             }
297         });
298 
299         Composite padding = new Composite(buttons, SWT.NONE);
300         padding.setLayoutData(new GridData(GridData.FILL_VERTICAL));
301 
302         mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
303         mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
304         mRefreshButton.setText("Refresh");
305         mRefreshButton.setToolTipText("Reloads the list of AVD.\nUse this if you create AVDs from the command line.");
306         mRefreshButton.addSelectionListener(new SelectionAdapter() {
307             @Override
308             public void widgetSelected(SelectionEvent arg0) {
309                 refresh(true);
310             }
311         });
312 
313         if (displayMode != DisplayMode.MANAGER) {
314             mManagerButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
315             mManagerButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
316             mManagerButton.setText("Manager...");
317             mManagerButton.setToolTipText("Launches the AVD manager.");
318             mManagerButton.addSelectionListener(new SelectionAdapter() {
319                 @Override
320                 public void widgetSelected(SelectionEvent e) {
321                     onManager();
322                 }
323             });
324         } else {
325             Composite legend = new Composite(group, SWT.NONE);
326             legend.setLayout(gl = new GridLayout(2, false /*makeColumnsEqualWidth*/));
327             gl.marginHeight = gl.marginWidth = 0;
328             legend.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false,
329                     NUM_COL, 1));
330             legend.setFont(group.getFont());
331 
332             new Label(legend, SWT.NONE).setImage(mOkImage);
333             new Label(legend, SWT.NONE).setText("A valid Android Virtual Device.");
334             new Label(legend, SWT.NONE).setImage(mBrokenImage);
335             new Label(legend, SWT.NONE).setText(
336                     "An Android Virtual Device that failed to load. Click 'Details' to see the error.");
337         }
338 
339         // create the table columns
340         final TableColumn column0 = new TableColumn(mTable, SWT.NONE);
341         column0.setText("AVD Name");
342         final TableColumn column1 = new TableColumn(mTable, SWT.NONE);
343         column1.setText("Target Name");
344         final TableColumn column2 = new TableColumn(mTable, SWT.NONE);
345         column2.setText("Platform");
346         final TableColumn column3 = new TableColumn(mTable, SWT.NONE);
347         column3.setText("API Level");
348 
349         adjustColumnsWidth(mTable, column0, column1, column2, column3);
350         setupSelectionListener(mTable);
351         fillTable(mTable);
352     }
353 
354     /**
355      * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}.
356      *
357      * @param parent The parent composite where the selector will be added.
358      * @param manager the AVD manager.
359      * @param displayMode The display mode ({@link DisplayMode}).
360      */
AvdSelector(Composite parent, String osSdkPath, AvdManager manager, DisplayMode displayMode)361     public AvdSelector(Composite parent,
362             String osSdkPath,
363             AvdManager manager,
364             DisplayMode displayMode) {
365         this(parent, osSdkPath, manager, (IAvdFilter)null /* filter */, displayMode);
366     }
367 
368     /**
369      * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered
370      * by an {@link IAndroidTarget}.
371      * <p/>Only the {@link AvdInfo} able to run applications developed for the given
372      * {@link IAndroidTarget} will be displayed.
373      *
374      * @param parent The parent composite where the selector will be added.
375      * @param manager the AVD manager.
376      * @param filter Only shows the AVDs matching this target (must not be null).
377      * @param displayMode The display mode ({@link DisplayMode}).
378      */
AvdSelector(Composite parent, String osSdkPath, AvdManager manager, IAndroidTarget filter, DisplayMode displayMode)379     public AvdSelector(Composite parent,
380             String osSdkPath,
381             AvdManager manager,
382             IAndroidTarget filter,
383             DisplayMode displayMode) {
384         this(parent, osSdkPath, manager, new TargetBasedFilter(filter), displayMode);
385     }
386 
387     /**
388      * Sets an optional SettingsController.
389      * @param controller the controller.
390      */
setSettingsController(SettingsController controller)391     public void setSettingsController(SettingsController controller) {
392         mController = controller;
393     }
394     /**
395      * Sets the table grid layout data.
396      *
397      * @param heightHint If > 0, the height hint is set to the requested value.
398      */
setTableHeightHint(int heightHint)399     public void setTableHeightHint(int heightHint) {
400         GridData data = new GridData();
401         if (heightHint > 0) {
402             data.heightHint = heightHint;
403         }
404         data.grabExcessVerticalSpace = true;
405         data.grabExcessHorizontalSpace = true;
406         data.horizontalAlignment = GridData.FILL;
407         data.verticalAlignment = GridData.FILL;
408         mTable.setLayoutData(data);
409     }
410 
411     /**
412      * Refresh the display of Android Virtual Devices.
413      * Tries to keep the selection.
414      * <p/>
415      * This must be called from the UI thread.
416      *
417      * @param reload if true, the AVD manager will reload the AVD from the disk.
418      * @return false if the reloading failed. This is always true if <var>reload</var> is
419      * <code>false</code>.
420      */
refresh(boolean reload)421     public boolean refresh(boolean reload) {
422         if (reload) {
423             try {
424                 mAvdManager.reloadAvds(NullSdkLog.getLogger());
425             } catch (AndroidLocationException e) {
426                 return false;
427             }
428         }
429 
430         AvdInfo selected = getSelected();
431 
432         fillTable(mTable);
433 
434         setSelection(selected);
435 
436         return true;
437     }
438 
439     /**
440      * Sets a new AVD manager
441      * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
442      * @param manager the AVD manager.
443      */
setManager(AvdManager manager)444     public void setManager(AvdManager manager) {
445         mAvdManager = manager;
446     }
447 
448     /**
449      * Sets a new AVD filter.
450      * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
451      * @param filter An IAvdFilter. If non-null, this will filter out the AVD to not display.
452      */
setFilter(IAvdFilter filter)453     public void setFilter(IAvdFilter filter) {
454         mTargetFilter = filter;
455     }
456 
457     /**
458      * Sets a new Android Target-based AVD filter.
459      * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
460      * @param target An IAndroidTarget. If non-null, only AVD whose target are compatible with the
461      * filter target will displayed an available for selection.
462      */
setFilter(IAndroidTarget target)463     public void setFilter(IAndroidTarget target) {
464         if (target != null) {
465             mTargetFilter = new TargetBasedFilter(target);
466         } else {
467             mTargetFilter = null;
468         }
469     }
470 
471     /**
472      * Sets a selection listener. Set it to null to remove it.
473      * The listener will be called <em>after</em> this table processed its selection
474      * events so that the caller can see the updated state.
475      * <p/>
476      * The event's item contains a {@link TableItem}.
477      * The {@link TableItem#getData()} contains an {@link IAndroidTarget}.
478      * <p/>
479      * It is recommended that the caller uses the {@link #getSelected()} method instead.
480      * <p/>
481      * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to
482      * display the details of the selected AVD.<br>
483      * To disable it (when you provide your own double click action), set
484      * {@link SelectionEvent#doit} to false in
485      * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)}
486      *
487      * @param selectionListener The new listener or null to remove it.
488      */
setSelectionListener(SelectionListener selectionListener)489     public void setSelectionListener(SelectionListener selectionListener) {
490         mSelectionListener = selectionListener;
491     }
492 
493     /**
494      * Sets the current target selection.
495      * <p/>
496      * If the selection is actually changed, this will invoke the selection listener
497      * (if any) with a null event.
498      *
499      * @param target the target to be selected. Use null to deselect everything.
500      * @return true if the target could be selected, false otherwise.
501      */
setSelection(AvdInfo target)502     public boolean setSelection(AvdInfo target) {
503         boolean found = false;
504         boolean modified = false;
505 
506         int selIndex = mTable.getSelectionIndex();
507         int index = 0;
508         for (TableItem i : mTable.getItems()) {
509             if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
510                 if ((AvdInfo) i.getData() == target) {
511                     found = true;
512                     if (!i.getChecked()) {
513                         modified = true;
514                         i.setChecked(true);
515                     }
516                 } else if (i.getChecked()) {
517                     modified = true;
518                     i.setChecked(false);
519                 }
520             } else {
521                 if ((AvdInfo) i.getData() == target) {
522                     found = true;
523                     if (index != selIndex) {
524                         mTable.setSelection(index);
525                         modified = true;
526                     }
527                     break;
528                 }
529 
530                 index++;
531             }
532         }
533 
534         if (modified && mSelectionListener != null) {
535             mSelectionListener.widgetSelected(null);
536         }
537 
538         enableActionButtons();
539 
540         return found;
541     }
542 
543     /**
544      * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will
545      * return the {@link AvdInfo} that is checked instead of the list selection.
546      *
547      * @return The currently selected item or null.
548      */
getSelected()549     public AvdInfo getSelected() {
550         if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
551             for (TableItem i : mTable.getItems()) {
552                 if (i.getChecked()) {
553                     return (AvdInfo) i.getData();
554                 }
555             }
556         } else {
557             int selIndex = mTable.getSelectionIndex();
558             if (selIndex >= 0) {
559                 return (AvdInfo) mTable.getItem(selIndex).getData();
560             }
561         }
562 
563         return null;
564     }
565 
566     /**
567      * Enables the receiver if the argument is true, and disables it otherwise.
568      * A disabled control is typically not selectable from the user interface
569      * and draws with an inactive or "grayed" look.
570      *
571      * @param enabled the new enabled state.
572      */
setEnabled(boolean enabled)573     public void setEnabled(boolean enabled) {
574         mIsEnabled = enabled;
575 
576         mTable.setEnabled(mIsEnabled);
577         mRefreshButton.setEnabled(mIsEnabled);
578 
579         if (mNewButton != null) {
580             mNewButton.setEnabled(mIsEnabled);
581         }
582         if (mManagerButton != null) {
583             mManagerButton.setEnabled(mIsEnabled);
584         }
585 
586         enableActionButtons();
587     }
588 
isEnabled()589     public boolean isEnabled() {
590         return mIsEnabled;
591     }
592 
593     /**
594      * Adds a listener to adjust the columns width when the parent is resized.
595      * <p/>
596      * If we need something more fancy, we might want to use this:
597      * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
598      */
adjustColumnsWidth(final Table table, final TableColumn column0, final TableColumn column1, final TableColumn column2, final TableColumn column3)599     private void adjustColumnsWidth(final Table table,
600             final TableColumn column0,
601             final TableColumn column1,
602             final TableColumn column2,
603             final TableColumn column3) {
604         // Add a listener to resize the column to the full width of the table
605         table.addControlListener(new ControlAdapter() {
606             @Override
607             public void controlResized(ControlEvent e) {
608                 Rectangle r = table.getClientArea();
609                 column0.setWidth(r.width * 25 / 100); // 25%
610                 column1.setWidth(r.width * 45 / 100); // 45%
611                 column2.setWidth(r.width * 15 / 100); // 15%
612                 column3.setWidth(r.width * 15 / 100); // 15%
613             }
614         });
615     }
616 
617     /**
618      * Creates a selection listener that will check or uncheck the whole line when
619      * double-clicked (aka "the default selection").
620      */
setupSelectionListener(final Table table)621     private void setupSelectionListener(final Table table) {
622         // Add a selection listener that will check/uncheck items when they are double-clicked
623         table.addSelectionListener(new SelectionListener() {
624 
625             /**
626              * Handles single-click selection on the table.
627              * {@inheritDoc}
628              */
629             public void widgetSelected(SelectionEvent e) {
630                 if (e.item instanceof TableItem) {
631                     TableItem i = (TableItem) e.item;
632                     enforceSingleSelection(i);
633                 }
634 
635                 if (mSelectionListener != null) {
636                     mSelectionListener.widgetSelected(e);
637                 }
638 
639                 enableActionButtons();
640             }
641 
642             /**
643              * Handles double-click selection on the table.
644              * Note that the single-click handler will probably already have been called.
645              *
646              * On double-click, <em>always</em> check the table item.
647              *
648              * {@inheritDoc}
649              */
650             public void widgetDefaultSelected(SelectionEvent e) {
651                 if (e.item instanceof TableItem) {
652                     TableItem i = (TableItem) e.item;
653                     if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
654                         i.setChecked(true);
655                     }
656                     enforceSingleSelection(i);
657 
658                 }
659 
660                 // whether or not we display details. default: true when not in SIMPLE_CHECK mode.
661                 boolean showDetails = mDisplayMode != DisplayMode.SIMPLE_CHECK;
662 
663                 if (mSelectionListener != null) {
664                     mSelectionListener.widgetDefaultSelected(e);
665                     showDetails &= e.doit; // enforce false in SIMPLE_CHECK
666                 }
667 
668                 if (showDetails) {
669                     onDetails();
670                 }
671 
672                 enableActionButtons();
673             }
674 
675             /**
676              * To ensure single selection, uncheck all other items when this one is selected.
677              * This makes the chekboxes act as radio buttons.
678              */
679             private void enforceSingleSelection(TableItem item) {
680                 if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
681                     if (item.getChecked()) {
682                         Table parentTable = item.getParent();
683                         for (TableItem i2 : parentTable.getItems()) {
684                             if (i2 != item && i2.getChecked()) {
685                                 i2.setChecked(false);
686                             }
687                         }
688                     }
689                 } else {
690                     // pass
691                 }
692             }
693         });
694     }
695 
696     /**
697      * Fills the table with all AVD.
698      * The table columns are:
699      * <ul>
700      * <li>column 0: sdk name
701      * <li>column 1: sdk vendor
702      * <li>column 2: sdk api name
703      * <li>column 3: sdk version
704      * </ul>
705      */
fillTable(final Table table)706     private void fillTable(final Table table) {
707         table.removeAll();
708 
709         // get the AVDs
710         AvdInfo avds[] = null;
711         if (mAvdManager != null) {
712             if (mDisplayMode == DisplayMode.MANAGER) {
713                 avds = mAvdManager.getAllAvds();
714             } else {
715                 avds = mAvdManager.getValidAvds();
716             }
717         }
718 
719         if (avds != null && avds.length > 0) {
720             table.setEnabled(true);
721 
722             if (mTargetFilter != null) {
723                 mTargetFilter.prepare();
724             }
725 
726             for (AvdInfo avd : avds) {
727                 if (mTargetFilter == null || mTargetFilter.accept(avd)) {
728                     TableItem item = new TableItem(table, SWT.NONE);
729                     item.setData(avd);
730                     item.setText(0, avd.getName());
731                     if (mDisplayMode == DisplayMode.MANAGER) {
732                         item.setImage(0, avd.getStatus() == AvdStatus.OK ? mOkImage : mBrokenImage);
733                     }
734                     IAndroidTarget target = avd.getTarget();
735                     if (target != null) {
736                         item.setText(1, target.getFullName());
737                         item.setText(2, target.getVersionName());
738                         item.setText(3, target.getVersion().getApiString());
739                     } else {
740                         item.setText(1, "?");
741                         item.setText(2, "?");
742                         item.setText(3, "?");
743                     }
744                 }
745             }
746 
747             if (mTargetFilter != null) {
748                 mTargetFilter.cleanup();
749             }
750         }
751 
752         if (table.getItemCount() == 0) {
753             table.setEnabled(false);
754             TableItem item = new TableItem(table, SWT.NONE);
755             item.setData(null);
756             item.setText(0, "--");
757             item.setText(1, "No AVD available");
758             item.setText(2, "--");
759             item.setText(3, "--");
760         }
761     }
762 
763     /**
764      * Returns the currently selected AVD in the table.
765      * <p/>
766      * Unlike {@link #getSelected()} this will always return the item being selected
767      * in the list, ignoring the check boxes state in {@link DisplayMode#SIMPLE_CHECK} mode.
768      */
getTableSelection()769     private AvdInfo getTableSelection() {
770         int selIndex = mTable.getSelectionIndex();
771         if (selIndex >= 0) {
772             return (AvdInfo) mTable.getItem(selIndex).getData();
773         }
774 
775         return null;
776     }
777 
778     /**
779      * Updates the enable state of the Details, Start, Delete and Update buttons.
780      */
enableActionButtons()781     private void enableActionButtons() {
782         if (mIsEnabled == false) {
783             mDetailsButton.setEnabled(false);
784             mStartButton.setEnabled(false);
785 
786             if (mDeleteButton != null) {
787                 mDeleteButton.setEnabled(false);
788             }
789             if (mRepairButton != null) {
790                 mRepairButton.setEnabled(false);
791             }
792         } else {
793             AvdInfo selection = getTableSelection();
794             boolean hasSelection = selection != null;
795 
796             mDetailsButton.setEnabled(hasSelection);
797             mStartButton.setEnabled(mOsSdkPath != null &&
798                     hasSelection &&
799                     selection.getStatus() == AvdStatus.OK);
800 
801             if (mDeleteButton != null) {
802                 mDeleteButton.setEnabled(hasSelection);
803             }
804             if (mRepairButton != null) {
805                 mRepairButton.setEnabled(hasSelection &&
806                         selection.getStatus() == AvdStatus.ERROR_IMAGE_DIR);
807             }
808         }
809     }
810 
onNew()811     private void onNew() {
812         AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), mAvdManager,
813                 mImageFactory);
814         if (dlg.open() == Window.OK) {
815             refresh(false /*reload*/);
816         }
817     }
818 
onDetails()819     private void onDetails() {
820         final AvdInfo avdInfo = getTableSelection();
821 
822         AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdInfo);
823         dlg.open();
824     }
825 
onDelete()826     private void onDelete() {
827         final AvdInfo avdInfo = getTableSelection();
828 
829         // get the current Display
830         final Display display = mTable.getDisplay();
831 
832         // Confirm you want to delete this AVD
833         final boolean[] result = new boolean[1];
834         display.syncExec(new Runnable() {
835             public void run() {
836                 Shell shell = display.getActiveShell();
837                 result[0] = MessageDialog.openQuestion(shell,
838                         "Delete Android Virtual Device",
839                         String.format(
840                                 "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.",
841                                 avdInfo.getName()));
842             }
843         });
844 
845         if (result[0] == false) {
846             return;
847         }
848 
849         // log for this action.
850         SdkLog log = new SdkLog(
851                 String.format("Result of deleting AVD '%s':", avdInfo.getName()),
852                 display);
853 
854         // delete the AVD
855         boolean success = mAvdManager.deleteAvd(avdInfo, log);
856 
857         // display the result
858         log.displayResult(success);
859 
860         if (success) {
861             refresh(false /*reload*/);
862         }
863     }
864 
865     /**
866      * Repairs the selected AVD.
867      * <p/>
868      * For now this only supports fixing the wrong value in image.sysdir.*
869      */
onRepair()870     private void onRepair() {
871         final AvdInfo avdInfo = getTableSelection();
872 
873         // get the current Display
874         final Display display = mTable.getDisplay();
875 
876         // log for this action.
877         SdkLog log = new SdkLog(
878                 String.format("Result of updating AVD '%s':", avdInfo.getName()),
879                 display);
880 
881         // delete the AVD
882         try {
883             mAvdManager.updateAvd(avdInfo, log);
884 
885             // display the result
886             log.displayResult(true /* success */);
887 
888             refresh(false /*reload*/);
889         } catch (IOException e) {
890             log.error(e, null);
891             log.displayResult(false /* success */);
892         }
893     }
894 
onManager()895     private void onManager() {
896         UpdaterWindow window = new UpdaterWindow(
897                 mTable.getShell(),
898                 null /*sdk log*/,
899                 mAvdManager.getSdkManager().getLocation(),
900                 false /*userCanChangeSdkRoot*/);
901         window.open();
902         refresh(true /*reload*/); // UpdaterWindow uses its own AVD manager so this one must reload.
903     }
904 
onStart()905     private void onStart() {
906         AvdInfo avdInfo = getTableSelection();
907 
908         if (avdInfo == null || mOsSdkPath == null) {
909             return;
910         }
911 
912         AvdStartDialog dialog = new AvdStartDialog(mTable.getShell(), avdInfo, mOsSdkPath,
913                 mController);
914         if (dialog.open() == Window.OK) {
915             String path = mOsSdkPath +
916                 File.separator +
917                 SdkConstants.OS_SDK_TOOLS_FOLDER +
918                 SdkConstants.FN_EMULATOR;
919 
920             final String avdName = avdInfo.getName();
921 
922             // build the command line based on the available parameters.
923             ArrayList<String> list = new ArrayList<String>();
924             list.add(path);
925             list.add("-avd");                             //$NON-NLS-1$
926             list.add(avdName);
927             if (dialog.getWipeData()) {
928                 list.add("-wipe-data");                   //$NON-NLS-1$
929             }
930             float scale = dialog.getScale();
931             if (scale != 0.f) {
932                 // do the rounding ourselves. This is because %.1f will write .4899 as .4
933                 scale = Math.round(scale * 100);
934                 scale /=  100.f;
935                 list.add("-scale");                       //$NON-NLS-1$
936                 list.add(String.format("%.2f", scale));   //$NON-NLS-1$
937             }
938 
939             // convert the list into an array for the call to exec.
940             final String[] command = list.toArray(new String[list.size()]);
941 
942             // launch the emulator
943             new ProgressTask(mTable.getShell(),
944                     "Starting Android Emulator",
945                     new ITask() {
946                         public void run(ITaskMonitor monitor) {
947                             try {
948                                 monitor.setDescription("Starting emulator for AVD '%1$s'",
949                                         avdName);
950                                 int n = 10;
951                                 monitor.setProgressMax(n);
952                                 Process process = Runtime.getRuntime().exec(command);
953                                 grabEmulatorOutput(process, monitor);
954 
955                                 // This small wait prevents the dialog from closing too fast:
956                                 // When it works, the emulator returns immediately, even if
957                                 // no UI is shown yet. And when it fails (because the AVD is
958                                 // locked/running)
959                                 // if we don't have a wait we don't capture the error for
960                                 // some reason.
961                                 for (int i = 0; i < n; i++) {
962                                     try {
963                                         Thread.sleep(100);
964                                         monitor.incProgress(1);
965                                     } catch (InterruptedException e) {
966                                         // ignore
967                                     }
968                                 }
969                             } catch (IOException e) {
970                                 monitor.setResult("Failed to start emulator: %1$s",
971                                         e.getMessage());
972                             }
973                         }
974             });
975         }
976     }
977 
978     /**
979      * Get the stderr/stdout outputs of a process and return when the process is done.
980      * Both <b>must</b> be read or the process will block on windows.
981      * @param process The process to get the output from.
982      * @param monitor An {@link ISdkLog} to capture errors.
983      */
grabEmulatorOutput(final Process process, final ITaskMonitor monitor)984     private void grabEmulatorOutput(final Process process, final ITaskMonitor monitor) {
985         // read the lines as they come. if null is returned, it's because the process finished
986         new Thread("emu-stderr") { //$NON-NLS-1$
987             @Override
988             public void run() {
989                 // create a buffer to read the stderr output
990                 InputStreamReader is = new InputStreamReader(process.getErrorStream());
991                 BufferedReader errReader = new BufferedReader(is);
992 
993                 try {
994                     while (true) {
995                         String line = errReader.readLine();
996                         if (line != null) {
997                             monitor.setResult("%1$s", line);    //$NON-NLS-1$
998                         } else {
999                             break;
1000                         }
1001                     }
1002                 } catch (IOException e) {
1003                     // do nothing.
1004                 }
1005             }
1006         }.start();
1007 
1008         new Thread("emu-stdout") { //$NON-NLS-1$
1009             @Override
1010             public void run() {
1011                 InputStreamReader is = new InputStreamReader(process.getInputStream());
1012                 BufferedReader outReader = new BufferedReader(is);
1013 
1014                 try {
1015                     while (true) {
1016                         String line = outReader.readLine();
1017                         if (line != null) {
1018                             monitor.setResult("%1$s", line);    //$NON-NLS-1$
1019                         } else {
1020                             break;
1021                         }
1022                     }
1023                 } catch (IOException e) {
1024                     // do nothing.
1025                 }
1026             }
1027         }.start();
1028     }
1029 
1030     /**
1031      * Collects all log from the AVD action and displays it in a dialog.
1032      */
1033     static class SdkLog implements ISdkLog {
1034 
1035         final ArrayList<String> logMessages = new ArrayList<String>();
1036         private final String mMessage;
1037         private final Display mDisplay;
1038 
SdkLog(String message, Display display)1039         public SdkLog(String message, Display display) {
1040             mMessage = message;
1041             mDisplay = display;
1042         }
1043 
error(Throwable throwable, String errorFormat, Object... arg)1044         public void error(Throwable throwable, String errorFormat, Object... arg) {
1045             if (errorFormat != null) {
1046                 logMessages.add(String.format("Error: " + errorFormat, arg));
1047             }
1048 
1049             if (throwable != null) {
1050                 logMessages.add(throwable.getMessage());
1051             }
1052         }
1053 
warning(String warningFormat, Object... arg)1054         public void warning(String warningFormat, Object... arg) {
1055             logMessages.add(String.format("Warning: " + warningFormat, arg));
1056         }
1057 
printf(String msgFormat, Object... arg)1058         public void printf(String msgFormat, Object... arg) {
1059             logMessages.add(String.format(msgFormat, arg));
1060         }
1061 
1062         /**
1063          * Displays the log if anything was captured.
1064          */
displayResult(final boolean success)1065         public void displayResult(final boolean success) {
1066             if (logMessages.size() > 0) {
1067                 final StringBuilder sb = new StringBuilder(mMessage + "\n\n");
1068                 for (String msg : logMessages) {
1069                     sb.append(msg);
1070                 }
1071 
1072                 // display the message
1073                 // dialog box only run in ui thread..
1074                 mDisplay.asyncExec(new Runnable() {
1075                     public void run() {
1076                         Shell shell = mDisplay.getActiveShell();
1077                         if (success) {
1078                             MessageDialog.openInformation(shell, "Android Virtual Devices Manager",
1079                                     sb.toString());
1080                         } else {
1081                             MessageDialog.openError(shell, "Android Virtual Devices Manager",
1082                                     sb.toString());
1083 
1084                         }
1085                     }
1086                 });
1087             }
1088         }
1089     }
1090 }
1091