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