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