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