FileDocCategorySizeDatePackage
DeviceChooserDialog.javaAPI DocAndroid 1.5 API27228Wed May 06 22:41:10 BST 2009com.android.ide.eclipse.adt.launch

DeviceChooserDialog.java

/*
 * 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.launch;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.Client;
import com.android.ddmlib.Device;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Device.DeviceState;
import com.android.ddmuilib.IImageLoader;
import com.android.ddmuilib.ImageHelper;
import com.android.ddmuilib.TableHelper;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.ddms.DdmsPlugin;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.avd.AvdManager;
import com.android.sdklib.avd.AvdManager.AvdInfo;
import com.android.sdkuilib.AvdSelector;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.preference.IPreferenceStore;
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.Shell;
import org.eclipse.swt.widgets.Table;

import java.util.ArrayList;

/**
 * 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 final static String PREFS_COL_SERIAL = "deviceChooser.serial"; //$NON-NLS-1$
    private final static String PREFS_COL_STATE  = "deviceChooser.state"; //$NON-NLS-1$
    private final static String PREFS_COL_AVD     = "deviceChooser.avd"; //$NON-NLS-1$
    private final static String PREFS_COL_TARGET = "deviceChooser.target"; //$NON-NLS-1$
    private final static String PREFS_COL_DEBUG  = "deviceChooser.debug"; //$NON-NLS-1$

    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 Sdk mSdk;

    private final AvdInfo[] mFullAvdList;

    private Button mDeviceRadioButton;

    private boolean mDisableAvdSelectionChange = false;
    
    /**
     * Basic Content Provider for a table full of {@link Device} objects. The input is
     * a {@link AndroidDebugBridge}.
     */
    private class ContentProvider implements IStructuredContentProvider {
        public Object[] getElements(Object inputElement) {
            if (inputElement instanceof AndroidDebugBridge) {
                return ((AndroidDebugBridge)inputElement).getDevices();
            }

            return new Object[0];
        }

        public void dispose() {
            // pass
        }

        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 Device} objects.
     */
    private class LabelProvider implements ITableLabelProvider {

        public Image getColumnImage(Object element, int columnIndex) {
            if (element instanceof Device) {
                Device device = (Device)element;
                switch (columnIndex) {
                    case 0:
                        return device.isEmulator() ? mEmulatorImage : mDeviceImage;
                        
                    case 2:
                        // check for compatibility.
                        if (device.isEmulator() == false) { // physical device
                            // get the api level of the device
                            try {
                                String apiValue = device.getProperty(
                                        IDevice.PROP_BUILD_VERSION_NUMBER);
                                if (apiValue != null) {
                                    int api = Integer.parseInt(apiValue);
                                    if (api >= mProjectTarget.getApiVersionNumber()) {
                                        // if the project is compiling against an add-on, the optional
                                        // API may be missing from the device.
                                        return mProjectTarget.isPlatform() ?
                                                mMatchImage : mWarningImage;
                                    } else {
                                        return mNoMatchImage;
                                    }
                                } else {
                                    return mWarningImage;
                                }
                            } catch (NumberFormatException e) {
                                // lets consider the device non compatible
                                return mNoMatchImage;
                            }
                        } else {
                            // get the AvdInfo
                            AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
                                    true /*validAvdOnly*/);
                            if (info == null) {
                                return mWarningImage;
                            }
                            return mProjectTarget.isCompatibleBaseFor(info.getTarget()) ?
                                    mMatchImage : mNoMatchImage;
                        }
                }
            }

            return null;
        }

        public String getColumnText(Object element, int columnIndex) {
            if (element instanceof Device) {
                Device device = (Device)element;
                switch (columnIndex) {
                    case 0:
                        return device.getSerialNumber();
                    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;
        }

        public void addListener(ILabelProviderListener listener) {
            // pass
        }

        public void dispose() {
            // pass
        }

        public boolean isLabelProperty(Object element, String property) {
            // pass
            return false;
        }

        public void removeListener(ILabelProviderListener listener) {
            // pass
        }
    }
    
    public static class DeviceChooserResponse {
        private AvdInfo mAvdToLaunch;
        private IDevice mDeviceToUse;
        
        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 DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName,
            IAndroidTarget projectTarget) {
        super(parent);
        mResponse = response;
        mPackageName = packageName;
        mProjectTarget = projectTarget;
        mSdk = Sdk.getCurrent();
        
        // get the full list of Android Virtual Devices
        AvdManager avdManager = mSdk.getAvdManager();
        if (avdManager != null) {
            mFullAvdList = avdManager.getValidAvds();
        } else {
            mFullAvdList = null;
        }

        loadImages();
    }

    private void cleanup() {
        // done listening.
        AndroidDebugBridge.removeDeviceChangeListener(this);

        mEmulatorImage.dispose();
        mDeviceImage.dispose();
        mMatchImage.dispose();
        mNoMatchImage.dispose();
        mWarningImage.dispose();
    }
    
    @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;
    }

    
    @Override
    protected Control createDialogArea(Composite parent) {
        Composite top = new Composite(parent, SWT.NONE);
        top.setLayout(new GridLayout(1, true));

        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.getFirstSelected());
                }
                
                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);

        IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); 
        mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION);
        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$
                PREFS_COL_SERIAL, store);

        TableHelper.createTableColumn(mDeviceTable, "AVD Name",
                SWT.LEFT, "engineering", //$NON-NLS-1$
                PREFS_COL_AVD, store);

        TableHelper.createTableColumn(mDeviceTable, "Target",
                SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$
                PREFS_COL_TARGET, store);

        TableHelper.createTableColumn(mDeviceTable, "Debug",
                SWT.LEFT, "Debug", //$NON-NLS-1$
                PREFS_COL_DEBUG, store);

        TableHelper.createTableColumn(mDeviceTable, "State",
                SWT.LEFT, "bootloader", //$NON-NLS-1$
                PREFS_COL_STATE, 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, getNonRunningAvds(), mProjectTarget);
        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.getFirstSelected());
                    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();
                }
            }
        });
        
        AndroidDebugBridge.addDeviceChangeListener(this);

        return top;
    }
    
    private void loadImages() {
        IImageLoader ddmsLoader = DdmsPlugin.getImageLoader();
        Display display = DdmsPlugin.getDisplay();
        IImageLoader adtLoader = AdtPlugin.getImageLoader();

        if (mDeviceImage == null) {
            mDeviceImage = ImageHelper.loadImage(ddmsLoader, display,
                    "device.png", //$NON-NLS-1$
                    ICON_WIDTH, ICON_WIDTH,
                    display.getSystemColor(SWT.COLOR_RED));
        }
        if (mEmulatorImage == null) {
            mEmulatorImage = ImageHelper.loadImage(ddmsLoader, display,
                    "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
                    display.getSystemColor(SWT.COLOR_BLUE));
        }
        
        if (mMatchImage == null) {
            mMatchImage = ImageHelper.loadImage(adtLoader, display,
                    "match.png", //$NON-NLS-1$
                    ICON_WIDTH, ICON_WIDTH,
                    display.getSystemColor(SWT.COLOR_GREEN));
        }

        if (mNoMatchImage == null) {
            mNoMatchImage = ImageHelper.loadImage(adtLoader, display,
                    "error.png", //$NON-NLS-1$
                    ICON_WIDTH, ICON_WIDTH,
                    display.getSystemColor(SWT.COLOR_RED));
        }

        if (mWarningImage == null) {
            mWarningImage = ImageHelper.loadImage(adtLoader, display,
                    "warning.png", //$NON-NLS-1$
                    ICON_WIDTH, ICON_WIDTH,
                    display.getSystemColor(SWT.COLOR_YELLOW));
        }

    }
    
    /**
     * Returns a display string representing the state of the device.
     * @param d the device
     */
    private static String getStateString(Device 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(Device)
     */
    public void deviceConnected(Device device) {
        final DeviceChooserDialog dialog = this;
        exec(new Runnable() {
            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();
                } 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(Device)
     */
    public void deviceDisconnected(Device 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(Device, int)
     */
    public void deviceChanged(final Device device, int changeMask) {
        if ((changeMask & (Device.CHANGE_STATE | Device.CHANGE_BUILD_INFO)) != 0) {
            final DeviceChooserDialog dialog = this;
            exec(new Runnable() {
                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();

                        // 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 Device) {
                handleSelection((Device)data);
            } else {
                handleSelection(null);
            }
        }
    }

    private void handleSelection(Device 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();
            
            Device[] devices = bridge.getDevices();
            
            for (Device 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();
    }
    
    /**
     * Returns the list of {@link AvdInfo} that are not already running in an emulator.
     */
    private AvdInfo[] getNonRunningAvds() {
        ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
        
        Device[] devices = AndroidDebugBridge.getBridge().getDevices();
        
        // loop through all the Avd and put the one that are not running in the list.
        avdLoop: for (AvdInfo info : mFullAvdList) {
            for (Device d : devices) {
                if (info.getName().equals(d.getAvdName())) {
                    continue avdLoop;
                }
            }
            list.add(info);
        }
        
        return list.toArray(new AvdInfo[list.size()]);
    } 
    
    /**
     * Refills the AVD list keeping the current selection.
     */
    private void refillAvdList() {
        AvdInfo[] array = getNonRunningAvds();
        
        // save the current selection
        AvdInfo selected = mPreferredAvdSelector.getFirstSelected();
        
        // disable selection change.
        mDisableAvdSelectionChange = true;
        
        // set the new list in the selector
        mPreferredAvdSelector.setAvds(array, mProjectTarget);
        
        // 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;
    }
}