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