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 */ prepare()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 */ accept(AvdInfo avd)155 boolean accept(AvdInfo avd); 156 157 /** 158 * Called after {@link #accept(AvdInfo)} has been called on all the AVDs. 159 */ cleanup()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 TargetBasedFilter(IAndroidTarget target)170 TargetBasedFilter(IAndroidTarget target) { 171 mTarget = target; 172 } 173 174 @Override prepare()175 public void prepare() { 176 // nothing to prepare 177 } 178 179 @Override accept(AvdInfo avd)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 cleanup()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 */ AvdSelector(Composite parent, String osSdkPath, AvdManager manager, IAvdFilter filter, DisplayMode displayMode, ISdkLog sdkLog)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 */ AvdSelector(Composite parent, String osSdkPath, AvdManager manager, DisplayMode displayMode, ISdkLog sdkLog)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 */ AvdSelector(Composite parent, String osSdkPath, AvdManager manager, IAndroidTarget filter, DisplayMode displayMode, ISdkLog sdkLog)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 */ setSettingsController(SettingsController controller)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 */ setTableHeightHint(int heightHint)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 */ refresh(boolean reload)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 */ setManager(AvdManager manager)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 */ setFilter(IAvdFilter filter)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 */ setFilter(IAndroidTarget target)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 */ setSelectionListener(SelectionListener selectionListener)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 */ setSelection(AvdInfo target)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 */ getSelected()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 */ setEnabled(boolean enabled)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 isEnabled()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 */ adjustColumnsWidth(final Table table, final TableColumn column0, final TableColumn column1, final TableColumn column2, final TableColumn column3, final TableColumn column4)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 */ setupSelectionListener(final Table table)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 */ fillTable(final Table table)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 */ getTableSelection()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") enableActionButtons()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 onNew()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 onEdit()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 onDetails()899 private void onDetails() { 900 AvdInfo avdInfo = getTableSelection(); 901 902 AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdInfo); 903 dlg.open(); 904 } 905 onDelete()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 */ onRepair()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 onAvdManager()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 onStart()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 isAvdRepairable(AvdStatus avdStatus)1184 private boolean isAvdRepairable(AvdStatus avdStatus) { 1185 return avdStatus == AvdStatus.ERROR_IMAGE_DIR; 1186 } 1187 } 1188