blob: 995ccdf46f0c2eaf38093813cff335f78c547e65 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.launch;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Client;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceState;
import com.android.ddmuilib.ImageLoader;
import com.android.ddmuilib.TableHelper;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.ddms.DdmsPlugin;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdkuilib.internal.widgets.AvdSelector;
import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode;
import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import java.util.ArrayList;
import java.util.List;
/**
* A dialog that lets the user choose a device to deploy an application.
* The user can either choose an exiting running device (including running emulators)
* or start a new emulator using an Android Virtual Device configuration that matches
* the current project.
*/
public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener {
private final static int ICON_WIDTH = 16;
private Table mDeviceTable;
private TableViewer mViewer;
private AvdSelector mPreferredAvdSelector;
private Image mDeviceImage;
private Image mEmulatorImage;
private Image mMatchImage;
private Image mNoMatchImage;
private Image mWarningImage;
private final DeviceChooserResponse mResponse;
private final String mPackageName;
private final IAndroidTarget mProjectTarget;
private final AndroidVersion mMinApiVersion;
private final Sdk mSdk;
private Button mDeviceRadioButton;
private Button mUseDeviceForFutureLaunchesCheckbox;
private boolean mUseDeviceForFutureLaunches;
private boolean mDisableAvdSelectionChange = false;
/**
* Basic Content Provider for a table full of {@link IDevice} objects. The input is
* a {@link AndroidDebugBridge}.
*/
private class ContentProvider implements IStructuredContentProvider {
@Override
public Object[] getElements(Object inputElement) {
if (inputElement instanceof AndroidDebugBridge) {
return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices());
}
return new Object[0];
}
private Object[] findCompatibleDevices(IDevice[] devices) {
if (devices == null) {
return null;
}
List<IDevice> compatibleDevices = new ArrayList<IDevice>(devices.length);
for (IDevice device : devices) {
AndroidVersion deviceVersion = Sdk.getDeviceVersion(device);
if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) {
compatibleDevices.add(device);
}
}
return compatibleDevices.toArray();
}
@Override
public void dispose() {
// pass
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// pass
}
}
/**
* A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}.
* It provides labels and images for {@link IDevice} objects.
*/
private class LabelProvider implements ITableLabelProvider {
@Override
public Image getColumnImage(Object element, int columnIndex) {
if (element instanceof IDevice) {
IDevice device = (IDevice)element;
switch (columnIndex) {
case 0:
return device.isEmulator() ? mEmulatorImage : mDeviceImage;
case 2:
// check for compatibility.
if (device.isEmulator() == false) { // physical device
// get the version of the device
AndroidVersion deviceVersion = Sdk.getDeviceVersion(device);
if (deviceVersion == null) {
return mWarningImage;
} else {
if (!deviceVersion.canRun(mMinApiVersion)) {
return mNoMatchImage;
}
// if the project is compiling against an add-on,
// the optional API may be missing from the device.
return mProjectTarget.isPlatform() ?
mMatchImage : mWarningImage;
}
} else {
// get the AvdInfo
AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
true /*validAvdOnly*/);
AvdCompatibility.Compatibility c =
AvdCompatibility.canRun(info, mProjectTarget,
mMinApiVersion);
switch (c) {
case YES:
return mMatchImage;
case NO:
return mNoMatchImage;
case UNKNOWN:
return mWarningImage;
}
}
}
}
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
if (element instanceof IDevice) {
IDevice device = (IDevice)element;
switch (columnIndex) {
case 0:
return device.getName();
case 1:
if (device.isEmulator()) {
return device.getAvdName();
} else {
return "N/A"; // devices don't have AVD names.
}
case 2:
if (device.isEmulator()) {
AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
true /*validAvdOnly*/);
if (info == null) {
return "?";
}
return info.getTarget().getFullName();
} else {
String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION);
if (deviceBuild == null) {
return "unknown";
}
return deviceBuild;
}
case 3:
String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
return "Yes";
} else {
return "";
}
case 4:
return getStateString(device);
}
}
return null;
}
@Override
public void addListener(ILabelProviderListener listener) {
// pass
}
@Override
public void dispose() {
// pass
}
@Override
public boolean isLabelProperty(Object element, String property) {
// pass
return false;
}
@Override
public void removeListener(ILabelProviderListener listener) {
// pass
}
}
public static class DeviceChooserResponse {
private AvdInfo mAvdToLaunch;
private IDevice mDeviceToUse;
private boolean mUseDeviceForFutureLaunches;
public void setDeviceToUse(IDevice d) {
mDeviceToUse = d;
mAvdToLaunch = null;
}
public void setAvdToLaunch(AvdInfo avd) {
mAvdToLaunch = avd;
mDeviceToUse = null;
}
public IDevice getDeviceToUse() {
return mDeviceToUse;
}
public AvdInfo getAvdToLaunch() {
return mAvdToLaunch;
}
public void setUseDeviceForFutureLaunches(boolean en) {
mUseDeviceForFutureLaunches = en;
}
public boolean useDeviceForFutureLaunches() {
return mUseDeviceForFutureLaunches;
}
}
public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName,
IAndroidTarget projectTarget, AndroidVersion minApiVersion,
boolean useDeviceForFutureLaunches) {
super(parent);
mResponse = response;
mPackageName = packageName;
mProjectTarget = projectTarget;
mMinApiVersion = minApiVersion;
mSdk = Sdk.getCurrent();
mUseDeviceForFutureLaunches = useDeviceForFutureLaunches;
AndroidDebugBridge.addDeviceChangeListener(this);
loadImages();
}
private void cleanup() {
// done listening.
AndroidDebugBridge.removeDeviceChangeListener(this);
}
@Override
protected void okPressed() {
cleanup();
super.okPressed();
}
@Override
protected void cancelPressed() {
cleanup();
super.cancelPressed();
}
@Override
protected Control createContents(Composite parent) {
Control content = super.createContents(parent);
// this must be called after createContents() has happened so that the
// ok button has been created (it's created after the call to createDialogArea)
updateDefaultSelection();
return content;
}
/**
* Create the button bar: We override the Dialog implementation of this method
* so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons.
*/
@Override
protected Control createButtonBar(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(1, false);
layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
composite.setLayout(layout);
composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK);
mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches);
mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches);
mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches");
mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
mUseDeviceForFutureLaunches =
mUseDeviceForFutureLaunchesCheckbox.getSelection();
mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches);
}
});
mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
return composite;
}
@Override
protected Control createDialogArea(Composite parent) {
// set dialog title
getShell().setText("Android Device Chooser");
Composite top = new Composite(parent, SWT.NONE);
top.setLayout(new GridLayout(1, true));
String msg;
if (mProjectTarget.isPlatform()) {
msg = String.format("Select a device with min API level %s.",
mMinApiVersion.getApiString());
} else {
msg = String.format("Select a device compatible with target %s.",
mProjectTarget.getFullName());
}
Label label = new Label(top, SWT.NONE);
label.setText(msg);
mDeviceRadioButton = new Button(top, SWT.RADIO);
mDeviceRadioButton.setText("Choose a running Android device");
mDeviceRadioButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean deviceMode = mDeviceRadioButton.getSelection();
mDeviceTable.setEnabled(deviceMode);
mPreferredAvdSelector.setEnabled(!deviceMode);
if (deviceMode) {
handleDeviceSelection();
} else {
mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
}
enableOkButton();
}
});
mDeviceRadioButton.setSelection(true);
// offset the selector from the radio button
Composite offsetComp = new Composite(top, SWT.NONE);
offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
GridLayout layout = new GridLayout(1, false);
layout.marginRight = layout.marginHeight = 0;
layout.marginLeft = 30;
offsetComp.setLayout(layout);
mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
GridData gd;
mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
gd.heightHint = 100;
mDeviceTable.setHeaderVisible(true);
mDeviceTable.setLinesVisible(true);
TableHelper.createTableColumn(mDeviceTable, "Serial Number",
SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
null /* prefs name */, null /* prefs store */);
TableHelper.createTableColumn(mDeviceTable, "AVD Name",
SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
null /* prefs name */, null /* prefs store */);
TableHelper.createTableColumn(mDeviceTable, "Target",
SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$
null /* prefs name */, null /* prefs store */);
TableHelper.createTableColumn(mDeviceTable, "Debug",
SWT.LEFT, "Debug", //$NON-NLS-1$
null /* prefs name */, null /* prefs store */);
TableHelper.createTableColumn(mDeviceTable, "State",
SWT.LEFT, "bootloader", //$NON-NLS-1$
null /* prefs name */, null /* prefs store */);
// create the viewer for it
mViewer = new TableViewer(mDeviceTable);
mViewer.setContentProvider(new ContentProvider());
mViewer.setLabelProvider(new LabelProvider());
mViewer.setInput(AndroidDebugBridge.getBridge());
mDeviceTable.addSelectionListener(new SelectionAdapter() {
/**
* Handles single-click selection on the device selector.
* {@inheritDoc}
*/
@Override
public void widgetSelected(SelectionEvent e) {
handleDeviceSelection();
}
/**
* Handles double-click selection on the device selector.
* Note that the single-click handler will probably already have been called.
* {@inheritDoc}
*/
@Override
public void widgetDefaultSelected(SelectionEvent e) {
handleDeviceSelection();
if (isOkButtonEnabled()) {
okPressed();
}
}
});
Button radio2 = new Button(top, SWT.RADIO);
radio2.setText("Launch a new Android Virtual Device");
// offset the selector from the radio button
offsetComp = new Composite(top, SWT.NONE);
offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
layout = new GridLayout(1, false);
layout.marginRight = layout.marginHeight = 0;
layout.marginLeft = 30;
offsetComp.setLayout(layout);
mPreferredAvdSelector = new AvdSelector(offsetComp,
mSdk.getSdkOsLocation(),
mSdk.getAvdManager(),
new NonRunningAvdFilter(),
DisplayMode.SIMPLE_SELECTION,
new AdtConsoleSdkLog());
mPreferredAvdSelector.setTableHeightHint(100);
mPreferredAvdSelector.setEnabled(false);
mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() {
/**
* Handles single-click selection on the AVD selector.
* {@inheritDoc}
*/
@Override
public void widgetSelected(SelectionEvent e) {
if (mDisableAvdSelectionChange == false) {
mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
enableOkButton();
}
}
/**
* Handles double-click selection on the AVD selector.
*
* Note that the single-click handler will probably already have been called
* but the selected item can have changed in between.
*
* {@inheritDoc}
*/
@Override
public void widgetDefaultSelected(SelectionEvent e) {
widgetSelected(e);
if (isOkButtonEnabled()) {
okPressed();
}
}
});
return top;
}
private void loadImages() {
ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader();
Display display = DdmsPlugin.getDisplay();
IconFactory factory = IconFactory.getInstance();
if (mDeviceImage == null) {
mDeviceImage = ddmUiLibLoader.loadImage(display,
"device.png", //$NON-NLS-1$
ICON_WIDTH, ICON_WIDTH,
display.getSystemColor(SWT.COLOR_RED));
}
if (mEmulatorImage == null) {
mEmulatorImage = ddmUiLibLoader.loadImage(display,
"emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
display.getSystemColor(SWT.COLOR_BLUE));
}
if (mMatchImage == null) {
mMatchImage = factory.getIcon("match", //$NON-NLS-1$
IconFactory.COLOR_GREEN,
IconFactory.SHAPE_DEFAULT);
}
if (mNoMatchImage == null) {
mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$
IconFactory.COLOR_RED,
IconFactory.SHAPE_DEFAULT);
}
if (mWarningImage == null) {
mWarningImage = factory.getIcon("warning", //$NON-NLS-1$
SWT.COLOR_YELLOW,
IconFactory.SHAPE_DEFAULT);
}
}
/**
* Returns a display string representing the state of the device.
* @param d the device
*/
private static String getStateString(IDevice d) {
DeviceState deviceState = d.getState();
if (deviceState == DeviceState.ONLINE) {
return "Online";
} else if (deviceState == DeviceState.OFFLINE) {
return "Offline";
} else if (deviceState == DeviceState.BOOTLOADER) {
return "Bootloader";
}
return "??";
}
/**
* Sent when the a device is connected to the {@link AndroidDebugBridge}.
* <p/>
* This is sent from a non UI thread.
* @param device the new device.
*
* @see IDeviceChangeListener#deviceConnected(IDevice)
*/
@Override
public void deviceConnected(IDevice device) {
final DeviceChooserDialog dialog = this;
exec(new Runnable() {
@Override
public void run() {
if (mDeviceTable.isDisposed() == false) {
// refresh all
mViewer.refresh();
// update the selection
updateDefaultSelection();
// update the display of AvdInfo (since it's filtered to only display
// non running AVD.)
refillAvdList(false /*reloadAvds*/);
} else {
// table is disposed, we need to do something.
// lets remove ourselves from the listener.
AndroidDebugBridge.removeDeviceChangeListener(dialog);
}
}
});
}
/**
* Sent when the a device is connected to the {@link AndroidDebugBridge}.
* <p/>
* This is sent from a non UI thread.
* @param device the new device.
*
* @see IDeviceChangeListener#deviceDisconnected(IDevice)
*/
@Override
public void deviceDisconnected(IDevice device) {
deviceConnected(device);
}
/**
* Sent when a device data changed, or when clients are started/terminated on the device.
* <p/>
* This is sent from a non UI thread.
* @param device the device that was updated.
* @param changeMask the mask indicating what changed.
*
* @see IDeviceChangeListener#deviceChanged(IDevice, int)
*/
@Override
public void deviceChanged(final IDevice device, int changeMask) {
if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) {
final DeviceChooserDialog dialog = this;
exec(new Runnable() {
@Override
public void run() {
if (mDeviceTable.isDisposed() == false) {
// refresh the device
mViewer.refresh(device);
// update the defaultSelection.
updateDefaultSelection();
// update the display of AvdInfo (since it's filtered to only display
// non running AVD). This is done on deviceChanged because the avd name
// of a (emulator) device may be updated as the emulator boots.
refillAvdList(false /*reloadAvds*/);
// if the changed device is the current selection,
// we update the OK button based on its state.
if (device == mResponse.getDeviceToUse()) {
enableOkButton();
}
} else {
// table is disposed, we need to do something.
// lets remove ourselves from the listener.
AndroidDebugBridge.removeDeviceChangeListener(dialog);
}
}
});
}
}
/**
* Returns whether the dialog is in "device" mode (true), or in "avd" mode (false).
*/
private boolean isDeviceMode() {
return mDeviceRadioButton.getSelection();
}
/**
* Enables or disables the OK button of the dialog based on various selections in the dialog.
*/
private void enableOkButton() {
Button okButton = getButton(IDialogConstants.OK_ID);
if (isDeviceMode()) {
okButton.setEnabled(mResponse.getDeviceToUse() != null &&
mResponse.getDeviceToUse().isOnline());
} else {
okButton.setEnabled(mResponse.getAvdToLaunch() != null);
}
}
/**
* Returns true if the ok button is enabled.
*/
private boolean isOkButtonEnabled() {
Button okButton = getButton(IDialogConstants.OK_ID);
return okButton.isEnabled();
}
/**
* Executes the {@link Runnable} in the UI thread.
* @param runnable the runnable to execute.
*/
private void exec(Runnable runnable) {
try {
Display display = mDeviceTable.getDisplay();
display.asyncExec(runnable);
} catch (SWTException e) {
// tree is disposed, we need to do something. lets remove ourselves from the listener.
AndroidDebugBridge.removeDeviceChangeListener(this);
}
}
private void handleDeviceSelection() {
int count = mDeviceTable.getSelectionCount();
if (count != 1) {
handleSelection(null);
} else {
int index = mDeviceTable.getSelectionIndex();
Object data = mViewer.getElementAt(index);
if (data instanceof IDevice) {
handleSelection((IDevice)data);
} else {
handleSelection(null);
}
}
}
private void handleSelection(IDevice device) {
mResponse.setDeviceToUse(device);
enableOkButton();
}
/**
* Look for a default device to select. This is done by looking for the running
* clients on each device and finding one similar to the one being launched.
* <p/>
* This is done every time the device list changed unless there is a already selection.
*/
private void updateDefaultSelection() {
if (mDeviceTable.getSelectionCount() == 0) {
AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
IDevice[] devices = bridge.getDevices();
for (IDevice device : devices) {
Client[] clients = device.getClients();
for (Client client : clients) {
if (mPackageName.equals(client.getClientData().getClientDescription())) {
// found a match! Select it.
mViewer.setSelection(new StructuredSelection(device));
handleSelection(device);
// and we're done.
return;
}
}
}
}
handleDeviceSelection();
}
private final class NonRunningAvdFilter implements IAvdFilter {
private IDevice[] mDevices;
@Override
public void prepare() {
mDevices = AndroidDebugBridge.getBridge().getDevices();
}
@Override
public boolean accept(AvdInfo avd) {
if (mDevices != null) {
for (IDevice d : mDevices) {
// do not accept running avd's
if (avd.getName().equals(d.getAvdName())) {
return false;
}
// only accept avd's that can actually run the project
AvdCompatibility.Compatibility c =
AvdCompatibility.canRun(avd, mProjectTarget, mMinApiVersion);
return (c == AvdCompatibility.Compatibility.NO) ? false : true;
}
}
return true;
}
@Override
public void cleanup() {
mDevices = null;
}
}
/**
* Refills the AVD list keeping the current selection.
*/
private void refillAvdList(boolean reloadAvds) {
// save the current selection
AvdInfo selected = mPreferredAvdSelector.getSelected();
// disable selection change.
mDisableAvdSelectionChange = true;
// refresh the list
mPreferredAvdSelector.refresh(false);
// attempt to reselect the proper avd if needed
if (selected != null) {
if (mPreferredAvdSelector.setSelection(selected) == false) {
// looks like the selection is lost. this can happen if an emulator
// running the AVD that was selected was launched from outside of Eclipse).
mResponse.setAvdToLaunch(null);
enableOkButton();
}
}
// enable the selection change
mDisableAvdSelectionChange = false;
}
}