/*
 * 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.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.CanceledException;
import com.android.ddmlib.Client;
import com.android.ddmlib.ClientData;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.Log;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.ClientData.DebuggerStatus;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.actions.AvdManagerAction;
import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode;
import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.NullSdkLog;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.xml.ManifestData;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMConnector;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

/**
 * Controls the launch of Android application either on a device or on the
 * emulator. If an emulator is already running, this class will attempt to reuse
 * it.
 */
public final class AndroidLaunchController implements IDebugBridgeChangeListener,
        IDeviceChangeListener, IClientChangeListener, ILaunchController {

    private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$
    private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$
    private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$
    private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$
    private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$

    /**
     * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection
     * to running application. The integer is the port on which to connect.
     * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
     */
    private static final HashMap<ILaunchConfiguration, Integer> sRunningAppMap =
        new HashMap<ILaunchConfiguration, Integer>();

    private static final Object sListLock = sRunningAppMap;

    /**
     * List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
     * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
     * DelayedLaunchInfo object is moved to
     * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
     * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
     */
    private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
        new ArrayList<DelayedLaunchInfo>();

    /**
     * List of application waiting to be launched on a device/emulator.<br>
     * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
     * */
    private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList =
        new ArrayList<DelayedLaunchInfo>();

    /**
     * Application waiting to show up as waiting for debugger.
     * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
     */
    private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications =
        new ArrayList<DelayedLaunchInfo>();

    /**
     * List of clients that have appeared as waiting for debugger before their name was available.
     * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
     */
    private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>();

    /** static instance for singleton */
    private static AndroidLaunchController sThis = new AndroidLaunchController();

    /** private constructor to enforce singleton */
    private AndroidLaunchController() {
        AndroidDebugBridge.addDebugBridgeChangeListener(this);
        AndroidDebugBridge.addDeviceChangeListener(this);
        AndroidDebugBridge.addClientChangeListener(this);
    }

    /**
     * Returns the singleton reference.
     */
    public static AndroidLaunchController getInstance() {
        return sThis;
    }


    /**
     * Launches a remote java debugging session on an already running application
     * @param project The project of the application to debug.
     * @param debugPort The port to connect the debugger to.
     */
    public static void debugRunningApp(IProject project, int debugPort) {
        // get an existing or new launch configuration
        ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project);

        if (config != null) {
            setPortLaunchConfigAssociation(config, debugPort);

            // and launch
            DebugUITools.launch(config, ILaunchManager.DEBUG_MODE);
        }
    }

    /**
     * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}.
     * @param project the project
     * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was
     * an error when creating a new one.
     */
    public static ILaunchConfiguration getLaunchConfig(IProject project) {
        // get the launch manager
        ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();

        // now get the config type for our particular android type.
        ILaunchConfigurationType configType = manager.getLaunchConfigurationType(
                        LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);

        String name = project.getName();

        // search for an existing launch configuration
        ILaunchConfiguration config = findConfig(manager, configType, name);

        // test if we found one or not
        if (config == null) {
            // Didn't find a matching config, so we make one.
            // It'll be made in the "working copy" object first.
            ILaunchConfigurationWorkingCopy wc = null;

            try {
                // make the working copy object
                wc = configType.newInstance(null,
                        manager.generateUniqueLaunchConfigurationNameFrom(name));

                // set the project name
                wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);

                // set the launch mode to default.
                wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
                        LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);

                // set default target mode
                wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
                        LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());

                // default AVD: None
                wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);

                // set the default network speed
                wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
                        LaunchConfigDelegate.DEFAULT_SPEED);

                // and delay
                wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
                        LaunchConfigDelegate.DEFAULT_DELAY);

                // default wipe data mode
                wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
                        LaunchConfigDelegate.DEFAULT_WIPE_DATA);

                // default disable boot animation option
                wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
                        LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);

                // set default emulator options
                IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
                String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS);
                wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);

                // map the config and the project
                wc.setMappedResources(getResourcesToMap(project));

                // save the working copy to get the launch config object which we return.
                return wc.doSave();

            } catch (CoreException e) {
                String msg = String.format(
                        "Failed to create a Launch config for project '%1$s': %2$s",
                        project.getName(), e.getMessage());
                AdtPlugin.printErrorToConsole(project, msg);

                // no launch!
                return null;
            }
        }

        return config;
    }

    /**
     * Returns the list of resources to map to a Launch Configuration.
     * @param project the project associated to the launch configuration.
     */
    public static IResource[] getResourcesToMap(IProject project) {
        ArrayList<IResource> array = new ArrayList<IResource>(2);
        array.add(project);

        IFile manifest = ProjectHelper.getManifest(project);
        if (manifest != null) {
            array.add(manifest);
        }

        return array.toArray(new IResource[array.size()]);
    }

    /**
     * Launches an android app on the device or emulator
     *
     * @param project The project we're launching
     * @param mode the mode in which to launch, one of the mode constants
     *      defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or
     *      <code>DEBUG_MODE</code>.
     * @param apk the resource to the apk to launch.
     * @param packageName the Android package name of the app
     * @param debugPackageName the Android package name to debug
     * @param debuggable the debuggable value of the app's manifest, or null if not set.
     * @param requiredApiVersionNumber the api version required by the app, or null if none.
     * @param launchAction the action to perform after app sync
     * @param config the launch configuration
     * @param launch the launch object
     */
    public void launch(final IProject project, String mode, IFile apk,
            String packageName, String debugPackageName, Boolean debuggable,
            String requiredApiVersionNumber, final IAndroidLaunchAction launchAction,
            final AndroidLaunchConfiguration config, final AndroidLaunch launch,
            IProgressMonitor monitor) {

        String message = String.format("Performing %1$s", launchAction.getLaunchDescription());
        AdtPlugin.printToConsole(project, message);

        // create the launch info
        final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName,
                debugPackageName, launchAction, apk, debuggable, requiredApiVersionNumber, launch,
                monitor);

        // set the debug mode
        launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE));

        // get the SDK
        Sdk currentSdk = Sdk.getCurrent();
        AvdManager avdManager = currentSdk.getAvdManager();

        // reload the AVDs to make sure we are up to date
        try {
            avdManager.reloadAvds(NullSdkLog.getLogger());
        } catch (AndroidLocationException e1) {
            // this happens if the AVD Manager failed to find the folder in which the AVDs are
            // stored. This is unlikely to happen, but if it does, we should force to go manual
            // to allow using physical devices.
            config.mTargetMode = TargetMode.MANUAL;
        }

        // get the project target
        IAndroidTarget projectTarget = currentSdk.getTarget(project);

        // FIXME: check errors on missing sdk, AVD manager, or project target.

        // device chooser response object.
        final DeviceChooserResponse response = new DeviceChooserResponse();

        /*
         * Launch logic:
         * - Manually Mode
         *       Always display a UI that lets a user see the current running emulators/devices.
         *       The UI must show which devices are compatibles, and allow launching new emulators
         *       with compatible (and not yet running) AVD.
         * - Automatic Way
         *     * Preferred AVD set.
         *           If Preferred AVD is not running: launch it.
         *           Launch the application on the preferred AVD.
         *     * No preferred AVD.
         *           Count the number of compatible emulators/devices.
         *           If != 1, display a UI similar to manual mode.
         *           If == 1, launch the application on this AVD/device.
         */

        if (config.mTargetMode == TargetMode.AUTO) {
            // if we are in automatic target mode, we need to find the current devices
            IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();

            // first check if we have a preferred AVD name, and if it actually exists, and is valid
            // (ie able to run the project).
            // We need to check this in case the AVD was recreated with a different target that is
            // not compatible.
            AvdInfo preferredAvd = null;
            if (config.mAvdName != null) {
                preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/);
                if (projectTarget.canRunOn(preferredAvd.getTarget()) == false) {
                    preferredAvd = null;

                    AdtPlugin.printErrorToConsole(project, String.format(
                            "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...",
                            config.mAvdName, projectTarget.getName()));
                }
            }

            if (preferredAvd != null) {
                // look for a matching device

                for (IDevice d : devices) {
                    String deviceAvd = d.getAvdName();
                    if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) {
                        response.setDeviceToUse(d);

                        AdtPlugin.printToConsole(project, String.format(
                                "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'",
                                config.mAvdName, d));

                        continueLaunch(response, project, launch, launchInfo, config);
                        return;
                    }
                }

                // at this point we have a valid preferred AVD that is not running.
                // We need to start it.
                response.setAvdToLaunch(preferredAvd);

                AdtPlugin.printToConsole(project, String.format(
                        "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.",
                        config.mAvdName));

                continueLaunch(response, project, launch, launchInfo, config);
                return;
            }

            // no (valid) preferred AVD? look for one.

            // If the API level requested in the manifest is lower than the current project
            // target, when we will iterate devices/avds later ideally we will want to find
            // a device/avd which target is as close to the manifest as possible (instead of
            // a device which target is the same as the project's target) and use it as the
            // new default.

            int reqApiLevel = 0;
            try {
                reqApiLevel = Integer.parseInt(requiredApiVersionNumber);

                if (reqApiLevel > 0 && reqApiLevel < projectTarget.getVersion().getApiLevel()) {
                    int maxDist = projectTarget.getVersion().getApiLevel() - reqApiLevel;
                    IAndroidTarget candidate = null;

                    for (IAndroidTarget target : currentSdk.getTargets()) {
                        if (target.canRunOn(projectTarget)) {
                            int currDist = target.getVersion().getApiLevel() - reqApiLevel;
                            if (currDist >= 0 && currDist < maxDist) {
                                maxDist = currDist;
                                candidate = target;
                                if (maxDist == 0) {
                                    // Found a perfect match
                                    break;
                                }
                            }
                        }
                    }

                    if (candidate != null) {
                        // We found a better SDK target candidate, that is closer to the
                        // API level from minSdkVersion than the one currently used by the
                        // project. Below (in the for...devices loop) we'll try to find
                        // a device/AVD for it.
                        projectTarget = candidate;
                    }
                }
            } catch (NumberFormatException e) {
                // pass
            }

            HashMap<IDevice, AvdInfo> compatibleRunningAvds = new HashMap<IDevice, AvdInfo>();
            boolean hasDevice = false; // if there's 1+ device running, we may force manual mode,
                                       // as we cannot always detect proper compatibility with
                                       // devices. This is the case if the project target is not
                                       // a standard platform
            for (IDevice d : devices) {
                String deviceAvd = d.getAvdName();
                if (deviceAvd != null) { // physical devices return null.
                    AvdInfo info = avdManager.getAvd(deviceAvd, true /*validAvdOnly*/);
                    if (info != null && projectTarget.canRunOn(info.getTarget())) {
                        compatibleRunningAvds.put(d, info);
                    }
                } else {
                    if (projectTarget.isPlatform()) { // means this can run on any device as long
                                                      // as api level is high enough
                        AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
                        // the deviceVersion may be null if it wasn't yet queried (device just
                        // plugged in or emulator just booting up.
                        if (deviceVersion != null &&
                                deviceVersion.canRun(projectTarget.getVersion())) {
                            // device is compatible with project
                            compatibleRunningAvds.put(d, null);
                            continue;
                        }
                    } else {
                        // for non project platform, we can't be sure if a device can
                        // run an application or not, since we don't query the device
                        // for the list of optional libraries that it supports.
                    }
                    hasDevice = true;
                }
            }

            // depending on the number of devices, we'll simulate an automatic choice
            // from the device chooser or simply show up the device chooser.
            if (hasDevice == false && compatibleRunningAvds.size() == 0) {
                // if zero emulators/devices, we launch an emulator.
                // We need to figure out which AVD first.

                // we are going to take the closest AVD. ie a compatible AVD that has the API level
                // closest to the project target.
                AvdInfo defaultAvd = findMatchingAvd(avdManager, projectTarget);

                if (defaultAvd != null) {
                    response.setAvdToLaunch(defaultAvd);

                    AdtPlugin.printToConsole(project, String.format(
                            "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'",
                            defaultAvd.getName()));

                    continueLaunch(response, project, launch, launchInfo, config);
                    return;
                } else {
                    AdtPlugin.printToConsole(project, String.format(
                            "Failed to find an AVD compatible with target '%1$s'.",
                            projectTarget.getName()));

                    final Display display = AdtPlugin.getDisplay();
                    final boolean[] searchAgain = new boolean[] { false };
                    // ask the user to create a new one.
                    display.syncExec(new Runnable() {
                        public void run() {
                            Shell shell = display.getActiveShell();
                            if (MessageDialog.openQuestion(shell, "Android AVD Error",
                                    "No compatible targets were found. Do you wish to a add new Android Virtual Device?")) {
                                AvdManagerAction action = new AvdManagerAction();
                                action.run(null /*action*/);
                                searchAgain[0] = true;
                            }
                        }
                    });
                    if (searchAgain[0]) {
                        // attempt to reload the AVDs and find one compatible.
                        defaultAvd = findMatchingAvd(avdManager, projectTarget);

                        if (defaultAvd == null) {
                            AdtPlugin.printErrorToConsole(project, String.format(
                                    "Still no compatible AVDs with target '%1$s': Aborting launch.",
                                    projectTarget.getName()));
                            stopLaunch(launchInfo);
                        } else {
                            response.setAvdToLaunch(defaultAvd);

                            AdtPlugin.printToConsole(project, String.format(
                                    "Launching new emulator with compatible AVD '%1$s'",
                                    defaultAvd.getName()));

                            continueLaunch(response, project, launch, launchInfo, config);
                            return;
                        }
                    }
                }
            } else if (hasDevice == false && compatibleRunningAvds.size() == 1) {
                Entry<IDevice, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next();
                response.setDeviceToUse(e.getKey());

                // get the AvdInfo, if null, the device is a physical device.
                AvdInfo avdInfo = e.getValue();
                if (avdInfo != null) {
                    message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'",
                            response.getDeviceToUse(), e.getValue().getName());
                } else {
                    message = String.format("Automatic Target Mode: using device '%1$s'",
                            response.getDeviceToUse());
                }
                AdtPlugin.printToConsole(project, message);

                continueLaunch(response, project, launch, launchInfo, config);
                return;
            }

            // if more than one device, we'll bring up the DeviceChooser dialog below.
            if (compatibleRunningAvds.size() >= 2) {
                message = "Automatic Target Mode: Several compatible targets. Please select a target device.";
            } else if (hasDevice) {
                message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device.";
            }

            AdtPlugin.printToConsole(project, message);
        }

        // bring up the device chooser.
        final IAndroidTarget desiredProjectTarget = projectTarget;
        AdtPlugin.getDisplay().asyncExec(new Runnable() {
            public void run() {
                try {
                    // open the chooser dialog. It'll fill 'response' with the device to use
                    // or the AVD to launch.
                    DeviceChooserDialog dialog = new DeviceChooserDialog(
                            AdtPlugin.getDisplay().getActiveShell(),
                            response, launchInfo.getPackageName(), desiredProjectTarget);
                    if (dialog.open() == Dialog.OK) {
                        AndroidLaunchController.this.continueLaunch(response, project, launch,
                                launchInfo, config);
                    } else {
                        AdtPlugin.printErrorToConsole(project, "Launch canceled!");
                        stopLaunch(launchInfo);
                        return;
                    }
                } catch (Exception e) {
                    // there seems to be some case where the shell will be null. (might be
                    // an OS X bug). Because of this the creation of the dialog will throw
                    // and IllegalArg exception interrupting the launch with no user feedback.
                    // So we trap all the exception and display something.
                    String msg = e.getMessage();
                    if (msg == null) {
                        msg = e.getClass().getCanonicalName();
                    }
                    AdtPlugin.printErrorToConsole(project,
                            String.format("Error during launch: %s", msg));
                    stopLaunch(launchInfo);
                }
            }
        });
    }

    /**
     * Find a matching AVD.
     */
    private AvdInfo findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget) {
        AvdInfo[] avds = avdManager.getValidAvds();
        AvdInfo defaultAvd = null;
        for (AvdInfo avd : avds) {
            if (projectTarget.canRunOn(avd.getTarget())) {
                // at this point we can ignore the code name issue since
                // IAndroidTarget.canRunOn() will already have filtered the non
                // compatible AVDs.
                if (defaultAvd == null ||
                        avd.getTarget().getVersion().getApiLevel() <
                            defaultAvd.getTarget().getVersion().getApiLevel()) {
                    defaultAvd = avd;
                }
            }
        }
        return defaultAvd;
    }

    /**
     * Continues the launch based on the DeviceChooser response.
     * @param response the device chooser response
     * @param project The project being launched
     * @param launch The eclipse launch info
     * @param launchInfo The {@link DelayedLaunchInfo}
     * @param config The config needed to start a new emulator.
     */
    void continueLaunch(final DeviceChooserResponse response, final IProject project,
            final AndroidLaunch launch, final DelayedLaunchInfo launchInfo,
            final AndroidLaunchConfiguration config) {

        // Since this is called from the UI thread we spawn a new thread
        // to finish the launch.
        new Thread() {
            @Override
            public void run() {
                if (response.getAvdToLaunch() != null) {
                    // there was no selected device, we start a new emulator.
                    synchronized (sListLock) {
                        AvdInfo info = response.getAvdToLaunch();
                        mWaitingForEmulatorLaunches.add(launchInfo);
                        AdtPlugin.printToConsole(project, String.format(
                                "Launching a new emulator with Virtual Device '%1$s'",
                                info.getName()));
                        boolean status = launchEmulator(config, info);

                        if (status == false) {
                            // launching the emulator failed!
                            AdtPlugin.displayError("Emulator Launch",
                                    "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing.");

                            // stop the launch and return
                            mWaitingForEmulatorLaunches.remove(launchInfo);
                            AdtPlugin.printErrorToConsole(project, "Launch canceled!");
                            stopLaunch(launchInfo);
                            return;
                        }

                        return;
                    }
                } else if (response.getDeviceToUse() != null) {
                    launchInfo.setDevice(response.getDeviceToUse());
                    simpleLaunch(launchInfo, launchInfo.getDevice());
                }
            }
        }.start();
    }

    /**
     * Queries for a debugger port for a specific {@link ILaunchConfiguration}.
     * <p/>
     * If the configuration and a debugger port where added through
     * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method
     * will return the debugger port, and remove the configuration from the list.
     * @param launchConfig the {@link ILaunchConfiguration}
     * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the
     * configuration was not setup.
     */
    static int getPortForConfig(ILaunchConfiguration launchConfig) {
        synchronized (sListLock) {
            Integer port = sRunningAppMap.get(launchConfig);
            if (port != null) {
                sRunningAppMap.remove(launchConfig);
                return port;
            }
        }

        return LaunchConfigDelegate.INVALID_DEBUG_PORT;
    }

    /**
     * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of
     * launch config to connect directly to a running app instead of doing full launch (sync,
     * launch, and connect to).
     * @param launchConfig the {@link ILaunchConfiguration} object.
     * @param port The debugger port to connect to.
     */
    private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig,
            int port) {
        synchronized (sListLock) {
            sRunningAppMap.put(launchConfig, port);
        }
    }

    /**
     * Checks the build information, and returns whether the launch should continue.
     * <p/>The value tested are:
     * <ul>
     * <li>Minimum API version requested by the application. If the target device does not match,
     * the launch is canceled.</li>
     * <li>Debuggable attribute of the application and whether or not the device requires it. If
     * the device requires it and it is not set in the manifest, the launch will be forced to
     * "release" mode instead of "debug"</li>
     * <ul>
     */
    private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) {
        if (device != null) {
            // check the app required API level versus the target device API level

            String deviceVersion = device.getProperty(IDevice.PROP_BUILD_VERSION);
            String deviceApiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
            String deviceCodeName = device.getProperty(IDevice.PROP_BUILD_CODENAME);

            int deviceApiLevel = -1;
            try {
                deviceApiLevel = Integer.parseInt(deviceApiLevelString);
            } catch (NumberFormatException e) {
                // pass, we'll keep the apiLevel value at -1.
            }

            String requiredApiString = launchInfo.getRequiredApiVersionNumber();
            if (requiredApiString != null) {
                int requiredApi = -1;
                try {
                    requiredApi = Integer.parseInt(requiredApiString);
                } catch (NumberFormatException e) {
                    // pass, we'll keep requiredApi value at -1.
                }

                if (requiredApi == -1) {
                    // this means the manifest uses a codename for minSdkVersion
                    // check that the device is using the same codename
                    if (requiredApiString.equals(deviceCodeName) == false) {
                        AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
                            "ERROR: Application requires a device running '%1$s'!",
                            requiredApiString));
                        return false;
                    }
                } else {
                    // app requires a specific API level
                    if (deviceApiLevel == -1) {
                        AdtPlugin.printToConsole(launchInfo.getProject(),
                                "WARNING: Unknown device API version!");
                    } else if (deviceApiLevel < requiredApi) {
                        String msg = String.format(
                                "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).",
                                requiredApi, deviceApiLevel, deviceVersion);
                        AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);

                        // abort the launch
                        return false;
                    }
                }
            } else {
                // warn the application API level requirement is not set.
                AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                        "WARNING: Application does not specify an API level requirement!");

                // and display the target device API level (if known)
                if (deviceApiLevel == -1) {
                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                            "WARNING: Unknown device API version!");
                } else {
                    AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
                            "Device API version is %1$d (Android %2$s)", deviceApiLevel,
                            deviceVersion));
                }
            }

            // now checks that the device/app can be debugged (if needed)
            if (device.isEmulator() == false && launchInfo.isDebugMode()) {
                String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE);
                if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$
                    // the device is "secure" and requires apps to declare themselves as debuggable!
                    // launchInfo.getDebuggable() will return null if the manifest doesn't declare
                    // anything. In this case this is fine since the build system does insert
                    // debuggable=true. The only case to look for is if false is manually set
                    // in the manifest.
                    if (launchInfo.getDebuggable() == Boolean.FALSE) {
                        String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.",
                                launchInfo.getPackageName());
                        AdtPlugin.printErrorToConsole(launchInfo.getProject(), message);

                        // because am -D does not check for ro.debuggable and the
                        // 'debuggable' attribute, it is important we do not use the -D option
                        // in this case or the app will wait for a debugger forever and never
                        // really launch.
                        launchInfo.setDebugMode(false);
                    }
                }
            }
        }

        return true;
    }

    /**
     * Do a simple launch on the specified device, attempting to sync the new
     * package, and then launching the application. Failed sync/launch will
     * stop the current AndroidLaunch and return false;
     * @param launchInfo
     * @param device
     * @return true if succeed
     */
    private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) {
        // API level check
        if (checkBuildInfo(launchInfo, device) == false) {
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
            stopLaunch(launchInfo);
            return false;
        }

        // sync the app
        if (syncApp(launchInfo, device) == false) {
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
            stopLaunch(launchInfo);
            return false;
        }

        // launch the app
        launchApp(launchInfo, device);

        return true;
    }


    /**
     * If needed, syncs the application and all its dependencies on the device/emulator.
     *
     * @param launchInfo The Launch information object.
     * @param device the device on which to sync the application
     * @return true if the install succeeded.
     */
    private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) {
        boolean alreadyInstalled = ApkInstallManager.getInstance().isApplicationInstalled(
                launchInfo.getProject(), launchInfo.getPackageName(), device);

        if (alreadyInstalled) {
            AdtPlugin.printToConsole(launchInfo.getProject(),
            "Application already deployed. No need to reinstall.");
        } else {
            if (doSyncApp(launchInfo, device) == false) {
                return false;
            }
        }

        // The app is now installed, now try the dependent projects
        for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
            String msg = String.format("Project dependency found, installing: %s",
                    dependentLaunchInfo.getProject().getName());
            AdtPlugin.printToConsole(launchInfo.getProject(), msg);
            if (syncApp(dependentLaunchInfo, device) == false) {
                return false;
            }
        }

        return true;
    }

    /**
     * Syncs the application on the device/emulator.
     *
     * @param launchInfo The Launch information object.
     * @param device the device on which to sync the application
     * @return true if the install succeeded.
     */
    private boolean doSyncApp(DelayedLaunchInfo launchInfo, IDevice device) {
        IPath path = launchInfo.getPackageFile().getLocation();
        String fileName = path.lastSegment();
        try {
            String message = String.format("Uploading %1$s onto device '%2$s'",
                    fileName, device.getSerialNumber());
            AdtPlugin.printToConsole(launchInfo.getProject(), message);

            String remotePackagePath = device.syncPackageToDevice(path.toOSString());
            boolean installResult = installPackage(launchInfo, remotePackagePath, device);
            device.removeRemotePackage(remotePackagePath);

            // if the installation succeeded, we register it.
            if (installResult) {
               ApkInstallManager.getInstance().registerInstallation(
                       launchInfo.getProject(), launchInfo.getPackageName(), device);
            }
            return installResult;
        }
        catch (IOException e) {
            String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", fileName,
                    device.getSerialNumber(), e.getMessage());
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
        } catch (TimeoutException e) {
            String msg = String.format("Failed to install %1$s on device '%2$s': timeout", fileName,
                    device.getSerialNumber());
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
        } catch (AdbCommandRejectedException e) {
            String msg = String.format(
                    "Failed to install %1$s on device '%2$s': adb rejected install command with: %3$s",
                    fileName, device.getSerialNumber(), e.getMessage());
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
        } catch (CanceledException e) {
            if (e.wasCanceled()) {
                AdtPlugin.printToConsole(launchInfo.getProject(),
                        String.format("Install of %1$s canceled", fileName));
            } else {
                String msg = String.format("Failed to install %1$s on device '%2$s': %3$s",
                        fileName, device.getSerialNumber(), e.getMessage());
                AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
            }
        }

        return false;
    }

    /**
     * For the current launchInfo, create additional DelayedLaunchInfo that should be used to
     * sync APKs that we are dependent on to the device.
     *
     * @param launchInfo the original launch info that we want to find the
     * @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
     */
    public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
        List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();

        // Convert to equivalent JavaProject
        IJavaProject javaProject;
        try {
            //assuming this is an Android (and Java) project since it is attached to the launchInfo.
            javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
        } catch (CoreException e) {
            // return empty dependencies
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
            return dependencies;
        }

        // Get all projects that this depends on
        List<IJavaProject> androidProjectList;
        try {
            androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
        } catch (JavaModelException e) {
            // return empty dependencies
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
            return dependencies;
        }

        // for each project, parse manifest and create launch information
        for (IJavaProject androidProject : androidProjectList) {
            // Parse the Manifest to get various required information
            // copied from LaunchConfigDelegate
            ManifestData manifestData = AndroidManifestHelper.parseForData(
                    androidProject.getProject());

            if (manifestData == null) {
                continue;
            }

            // Get the APK location (can return null)
            IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
            if (apk == null) {
                // getApplicationPackage will have logged an error message
                continue;
            }

            // Create new launchInfo as an hybrid between parent and dependency information
            DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
                    androidProject.getProject(),
                    manifestData.getPackage(),
                    manifestData.getPackage(),
                    launchInfo.getLaunchAction(),
                    apk,
                    manifestData.getDebuggable(),
                    manifestData.getMinSdkVersionString(),
                    launchInfo.getLaunch(),
                    launchInfo.getMonitor());

            // Add to the list
            dependencies.add(delayedLaunchInfo);
        }

        return dependencies;
    }

    /**
     * Installs the application package on the device, and handles return result
     * @param launchInfo The launch information
     * @param remotePath The remote path of the package.
     * @param device The device on which the launch is done.
     */
    private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath,
            final IDevice device) {
        String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName());
        AdtPlugin.printToConsole(launchInfo.getProject(), message);
        try {
            // try a reinstall first, because the most common case is the app is already installed
            String result = doInstall(launchInfo, remotePath, device, true /* reinstall */);

            /* For now we force to retry the install (after uninstalling) because there's no
             * other way around it: adb install does not want to update a package w/o uninstalling
             * the old one first!
             */
            return checkInstallResult(result, device, launchInfo, remotePath,
                    InstallRetryMode.ALWAYS);
        } catch (Exception e) {
            String msg = String.format(
                    "Failed to install %1$s on device '%2$s!",
                    launchInfo.getPackageFile().getName(), device.getSerialNumber());
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e.getMessage());
        }

        return false;
    }

    /**
     * Checks the result of an installation, and takes optional actions based on it.
     * @param result the result string from the installation
     * @param device the device on which the installation occured.
     * @param launchInfo the {@link DelayedLaunchInfo}
     * @param remotePath the temporary path of the package on the device
     * @param retryMode indicates what to do in case, a package already exists.
     * @return <code>true<code> if success, <code>false</code> otherwise.
     * @throws InstallException
     */
    private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo,
            String remotePath, InstallRetryMode retryMode) throws InstallException {
        if (result == null) {
            AdtPlugin.printToConsole(launchInfo.getProject(), "Success!");
            return true;
        }
        else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$
            // this should never happen, since reinstall mode is used on the first attempt
            if (retryMode == InstallRetryMode.PROMPT) {
                boolean prompt = AdtPlugin.displayPrompt("Application Install",
                        "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?");
                if (prompt) {
                    retryMode = InstallRetryMode.ALWAYS;
                } else {
                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                        "Installation error! The package already exists.");
                    return false;
                }
            }

            if (retryMode == InstallRetryMode.ALWAYS) {
                /*
                 * TODO: create a UI that gives the dev the choice to:
                 * - clean uninstall on launch
                 * - full uninstall if application exists.
                 * - soft uninstall if application exists (keeps the app data around).
                 * - always ask (choice of soft-reinstall, full reinstall)
                AdtPlugin.printErrorToConsole(launchInfo.mProject,
                        "Application already exists, uninstalling...");
                String res = doUninstall(device, launchInfo);
                if (res == null) {
                    AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
                } else {
                    AdtPlugin.printErrorToConsole(launchInfo.mProject,
                            String.format("Failed to uninstall: %1$s", res));
                    return false;
                }
                */

                AdtPlugin.printToConsole(launchInfo.getProject(),
                        "Application already exists. Attempting to re-install instead...");
                String res = doInstall(launchInfo, remotePath, device, true /* reinstall */ );
                return checkInstallResult(res, device, launchInfo, remotePath,
                        InstallRetryMode.NEVER);
            }
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                    "Installation error! The package already exists.");
        } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                "Installation failed due to invalid APK file!",
                "Please check logcat output for more details.");
        } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                "Installation failed due to invalid URI!",
                "Please check logcat output for more details.");
        } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                String.format("Installation failed: Could not copy %1$s to its final location!",
                        launchInfo.getPackageFile().getName()),
                "Please check logcat output for more details.");
        } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) {
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                    "Re-installation failed due to different application signatures.",
                    "You must perform a full uninstall of the application. WARNING: This will remove the application data!",
                    String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName()));
        } else {
            AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                String.format("Installation error: %1$s", result),
                "Please check logcat output for more details.");
        }

        return false;
    }

    /**
     * Performs the uninstallation of an application.
     * @param device the device on which to install the application.
     * @param launchInfo the {@link DelayedLaunchInfo}.
     * @return a {@link String} with an error code, or <code>null</code> if success.
     * @throws InstallException if the installation failed.
     */
    @SuppressWarnings("unused")
    private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo)
            throws InstallException {
        try {
            return device.uninstallPackage(launchInfo.getPackageName());
        } catch (InstallException e) {
            String msg = String.format(
                    "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage());
            AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
            throw e;
        }
    }

    /**
     * Performs the installation of an application whose package has been uploaded on the device.
     *
     * @param launchInfo the {@link DelayedLaunchInfo}.
     * @param remotePath the path of the application package in the device tmp folder.
     * @param device the device on which to install the application.
     * @param reinstall
     * @return a {@link String} with an error code, or <code>null</code> if success.
     * @throws InstallException if the uninstallation failed.
     */
    private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath,
            final IDevice device, boolean reinstall) throws InstallException {
        return device.installRemotePackage(remotePath, reinstall);
    }

    /**
     * launches an application on a device or emulator
     *
     * @param info the {@link DelayedLaunchInfo} that indicates the launch action
     * @param device the device or emulator to launch the application on
     */
    public void launchApp(final DelayedLaunchInfo info, IDevice device) {
        if (info.isDebugMode()) {
            synchronized (sListLock) {
                if (mWaitingForDebuggerApplications.contains(info) == false) {
                    mWaitingForDebuggerApplications.add(info);
                }
            }
        }
        if (info.getLaunchAction().doLaunchAction(info, device)) {
            // if the app is not a debug app, we need to do some clean up, as
            // the process is done!
            if (info.isDebugMode() == false) {
                // stop the launch object, since there's no debug, and it can't
                // provide any control over the app
                stopLaunch(info);
            }
        } else {
            // something went wrong or no further launch action needed
            // lets stop the Launch
            stopLaunch(info);
        }
    }

    private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) {

        // split the custom command line in segments
        ArrayList<String> customArgs = new ArrayList<String>();
        boolean hasWipeData = false;
        if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) {
            String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$

            // we need to remove the empty strings
            for (String s : segments) {
                if (s.length() > 0) {
                    customArgs.add(s);
                    if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) {
                        hasWipeData = true;
                    }
                }
            }
        }

        boolean needsWipeData = config.mWipeData && !hasWipeData;
        if (needsWipeData) {
            if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) {
                needsWipeData = false;
            }
        }

        // build the command line based on the available parameters.
        ArrayList<String> list = new ArrayList<String>();

        String path = AdtPlugin.getOsAbsoluteEmulator();

        list.add(path);

        list.add(FLAG_AVD);
        list.add(avdToLaunch.getName());

        if (config.mNetworkSpeed != null) {
            list.add(FLAG_NETSPEED);
            list.add(config.mNetworkSpeed);
        }

        if (config.mNetworkDelay != null) {
            list.add(FLAG_NETDELAY);
            list.add(config.mNetworkDelay);
        }

        if (needsWipeData) {
            list.add(FLAG_WIPE_DATA);
        }

        if (config.mNoBootAnim) {
            list.add(FLAG_NO_BOOT_ANIM);
        }

        list.addAll(customArgs);

        // convert the list into an array for the call to exec.
        String[] command = list.toArray(new String[list.size()]);

        // launch the emulator
        try {
            Process process = Runtime.getRuntime().exec(command);
            grabEmulatorOutput(process);
        } catch (IOException e) {
            return false;
        }

        return true;
    }

    /**
     * Looks for and returns an existing {@link ILaunchConfiguration} object for a
     * specified project.
     * @param manager The {@link ILaunchManager}.
     * @param type The {@link ILaunchConfigurationType}.
     * @param projectName The name of the project
     * @return an existing <code>ILaunchConfiguration</code> object matching the project, or
     *      <code>null</code>.
     */
    private static ILaunchConfiguration findConfig(ILaunchManager manager,
            ILaunchConfigurationType type, String projectName) {
        try {
            ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);

            for (ILaunchConfiguration config : configs) {
                if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
                        "").equals(projectName)) {  //$NON-NLS-1$
                    return config;
                }
            }
        } catch (CoreException e) {
            MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(),
                    "Launch Error", e.getStatus().getMessage());
        }

        // didn't find anything that matches. Return null
        return null;

    }


    /**
     * Connects a remote debugger on the specified port.
     * @param debugPort The port to connect the debugger to
     * @param launch The associated AndroidLaunch object.
     * @param monitor A Progress monitor
     * @return false if cancelled by the monitor
     * @throws CoreException
     */
    @SuppressWarnings("deprecation")
    public static boolean connectRemoteDebugger(int debugPort,
            AndroidLaunch launch, IProgressMonitor monitor)
                throws CoreException {
        // get some default parameters.
        int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);

        HashMap<String, String> newMap = new HashMap<String, String>();

        newMap.put("hostname", "localhost");  //$NON-NLS-1$ //$NON-NLS-2$

        newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$

        newMap.put("timeout", Integer.toString(connectTimeout));

        // get the default VM connector
        IVMConnector connector = JavaRuntime.getDefaultVMConnector();

        // connect to remote VM
        connector.connect(newMap, monitor, launch);

        // check for cancellation
        if (monitor.isCanceled()) {
            IDebugTarget[] debugTargets = launch.getDebugTargets();
            for (IDebugTarget target : debugTargets) {
                if (target.canDisconnect()) {
                    target.disconnect();
                }
            }
            return false;
        }

        return true;
    }

    /**
     * Launch a new thread that connects a remote debugger on the specified port.
     * @param debugPort The port to connect the debugger to
     * @param androidLaunch The associated AndroidLaunch object.
     * @param monitor A Progress monitor
     * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor)
     */
    public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch,
            final IProgressMonitor monitor) {
        new Thread("Debugger connection") { //$NON-NLS-1$
            @Override
            public void run() {
                try {
                    connectRemoteDebugger(debugPort, androidLaunch, monitor);
                } catch (CoreException e) {
                    androidLaunch.stopLaunch();
                }
                monitor.done();
            }
        }.start();
    }

    /**
     * Sent when a new {@link AndroidDebugBridge} is started.
     * <p/>
     * This is sent from a non UI thread.
     * @param bridge the new {@link AndroidDebugBridge} object.
     *
     * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
     */
    public void bridgeChanged(AndroidDebugBridge bridge) {
        // The adb server has changed. We cancel any pending launches.
        String message = "adb server change: cancelling '%1$s'!";
        synchronized (sListLock) {
            for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) {
                AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                    String.format(message, launchInfo.getLaunchAction().getLaunchDescription()));
                stopLaunch(launchInfo);
            }
            for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) {
                AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                        String.format(message,
                                launchInfo.getLaunchAction().getLaunchDescription()));
                stopLaunch(launchInfo);
            }

            mWaitingForReadyEmulatorList.clear();
            mWaitingForDebuggerApplications.clear();
        }
    }

    /**
     * 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)
     */
    public void deviceConnected(IDevice device) {
        synchronized (sListLock) {
            // look if there's an app waiting for a device
            if (mWaitingForEmulatorLaunches.size() > 0) {
                // get/remove first launch item from the list
                // FIXME: what if we have multiple launches waiting?
                DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0);
                mWaitingForEmulatorLaunches.remove(0);

                // give the launch item its device for later use.
                launchInfo.setDevice(device);

                // and move it to the other list
                mWaitingForReadyEmulatorList.add(launchInfo);

                // and tell the user about it
                AdtPlugin.printToConsole(launchInfo.getProject(),
                        String.format("New emulator found: %1$s", device.getSerialNumber()));
                AdtPlugin.printToConsole(launchInfo.getProject(),
                        String.format("Waiting for HOME ('%1$s') to be launched...",
                            AdtPlugin.getDefault().getPreferenceStore().getString(
                                    AdtPrefs.PREFS_HOME_PACKAGE)));
            }
        }
    }

    /**
     * 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)
     */
    @SuppressWarnings("unchecked")
    public void deviceDisconnected(IDevice device) {
        // any pending launch on this device must be canceled.
        String message = "%1$s disconnected! Cancelling '%2$s'!";
        synchronized (sListLock) {
            ArrayList<DelayedLaunchInfo> copyList =
                (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.clone();
            for (DelayedLaunchInfo launchInfo : copyList) {
                if (launchInfo.getDevice() == device) {
                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                            String.format(message, device.getSerialNumber(),
                                    launchInfo.getLaunchAction().getLaunchDescription()));
                    stopLaunch(launchInfo);
                }
            }
            copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone();
            for (DelayedLaunchInfo launchInfo : copyList) {
                if (launchInfo.getDevice() == device) {
                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                            String.format(message, device.getSerialNumber(),
                                    launchInfo.getLaunchAction().getLaunchDescription()));
                    stopLaunch(launchInfo);
                }
            }
        }
    }

    /**
     * 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)
     */
    public void deviceChanged(IDevice device, int changeMask) {
        // We could check if any starting device we care about is now ready, but we can wait for
        // its home app to show up, so...
    }

    /**
     * Sent when an existing client information changed.
     * <p/>
     * This is sent from a non UI thread.
     * @param client the updated client.
     * @param changeMask the bit mask describing the changed properties. It can contain
     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
     *
     * @see IClientChangeListener#clientChanged(Client, int)
     */
    public void clientChanged(final Client client, int changeMask) {
        boolean connectDebugger = false;
        if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
            String applicationName = client.getClientData().getClientDescription();
            if (applicationName != null) {
                IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
                String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE);

                if (home.equals(applicationName)) {

                    // looks like home is up, get its device
                    IDevice device = client.getDevice();

                    // look for application waiting for home
                    synchronized (sListLock) {
                        for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) {
                            DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i);
                            if (launchInfo.getDevice() == device) {
                                // it's match, remove from the list
                                mWaitingForReadyEmulatorList.remove(i);

                                // We couldn't check earlier the API level of the device
                                // (it's asynchronous when the device boot, and usually
                                // deviceConnected is called before it's queried for its build info)
                                // so we check now
                                if (checkBuildInfo(launchInfo, device) == false) {
                                    // device is not the proper API!
                                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                                            "Launch canceled!");
                                    stopLaunch(launchInfo);
                                    return;
                                }

                                AdtPlugin.printToConsole(launchInfo.getProject(),
                                        String.format("HOME is up on device '%1$s'",
                                                device.getSerialNumber()));

                                // attempt to sync the new package onto the device.
                                if (syncApp(launchInfo, device)) {
                                    // application package is sync'ed, lets attempt to launch it.
                                    launchApp(launchInfo, device);
                                } else {
                                    // failure! Cancel and return
                                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                                    "Launch canceled!");
                                    stopLaunch(launchInfo);
                                }

                                break;
                            } else {
                                i++;
                            }
                        }
                    }
                }

                // check if it's already waiting for a debugger, and if so we connect to it.
                if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
                    // search for this client in the list;
                    synchronized (sListLock) {
                        int index = mUnknownClientsWaitingForDebugger.indexOf(client);
                        if (index != -1) {
                            connectDebugger = true;
                            mUnknownClientsWaitingForDebugger.remove(client);
                        }
                    }
                }
            }
        }

        // if it's not home, it could be an app that is now in debugger mode that we're waiting for
        // lets check it

        if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) {
            ClientData clientData = client.getClientData();
            String applicationName = client.getClientData().getClientDescription();
            if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
                // Get the application name, and make sure its valid.
                if (applicationName == null) {
                    // looks like we don't have the client yet, so we keep it around for when its
                    // name becomes available.
                    synchronized (sListLock) {
                        mUnknownClientsWaitingForDebugger.add(client);
                    }
                    return;
                } else {
                    connectDebugger = true;
                }
            }
        }

        if (connectDebugger) {
            Log.d("adt", "Debugging " + client);
            // now check it against the apps waiting for a debugger
            String applicationName = client.getClientData().getClientDescription();
            Log.d("adt", "App Name: " + applicationName);
            synchronized (sListLock) {
                for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) {
                    final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i);
                    if (client.getDevice() == launchInfo.getDevice() &&
                            applicationName.equals(launchInfo.getDebugPackageName())) {
                        // this is a match. We remove the launch info from the list
                        mWaitingForDebuggerApplications.remove(i);

                        // and connect the debugger.
                        String msg = String.format(
                                "Attempting to connect debugger to '%1$s' on port %2$d",
                                launchInfo.getDebugPackageName(), client.getDebuggerListenPort());
                        AdtPlugin.printToConsole(launchInfo.getProject(), msg);

                        new Thread("Debugger Connection") { //$NON-NLS-1$
                            @Override
                            public void run() {
                                try {
                                    if (connectRemoteDebugger(
                                            client.getDebuggerListenPort(),
                                            launchInfo.getLaunch(),
                                            launchInfo.getMonitor()) == false) {
                                        return;
                                    }
                                } catch (CoreException e) {
                                    // well something went wrong.
                                    AdtPlugin.printErrorToConsole(launchInfo.getProject(),
                                            String.format("Launch error: %s", e.getMessage()));
                                    // stop the launch
                                    stopLaunch(launchInfo);
                                }

                                launchInfo.getMonitor().done();
                            }
                        }.start();

                        // we're done processing this client.
                        return;

                    } else {
                        i++;
                    }
                }
            }

            // if we get here, we haven't found an app that we were launching, so we look
            // for opened android projects that contains the app asking for a debugger.
            // If we find one, we automatically connect to it.
            IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);

            if (project != null) {
                debugRunningApp(project, client.getDebuggerListenPort());
            }
        }
    }

    /**
     * Get the stderr/stdout outputs of a process and return when the process is done.
     * Both <b>must</b> be read or the process will block on windows.
     * @param process The process to get the output from
     */
    private void grabEmulatorOutput(final Process process) {
        // read the lines as they come. if null is returned, it's
        // because the process finished
        new Thread("") { //$NON-NLS-1$
            @Override
            public void run() {
                // create a buffer to read the stderr output
                InputStreamReader is = new InputStreamReader(process.getErrorStream());
                BufferedReader errReader = new BufferedReader(is);

                try {
                    while (true) {
                        String line = errReader.readLine();
                        if (line != null) {
                            AdtPlugin.printErrorToConsole("Emulator", line);
                        } else {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                }
            }
        }.start();

        new Thread("") { //$NON-NLS-1$
            @Override
            public void run() {
                InputStreamReader is = new InputStreamReader(process.getInputStream());
                BufferedReader outReader = new BufferedReader(is);

                try {
                    while (true) {
                        String line = outReader.readLine();
                        if (line != null) {
                            AdtPlugin.printToConsole("Emulator", line);
                        } else {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                }
            }
        }.start();
    }

    /* (non-Javadoc)
     * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo)
     */
    public void stopLaunch(DelayedLaunchInfo launchInfo) {
        launchInfo.getLaunch().stopLaunch();
        synchronized (sListLock) {
            mWaitingForReadyEmulatorList.remove(launchInfo);
            mWaitingForDebuggerApplications.remove(launchInfo);
        }
    }
}
