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