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