1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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.ide.eclipse.adt.internal.launch; 18 19 import com.android.ddmlib.AndroidDebugBridge; 20 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; 21 import com.android.ddmlib.Client; 22 import com.android.ddmlib.IDevice; 23 import com.android.ddmlib.IDevice.DeviceState; 24 import com.android.ddmuilib.ImageLoader; 25 import com.android.ddmuilib.TableHelper; 26 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 27 import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog; 28 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 29 import com.android.ide.eclipse.ddms.DdmsPlugin; 30 import com.android.sdklib.AndroidVersion; 31 import com.android.sdklib.IAndroidTarget; 32 import com.android.sdklib.internal.avd.AvdInfo; 33 import com.android.sdkuilib.internal.widgets.AvdSelector; 34 import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode; 35 import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; 36 37 import org.eclipse.jface.dialogs.Dialog; 38 import org.eclipse.jface.dialogs.IDialogConstants; 39 import org.eclipse.jface.viewers.ILabelProviderListener; 40 import org.eclipse.jface.viewers.IStructuredContentProvider; 41 import org.eclipse.jface.viewers.ITableLabelProvider; 42 import org.eclipse.jface.viewers.StructuredSelection; 43 import org.eclipse.jface.viewers.TableViewer; 44 import org.eclipse.jface.viewers.Viewer; 45 import org.eclipse.swt.SWT; 46 import org.eclipse.swt.SWTException; 47 import org.eclipse.swt.events.SelectionAdapter; 48 import org.eclipse.swt.events.SelectionEvent; 49 import org.eclipse.swt.graphics.Image; 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.Control; 55 import org.eclipse.swt.widgets.Display; 56 import org.eclipse.swt.widgets.Label; 57 import org.eclipse.swt.widgets.Shell; 58 import org.eclipse.swt.widgets.Table; 59 60 /** 61 * A dialog that lets the user choose a device to deploy an application. 62 * The user can either choose an exiting running device (including running emulators) 63 * or start a new emulator using an Android Virtual Device configuration that matches 64 * the current project. 65 */ 66 public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { 67 68 private final static int ICON_WIDTH = 16; 69 70 private Table mDeviceTable; 71 private TableViewer mViewer; 72 private AvdSelector mPreferredAvdSelector; 73 74 private Image mDeviceImage; 75 private Image mEmulatorImage; 76 private Image mMatchImage; 77 private Image mNoMatchImage; 78 private Image mWarningImage; 79 80 private final DeviceChooserResponse mResponse; 81 private final String mPackageName; 82 private final IAndroidTarget mProjectTarget; 83 private final Sdk mSdk; 84 85 private Button mDeviceRadioButton; 86 87 private boolean mDisableAvdSelectionChange = false; 88 89 /** 90 * Basic Content Provider for a table full of {@link IDevice} objects. The input is 91 * a {@link AndroidDebugBridge}. 92 */ 93 private static class ContentProvider implements IStructuredContentProvider { 94 public Object[] getElements(Object inputElement) { 95 if (inputElement instanceof AndroidDebugBridge) { 96 return ((AndroidDebugBridge)inputElement).getDevices(); 97 } 98 99 return new Object[0]; 100 } 101 102 public void dispose() { 103 // pass 104 } 105 106 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 107 // pass 108 } 109 } 110 111 112 /** 113 * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. 114 * It provides labels and images for {@link IDevice} objects. 115 */ 116 private class LabelProvider implements ITableLabelProvider { 117 118 public Image getColumnImage(Object element, int columnIndex) { 119 if (element instanceof IDevice) { 120 IDevice device = (IDevice)element; 121 switch (columnIndex) { 122 case 0: 123 return device.isEmulator() ? mEmulatorImage : mDeviceImage; 124 125 case 2: 126 // check for compatibility. 127 if (device.isEmulator() == false) { // physical device 128 // get the version of the device 129 AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); 130 if (deviceVersion == null) { 131 return mWarningImage; 132 } else { 133 if (deviceVersion.canRun(mProjectTarget.getVersion()) == false) { 134 return mNoMatchImage; 135 } 136 137 // if the project is compiling against an add-on, 138 // the optional API may be missing from the device. 139 return mProjectTarget.isPlatform() ? 140 mMatchImage : mWarningImage; 141 } 142 } else { 143 // get the AvdInfo 144 AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), 145 true /*validAvdOnly*/); 146 if (info == null) { 147 return mWarningImage; 148 } 149 return mProjectTarget.canRunOn(info.getTarget()) ? 150 mMatchImage : mNoMatchImage; 151 } 152 } 153 } 154 155 return null; 156 } 157 158 public String getColumnText(Object element, int columnIndex) { 159 if (element instanceof IDevice) { 160 IDevice device = (IDevice)element; 161 switch (columnIndex) { 162 case 0: 163 return device.getSerialNumber(); 164 case 1: 165 if (device.isEmulator()) { 166 return device.getAvdName(); 167 } else { 168 return "N/A"; // devices don't have AVD names. 169 } 170 case 2: 171 if (device.isEmulator()) { 172 AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), 173 true /*validAvdOnly*/); 174 if (info == null) { 175 return "?"; 176 } 177 return info.getTarget().getFullName(); 178 } else { 179 String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); 180 if (deviceBuild == null) { 181 return "unknown"; 182 } 183 return deviceBuild; 184 } 185 case 3: 186 String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); 187 if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ 188 return "Yes"; 189 } else { 190 return ""; 191 } 192 case 4: 193 return getStateString(device); 194 } 195 } 196 197 return null; 198 } 199 200 public void addListener(ILabelProviderListener listener) { 201 // pass 202 } 203 204 public void dispose() { 205 // pass 206 } 207 208 public boolean isLabelProperty(Object element, String property) { 209 // pass 210 return false; 211 } 212 213 public void removeListener(ILabelProviderListener listener) { 214 // pass 215 } 216 } 217 218 public static class DeviceChooserResponse { 219 private AvdInfo mAvdToLaunch; 220 private IDevice mDeviceToUse; 221 222 public void setDeviceToUse(IDevice d) { 223 mDeviceToUse = d; 224 mAvdToLaunch = null; 225 } 226 227 public void setAvdToLaunch(AvdInfo avd) { 228 mAvdToLaunch = avd; 229 mDeviceToUse = null; 230 } 231 232 public IDevice getDeviceToUse() { 233 return mDeviceToUse; 234 } 235 236 public AvdInfo getAvdToLaunch() { 237 return mAvdToLaunch; 238 } 239 } 240 241 public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName, 242 IAndroidTarget projectTarget) { 243 super(parent); 244 mResponse = response; 245 mPackageName = packageName; 246 mProjectTarget = projectTarget; 247 mSdk = Sdk.getCurrent(); 248 249 AndroidDebugBridge.addDeviceChangeListener(this); 250 loadImages(); 251 } 252 253 private void cleanup() { 254 // done listening. 255 AndroidDebugBridge.removeDeviceChangeListener(this); 256 } 257 258 @Override 259 protected void okPressed() { 260 cleanup(); 261 super.okPressed(); 262 } 263 264 @Override 265 protected void cancelPressed() { 266 cleanup(); 267 super.cancelPressed(); 268 } 269 270 @Override 271 protected Control createContents(Composite parent) { 272 Control content = super.createContents(parent); 273 274 // this must be called after createContents() has happened so that the 275 // ok button has been created (it's created after the call to createDialogArea) 276 updateDefaultSelection(); 277 278 return content; 279 } 280 281 282 @Override 283 protected Control createDialogArea(Composite parent) { 284 // set dialog title 285 getShell().setText("Android Device Chooser"); 286 287 Composite top = new Composite(parent, SWT.NONE); 288 top.setLayout(new GridLayout(1, true)); 289 290 Label label = new Label(top, SWT.NONE); 291 label.setText(String.format("Select a device compatible with target %s.", 292 mProjectTarget.getFullName())); 293 294 mDeviceRadioButton = new Button(top, SWT.RADIO); 295 mDeviceRadioButton.setText("Choose a running Android device"); 296 mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { 297 @Override 298 public void widgetSelected(SelectionEvent e) { 299 boolean deviceMode = mDeviceRadioButton.getSelection(); 300 301 mDeviceTable.setEnabled(deviceMode); 302 mPreferredAvdSelector.setEnabled(!deviceMode); 303 304 if (deviceMode) { 305 handleDeviceSelection(); 306 } else { 307 mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); 308 } 309 310 enableOkButton(); 311 } 312 }); 313 mDeviceRadioButton.setSelection(true); 314 315 316 // offset the selector from the radio button 317 Composite offsetComp = new Composite(top, SWT.NONE); 318 offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 319 GridLayout layout = new GridLayout(1, false); 320 layout.marginRight = layout.marginHeight = 0; 321 layout.marginLeft = 30; 322 offsetComp.setLayout(layout); 323 324 mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); 325 GridData gd; 326 mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); 327 gd.heightHint = 100; 328 329 mDeviceTable.setHeaderVisible(true); 330 mDeviceTable.setLinesVisible(true); 331 332 TableHelper.createTableColumn(mDeviceTable, "Serial Number", 333 SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ 334 null /* prefs name */, null /* prefs store */); 335 336 TableHelper.createTableColumn(mDeviceTable, "AVD Name", 337 SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ 338 null /* prefs name */, null /* prefs store */); 339 340 TableHelper.createTableColumn(mDeviceTable, "Target", 341 SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ 342 null /* prefs name */, null /* prefs store */); 343 344 TableHelper.createTableColumn(mDeviceTable, "Debug", 345 SWT.LEFT, "Debug", //$NON-NLS-1$ 346 null /* prefs name */, null /* prefs store */); 347 348 TableHelper.createTableColumn(mDeviceTable, "State", 349 SWT.LEFT, "bootloader", //$NON-NLS-1$ 350 null /* prefs name */, null /* prefs store */); 351 352 // create the viewer for it 353 mViewer = new TableViewer(mDeviceTable); 354 mViewer.setContentProvider(new ContentProvider()); 355 mViewer.setLabelProvider(new LabelProvider()); 356 mViewer.setInput(AndroidDebugBridge.getBridge()); 357 358 mDeviceTable.addSelectionListener(new SelectionAdapter() { 359 /** 360 * Handles single-click selection on the device selector. 361 * {@inheritDoc} 362 */ 363 @Override 364 public void widgetSelected(SelectionEvent e) { 365 handleDeviceSelection(); 366 } 367 368 /** 369 * Handles double-click selection on the device selector. 370 * Note that the single-click handler will probably already have been called. 371 * {@inheritDoc} 372 */ 373 @Override 374 public void widgetDefaultSelected(SelectionEvent e) { 375 handleDeviceSelection(); 376 if (isOkButtonEnabled()) { 377 okPressed(); 378 } 379 } 380 }); 381 382 Button radio2 = new Button(top, SWT.RADIO); 383 radio2.setText("Launch a new Android Virtual Device"); 384 385 // offset the selector from the radio button 386 offsetComp = new Composite(top, SWT.NONE); 387 offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 388 layout = new GridLayout(1, false); 389 layout.marginRight = layout.marginHeight = 0; 390 layout.marginLeft = 30; 391 offsetComp.setLayout(layout); 392 393 mPreferredAvdSelector = new AvdSelector(offsetComp, 394 mSdk.getSdkLocation(), 395 mSdk.getAvdManager(), 396 new NonRunningAvdFilter(), 397 DisplayMode.SIMPLE_SELECTION, 398 new AdtConsoleSdkLog()); 399 mPreferredAvdSelector.setTableHeightHint(100); 400 mPreferredAvdSelector.setEnabled(false); 401 mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { 402 /** 403 * Handles single-click selection on the AVD selector. 404 * {@inheritDoc} 405 */ 406 @Override 407 public void widgetSelected(SelectionEvent e) { 408 if (mDisableAvdSelectionChange == false) { 409 mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); 410 enableOkButton(); 411 } 412 } 413 414 /** 415 * Handles double-click selection on the AVD selector. 416 * 417 * Note that the single-click handler will probably already have been called 418 * but the selected item can have changed in between. 419 * 420 * {@inheritDoc} 421 */ 422 @Override 423 public void widgetDefaultSelected(SelectionEvent e) { 424 widgetSelected(e); 425 if (isOkButtonEnabled()) { 426 okPressed(); 427 } 428 } 429 }); 430 431 432 return top; 433 } 434 435 private void loadImages() { 436 ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader(); 437 Display display = DdmsPlugin.getDisplay(); 438 IconFactory factory = IconFactory.getInstance(); 439 440 if (mDeviceImage == null) { 441 mDeviceImage = ddmUiLibLoader.loadImage(display, 442 "device.png", //$NON-NLS-1$ 443 ICON_WIDTH, ICON_WIDTH, 444 display.getSystemColor(SWT.COLOR_RED)); 445 } 446 if (mEmulatorImage == null) { 447 mEmulatorImage = ddmUiLibLoader.loadImage(display, 448 "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ 449 display.getSystemColor(SWT.COLOR_BLUE)); 450 } 451 452 if (mMatchImage == null) { 453 mMatchImage = factory.getIcon("match", //$NON-NLS-1$ 454 IconFactory.COLOR_GREEN, 455 IconFactory.SHAPE_DEFAULT); 456 } 457 458 if (mNoMatchImage == null) { 459 mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$ 460 IconFactory.COLOR_RED, 461 IconFactory.SHAPE_DEFAULT); 462 } 463 464 if (mWarningImage == null) { 465 mWarningImage = factory.getIcon("warning", //$NON-NLS-1$ 466 SWT.COLOR_YELLOW, 467 IconFactory.SHAPE_DEFAULT); 468 } 469 470 } 471 472 /** 473 * Returns a display string representing the state of the device. 474 * @param d the device 475 */ 476 private static String getStateString(IDevice d) { 477 DeviceState deviceState = d.getState(); 478 if (deviceState == DeviceState.ONLINE) { 479 return "Online"; 480 } else if (deviceState == DeviceState.OFFLINE) { 481 return "Offline"; 482 } else if (deviceState == DeviceState.BOOTLOADER) { 483 return "Bootloader"; 484 } 485 486 return "??"; 487 } 488 489 /** 490 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 491 * <p/> 492 * This is sent from a non UI thread. 493 * @param device the new device. 494 * 495 * @see IDeviceChangeListener#deviceConnected(IDevice) 496 */ 497 public void deviceConnected(IDevice device) { 498 final DeviceChooserDialog dialog = this; 499 exec(new Runnable() { 500 public void run() { 501 if (mDeviceTable.isDisposed() == false) { 502 // refresh all 503 mViewer.refresh(); 504 505 // update the selection 506 updateDefaultSelection(); 507 508 // update the display of AvdInfo (since it's filtered to only display 509 // non running AVD.) 510 refillAvdList(false /*reloadAvds*/); 511 } else { 512 // table is disposed, we need to do something. 513 // lets remove ourselves from the listener. 514 AndroidDebugBridge.removeDeviceChangeListener(dialog); 515 } 516 517 } 518 }); 519 } 520 521 /** 522 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 523 * <p/> 524 * This is sent from a non UI thread. 525 * @param device the new device. 526 * 527 * @see IDeviceChangeListener#deviceDisconnected(IDevice) 528 */ 529 public void deviceDisconnected(IDevice device) { 530 deviceConnected(device); 531 } 532 533 /** 534 * Sent when a device data changed, or when clients are started/terminated on the device. 535 * <p/> 536 * This is sent from a non UI thread. 537 * @param device the device that was updated. 538 * @param changeMask the mask indicating what changed. 539 * 540 * @see IDeviceChangeListener#deviceChanged(IDevice, int) 541 */ 542 public void deviceChanged(final IDevice device, int changeMask) { 543 if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) { 544 final DeviceChooserDialog dialog = this; 545 exec(new Runnable() { 546 public void run() { 547 if (mDeviceTable.isDisposed() == false) { 548 // refresh the device 549 mViewer.refresh(device); 550 551 // update the defaultSelection. 552 updateDefaultSelection(); 553 554 // update the display of AvdInfo (since it's filtered to only display 555 // non running AVD). This is done on deviceChanged because the avd name 556 // of a (emulator) device may be updated as the emulator boots. 557 558 refillAvdList(false /*reloadAvds*/); 559 560 // if the changed device is the current selection, 561 // we update the OK button based on its state. 562 if (device == mResponse.getDeviceToUse()) { 563 enableOkButton(); 564 } 565 566 } else { 567 // table is disposed, we need to do something. 568 // lets remove ourselves from the listener. 569 AndroidDebugBridge.removeDeviceChangeListener(dialog); 570 } 571 } 572 }); 573 } 574 } 575 576 /** 577 * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). 578 */ 579 private boolean isDeviceMode() { 580 return mDeviceRadioButton.getSelection(); 581 } 582 583 /** 584 * Enables or disables the OK button of the dialog based on various selections in the dialog. 585 */ 586 private void enableOkButton() { 587 Button okButton = getButton(IDialogConstants.OK_ID); 588 589 if (isDeviceMode()) { 590 okButton.setEnabled(mResponse.getDeviceToUse() != null && 591 mResponse.getDeviceToUse().isOnline()); 592 } else { 593 okButton.setEnabled(mResponse.getAvdToLaunch() != null); 594 } 595 } 596 597 /** 598 * Returns true if the ok button is enabled. 599 */ 600 private boolean isOkButtonEnabled() { 601 Button okButton = getButton(IDialogConstants.OK_ID); 602 return okButton.isEnabled(); 603 } 604 605 /** 606 * Executes the {@link Runnable} in the UI thread. 607 * @param runnable the runnable to execute. 608 */ 609 private void exec(Runnable runnable) { 610 try { 611 Display display = mDeviceTable.getDisplay(); 612 display.asyncExec(runnable); 613 } catch (SWTException e) { 614 // tree is disposed, we need to do something. lets remove ourselves from the listener. 615 AndroidDebugBridge.removeDeviceChangeListener(this); 616 } 617 } 618 619 private void handleDeviceSelection() { 620 int count = mDeviceTable.getSelectionCount(); 621 if (count != 1) { 622 handleSelection(null); 623 } else { 624 int index = mDeviceTable.getSelectionIndex(); 625 Object data = mViewer.getElementAt(index); 626 if (data instanceof IDevice) { 627 handleSelection((IDevice)data); 628 } else { 629 handleSelection(null); 630 } 631 } 632 } 633 634 private void handleSelection(IDevice device) { 635 mResponse.setDeviceToUse(device); 636 enableOkButton(); 637 } 638 639 /** 640 * Look for a default device to select. This is done by looking for the running 641 * clients on each device and finding one similar to the one being launched. 642 * <p/> 643 * This is done every time the device list changed unless there is a already selection. 644 */ 645 private void updateDefaultSelection() { 646 if (mDeviceTable.getSelectionCount() == 0) { 647 AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); 648 649 IDevice[] devices = bridge.getDevices(); 650 651 for (IDevice device : devices) { 652 Client[] clients = device.getClients(); 653 654 for (Client client : clients) { 655 656 if (mPackageName.equals(client.getClientData().getClientDescription())) { 657 // found a match! Select it. 658 mViewer.setSelection(new StructuredSelection(device)); 659 handleSelection(device); 660 661 // and we're done. 662 return; 663 } 664 } 665 } 666 } 667 668 handleDeviceSelection(); 669 } 670 671 private final class NonRunningAvdFilter implements IAvdFilter { 672 673 private IDevice[] mDevices; 674 675 public void prepare() { 676 mDevices = AndroidDebugBridge.getBridge().getDevices(); 677 } 678 679 public boolean accept(AvdInfo avd) { 680 if (mDevices != null) { 681 for (IDevice d : mDevices) { 682 if (mProjectTarget.canRunOn(avd.getTarget()) == false || 683 avd.getName().equals(d.getAvdName())) { 684 return false; 685 } 686 } 687 } 688 689 return true; 690 } 691 692 public void cleanup() { 693 mDevices = null; 694 } 695 } 696 697 /** 698 * Refills the AVD list keeping the current selection. 699 */ 700 private void refillAvdList(boolean reloadAvds) { 701 // save the current selection 702 AvdInfo selected = mPreferredAvdSelector.getSelected(); 703 704 // disable selection change. 705 mDisableAvdSelectionChange = true; 706 707 // refresh the list 708 mPreferredAvdSelector.refresh(false); 709 710 // attempt to reselect the proper avd if needed 711 if (selected != null) { 712 if (mPreferredAvdSelector.setSelection(selected) == false) { 713 // looks like the selection is lost. this can happen if an emulator 714 // running the AVD that was selected was launched from outside of Eclipse). 715 mResponse.setAvdToLaunch(null); 716 enableOkButton(); 717 } 718 } 719 720 // enable the selection change 721 mDisableAvdSelectionChange = false; 722 } 723 } 724 725