Home | History | Annotate | Download | only in launch
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.launch;
     18 
     19 import com.android.ddmlib.AdbCommandRejectedException;
     20 import com.android.ddmlib.AndroidDebugBridge;
     21 import com.android.ddmlib.CanceledException;
     22 import com.android.ddmlib.Client;
     23 import com.android.ddmlib.ClientData;
     24 import com.android.ddmlib.IDevice;
     25 import com.android.ddmlib.InstallException;
     26 import com.android.ddmlib.Log;
     27 import com.android.ddmlib.TimeoutException;
     28 import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
     29 import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
     30 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
     31 import com.android.ddmlib.ClientData.DebuggerStatus;
     32 import com.android.ide.eclipse.adt.AdtPlugin;
     33 import com.android.ide.eclipse.adt.internal.actions.AvdManagerAction;
     34 import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
     35 import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode;
     36 import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse;
     37 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     38 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
     39 import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
     40 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     41 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     42 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     43 import com.android.ide.eclipse.ddms.DdmsPlugin;
     44 import com.android.ide.eclipse.ddms.views.LogCatView;
     45 import com.android.prefs.AndroidLocation.AndroidLocationException;
     46 import com.android.sdklib.AndroidVersion;
     47 import com.android.sdklib.IAndroidTarget;
     48 import com.android.sdklib.NullSdkLog;
     49 import com.android.sdklib.internal.avd.AvdInfo;
     50 import com.android.sdklib.internal.avd.AvdManager;
     51 import com.android.sdklib.xml.ManifestData;
     52 
     53 import org.eclipse.core.resources.IFile;
     54 import org.eclipse.core.resources.IProject;
     55 import org.eclipse.core.resources.IResource;
     56 import org.eclipse.core.runtime.CoreException;
     57 import org.eclipse.core.runtime.IPath;
     58 import org.eclipse.core.runtime.IProgressMonitor;
     59 import org.eclipse.debug.core.DebugPlugin;
     60 import org.eclipse.debug.core.ILaunchConfiguration;
     61 import org.eclipse.debug.core.ILaunchConfigurationType;
     62 import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
     63 import org.eclipse.debug.core.ILaunchManager;
     64 import org.eclipse.debug.core.model.IDebugTarget;
     65 import org.eclipse.debug.ui.DebugUITools;
     66 import org.eclipse.jdt.core.IJavaProject;
     67 import org.eclipse.jdt.core.JavaModelException;
     68 import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
     69 import org.eclipse.jdt.launching.IVMConnector;
     70 import org.eclipse.jdt.launching.JavaRuntime;
     71 import org.eclipse.jface.dialogs.Dialog;
     72 import org.eclipse.jface.dialogs.MessageDialog;
     73 import org.eclipse.jface.preference.IPreferenceStore;
     74 import org.eclipse.swt.widgets.Display;
     75 import org.eclipse.swt.widgets.Shell;
     76 
     77 import java.io.BufferedReader;
     78 import java.io.IOException;
     79 import java.io.InputStreamReader;
     80 import java.util.ArrayList;
     81 import java.util.HashMap;
     82 import java.util.List;
     83 import java.util.Map.Entry;
     84 
     85 /**
     86  * Controls the launch of Android application either on a device or on the
     87  * emulator. If an emulator is already running, this class will attempt to reuse
     88  * it.
     89  */
     90 public final class AndroidLaunchController implements IDebugBridgeChangeListener,
     91         IDeviceChangeListener, IClientChangeListener, ILaunchController {
     92 
     93     private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$
     94     private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$
     95     private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$
     96     private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$
     97     private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$
     98 
     99     /**
    100      * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection
    101      * to running application. The integer is the port on which to connect.
    102      * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
    103      */
    104     private static final HashMap<ILaunchConfiguration, Integer> sRunningAppMap =
    105         new HashMap<ILaunchConfiguration, Integer>();
    106 
    107     private static final Object sListLock = sRunningAppMap;
    108 
    109     /**
    110      * List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
    111      * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
    112      * DelayedLaunchInfo object is moved to
    113      * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
    114      * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
    115      */
    116     private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
    117         new ArrayList<DelayedLaunchInfo>();
    118 
    119     /**
    120      * List of application waiting to be launched on a device/emulator.<br>
    121      * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
    122      * */
    123     private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList =
    124         new ArrayList<DelayedLaunchInfo>();
    125 
    126     /**
    127      * Application waiting to show up as waiting for debugger.
    128      * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
    129      */
    130     private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications =
    131         new ArrayList<DelayedLaunchInfo>();
    132 
    133     /**
    134      * List of clients that have appeared as waiting for debugger before their name was available.
    135      * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
    136      */
    137     private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>();
    138 
    139     /** static instance for singleton */
    140     private static AndroidLaunchController sThis = new AndroidLaunchController();
    141 
    142     /** private constructor to enforce singleton */
    143     private AndroidLaunchController() {
    144         AndroidDebugBridge.addDebugBridgeChangeListener(this);
    145         AndroidDebugBridge.addDeviceChangeListener(this);
    146         AndroidDebugBridge.addClientChangeListener(this);
    147     }
    148 
    149     /**
    150      * Returns the singleton reference.
    151      */
    152     public static AndroidLaunchController getInstance() {
    153         return sThis;
    154     }
    155 
    156 
    157     /**
    158      * Launches a remote java debugging session on an already running application
    159      * @param project The project of the application to debug.
    160      * @param debugPort The port to connect the debugger to.
    161      */
    162     public static void debugRunningApp(IProject project, int debugPort) {
    163         // get an existing or new launch configuration
    164         ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project);
    165 
    166         if (config != null) {
    167             setPortLaunchConfigAssociation(config, debugPort);
    168 
    169             // and launch
    170             DebugUITools.launch(config, ILaunchManager.DEBUG_MODE);
    171         }
    172     }
    173 
    174     /**
    175      * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}.
    176      * @param project the project
    177      * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was
    178      * an error when creating a new one.
    179      */
    180     public static ILaunchConfiguration getLaunchConfig(IProject project) {
    181         // get the launch manager
    182         ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
    183 
    184         // now get the config type for our particular android type.
    185         ILaunchConfigurationType configType = manager.getLaunchConfigurationType(
    186                         LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
    187 
    188         String name = project.getName();
    189 
    190         // search for an existing launch configuration
    191         ILaunchConfiguration config = findConfig(manager, configType, name);
    192 
    193         // test if we found one or not
    194         if (config == null) {
    195             // Didn't find a matching config, so we make one.
    196             // It'll be made in the "working copy" object first.
    197             ILaunchConfigurationWorkingCopy wc = null;
    198 
    199             try {
    200                 // make the working copy object
    201                 wc = configType.newInstance(null,
    202                         manager.generateUniqueLaunchConfigurationNameFrom(name));
    203 
    204                 // set the project name
    205                 wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
    206 
    207                 // set the launch mode to default.
    208                 wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
    209                         LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
    210 
    211                 // set default target mode
    212                 wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
    213                         LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());
    214 
    215                 // default AVD: None
    216                 wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);
    217 
    218                 // set the default network speed
    219                 wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
    220                         LaunchConfigDelegate.DEFAULT_SPEED);
    221 
    222                 // and delay
    223                 wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
    224                         LaunchConfigDelegate.DEFAULT_DELAY);
    225 
    226                 // default wipe data mode
    227                 wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
    228                         LaunchConfigDelegate.DEFAULT_WIPE_DATA);
    229 
    230                 // default disable boot animation option
    231                 wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
    232                         LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
    233 
    234                 // set default emulator options
    235                 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
    236                 String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS);
    237                 wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
    238 
    239                 // map the config and the project
    240                 wc.setMappedResources(getResourcesToMap(project));
    241 
    242                 // save the working copy to get the launch config object which we return.
    243                 return wc.doSave();
    244 
    245             } catch (CoreException e) {
    246                 String msg = String.format(
    247                         "Failed to create a Launch config for project '%1$s': %2$s",
    248                         project.getName(), e.getMessage());
    249                 AdtPlugin.printErrorToConsole(project, msg);
    250 
    251                 // no launch!
    252                 return null;
    253             }
    254         }
    255 
    256         return config;
    257     }
    258 
    259     /**
    260      * Returns the list of resources to map to a Launch Configuration.
    261      * @param project the project associated to the launch configuration.
    262      */
    263     public static IResource[] getResourcesToMap(IProject project) {
    264         ArrayList<IResource> array = new ArrayList<IResource>(2);
    265         array.add(project);
    266 
    267         IFile manifest = ProjectHelper.getManifest(project);
    268         if (manifest != null) {
    269             array.add(manifest);
    270         }
    271 
    272         return array.toArray(new IResource[array.size()]);
    273     }
    274 
    275     /**
    276      * Launches an android app on the device or emulator
    277      *
    278      * @param project The project we're launching
    279      * @param mode the mode in which to launch, one of the mode constants
    280      *      defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or
    281      *      <code>DEBUG_MODE</code>.
    282      * @param apk the resource to the apk to launch.
    283      * @param packageName the Android package name of the app
    284      * @param debugPackageName the Android package name to debug
    285      * @param debuggable the debuggable value of the app's manifest, or null if not set.
    286      * @param requiredApiVersionNumber the api version required by the app, or null if none.
    287      * @param launchAction the action to perform after app sync
    288      * @param config the launch configuration
    289      * @param launch the launch object
    290      */
    291     public void launch(final IProject project, String mode, IFile apk,
    292             String packageName, String debugPackageName, Boolean debuggable,
    293             String requiredApiVersionNumber, final IAndroidLaunchAction launchAction,
    294             final AndroidLaunchConfiguration config, final AndroidLaunch launch,
    295             IProgressMonitor monitor) {
    296 
    297         String message = String.format("Performing %1$s", launchAction.getLaunchDescription());
    298         AdtPlugin.printToConsole(project, message);
    299 
    300         // create the launch info
    301         final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName,
    302                 debugPackageName, launchAction, apk, debuggable, requiredApiVersionNumber, launch,
    303                 monitor);
    304 
    305         // set the debug mode
    306         launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE));
    307 
    308         // get the SDK
    309         Sdk currentSdk = Sdk.getCurrent();
    310         AvdManager avdManager = currentSdk.getAvdManager();
    311 
    312         // reload the AVDs to make sure we are up to date
    313         try {
    314             avdManager.reloadAvds(NullSdkLog.getLogger());
    315         } catch (AndroidLocationException e1) {
    316             // this happens if the AVD Manager failed to find the folder in which the AVDs are
    317             // stored. This is unlikely to happen, but if it does, we should force to go manual
    318             // to allow using physical devices.
    319             config.mTargetMode = TargetMode.MANUAL;
    320         }
    321 
    322         // get the project target
    323         IAndroidTarget projectTarget = currentSdk.getTarget(project);
    324 
    325         // FIXME: check errors on missing sdk, AVD manager, or project target.
    326 
    327         // device chooser response object.
    328         final DeviceChooserResponse response = new DeviceChooserResponse();
    329 
    330         /*
    331          * Launch logic:
    332          * - Manually Mode
    333          *       Always display a UI that lets a user see the current running emulators/devices.
    334          *       The UI must show which devices are compatibles, and allow launching new emulators
    335          *       with compatible (and not yet running) AVD.
    336          * - Automatic Way
    337          *     * Preferred AVD set.
    338          *           If Preferred AVD is not running: launch it.
    339          *           Launch the application on the preferred AVD.
    340          *     * No preferred AVD.
    341          *           Count the number of compatible emulators/devices.
    342          *           If != 1, display a UI similar to manual mode.
    343          *           If == 1, launch the application on this AVD/device.
    344          */
    345 
    346         if (config.mTargetMode == TargetMode.AUTO) {
    347             // if we are in automatic target mode, we need to find the current devices
    348             IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
    349 
    350             // first check if we have a preferred AVD name, and if it actually exists, and is valid
    351             // (ie able to run the project).
    352             // We need to check this in case the AVD was recreated with a different target that is
    353             // not compatible.
    354             AvdInfo preferredAvd = null;
    355             if (config.mAvdName != null) {
    356                 preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/);
    357                 if (projectTarget.canRunOn(preferredAvd.getTarget()) == false) {
    358                     preferredAvd = null;
    359 
    360                     AdtPlugin.printErrorToConsole(project, String.format(
    361                             "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...",
    362                             config.mAvdName, projectTarget.getName()));
    363                 }
    364             }
    365 
    366             if (preferredAvd != null) {
    367                 // look for a matching device
    368 
    369                 for (IDevice d : devices) {
    370                     String deviceAvd = d.getAvdName();
    371                     if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) {
    372                         response.setDeviceToUse(d);
    373 
    374                         AdtPlugin.printToConsole(project, String.format(
    375                                 "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'",
    376                                 config.mAvdName, d));
    377 
    378                         continueLaunch(response, project, launch, launchInfo, config);
    379                         return;
    380                     }
    381                 }
    382 
    383                 // at this point we have a valid preferred AVD that is not running.
    384                 // We need to start it.
    385                 response.setAvdToLaunch(preferredAvd);
    386 
    387                 AdtPlugin.printToConsole(project, String.format(
    388                         "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.",
    389                         config.mAvdName));
    390 
    391                 continueLaunch(response, project, launch, launchInfo, config);
    392                 return;
    393             }
    394 
    395             // no (valid) preferred AVD? look for one.
    396 
    397             // If the API level requested in the manifest is lower than the current project
    398             // target, when we will iterate devices/avds later ideally we will want to find
    399             // a device/avd which target is as close to the manifest as possible (instead of
    400             // a device which target is the same as the project's target) and use it as the
    401             // new default.
    402 
    403             int reqApiLevel = 0;
    404             try {
    405                 reqApiLevel = Integer.parseInt(requiredApiVersionNumber);
    406 
    407                 if (reqApiLevel > 0 && reqApiLevel < projectTarget.getVersion().getApiLevel()) {
    408                     int maxDist = projectTarget.getVersion().getApiLevel() - reqApiLevel;
    409                     IAndroidTarget candidate = null;
    410 
    411                     for (IAndroidTarget target : currentSdk.getTargets()) {
    412                         if (target.canRunOn(projectTarget)) {
    413                             int currDist = target.getVersion().getApiLevel() - reqApiLevel;
    414                             if (currDist >= 0 && currDist < maxDist) {
    415                                 maxDist = currDist;
    416                                 candidate = target;
    417                                 if (maxDist == 0) {
    418                                     // Found a perfect match
    419                                     break;
    420                                 }
    421                             }
    422                         }
    423                     }
    424 
    425                     if (candidate != null) {
    426                         // We found a better SDK target candidate, that is closer to the
    427                         // API level from minSdkVersion than the one currently used by the
    428                         // project. Below (in the for...devices loop) we'll try to find
    429                         // a device/AVD for it.
    430                         projectTarget = candidate;
    431                     }
    432                 }
    433             } catch (NumberFormatException e) {
    434                 // pass
    435             }
    436 
    437             HashMap<IDevice, AvdInfo> compatibleRunningAvds = new HashMap<IDevice, AvdInfo>();
    438             boolean hasDevice = false; // if there's 1+ device running, we may force manual mode,
    439                                        // as we cannot always detect proper compatibility with
    440                                        // devices. This is the case if the project target is not
    441                                        // a standard platform
    442             for (IDevice d : devices) {
    443                 String deviceAvd = d.getAvdName();
    444                 if (deviceAvd != null) { // physical devices return null.
    445                     AvdInfo info = avdManager.getAvd(deviceAvd, true /*validAvdOnly*/);
    446                     if (info != null && projectTarget.canRunOn(info.getTarget())) {
    447                         compatibleRunningAvds.put(d, info);
    448                     }
    449                 } else {
    450                     if (projectTarget.isPlatform()) { // means this can run on any device as long
    451                                                       // as api level is high enough
    452                         AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
    453                         // the deviceVersion may be null if it wasn't yet queried (device just
    454                         // plugged in or emulator just booting up.
    455                         if (deviceVersion != null &&
    456                                 deviceVersion.canRun(projectTarget.getVersion())) {
    457                             // device is compatible with project
    458                             compatibleRunningAvds.put(d, null);
    459                             continue;
    460                         }
    461                     } else {
    462                         // for non project platform, we can't be sure if a device can
    463                         // run an application or not, since we don't query the device
    464                         // for the list of optional libraries that it supports.
    465                     }
    466                     hasDevice = true;
    467                 }
    468             }
    469 
    470             // depending on the number of devices, we'll simulate an automatic choice
    471             // from the device chooser or simply show up the device chooser.
    472             if (hasDevice == false && compatibleRunningAvds.size() == 0) {
    473                 // if zero emulators/devices, we launch an emulator.
    474                 // We need to figure out which AVD first.
    475 
    476                 // we are going to take the closest AVD. ie a compatible AVD that has the API level
    477                 // closest to the project target.
    478                 AvdInfo defaultAvd = findMatchingAvd(avdManager, projectTarget);
    479 
    480                 if (defaultAvd != null) {
    481                     response.setAvdToLaunch(defaultAvd);
    482 
    483                     AdtPlugin.printToConsole(project, String.format(
    484                             "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'",
    485                             defaultAvd.getName()));
    486 
    487                     continueLaunch(response, project, launch, launchInfo, config);
    488                     return;
    489                 } else {
    490                     AdtPlugin.printToConsole(project, String.format(
    491                             "Failed to find an AVD compatible with target '%1$s'.",
    492                             projectTarget.getName()));
    493 
    494                     final Display display = AdtPlugin.getDisplay();
    495                     final boolean[] searchAgain = new boolean[] { false };
    496                     // ask the user to create a new one.
    497                     display.syncExec(new Runnable() {
    498                         public void run() {
    499                             Shell shell = display.getActiveShell();
    500                             if (MessageDialog.openQuestion(shell, "Android AVD Error",
    501                                     "No compatible targets were found. Do you wish to a add new Android Virtual Device?")) {
    502                                 AvdManagerAction action = new AvdManagerAction();
    503                                 action.run(null /*action*/);
    504                                 searchAgain[0] = true;
    505                             }
    506                         }
    507                     });
    508                     if (searchAgain[0]) {
    509                         // attempt to reload the AVDs and find one compatible.
    510                         defaultAvd = findMatchingAvd(avdManager, projectTarget);
    511 
    512                         if (defaultAvd == null) {
    513                             AdtPlugin.printErrorToConsole(project, String.format(
    514                                     "Still no compatible AVDs with target '%1$s': Aborting launch.",
    515                                     projectTarget.getName()));
    516                             stopLaunch(launchInfo);
    517                         } else {
    518                             response.setAvdToLaunch(defaultAvd);
    519 
    520                             AdtPlugin.printToConsole(project, String.format(
    521                                     "Launching new emulator with compatible AVD '%1$s'",
    522                                     defaultAvd.getName()));
    523 
    524                             continueLaunch(response, project, launch, launchInfo, config);
    525                             return;
    526                         }
    527                     }
    528                 }
    529             } else if (hasDevice == false && compatibleRunningAvds.size() == 1) {
    530                 Entry<IDevice, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next();
    531                 response.setDeviceToUse(e.getKey());
    532 
    533                 // get the AvdInfo, if null, the device is a physical device.
    534                 AvdInfo avdInfo = e.getValue();
    535                 if (avdInfo != null) {
    536                     message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'",
    537                             response.getDeviceToUse(), e.getValue().getName());
    538                 } else {
    539                     message = String.format("Automatic Target Mode: using device '%1$s'",
    540                             response.getDeviceToUse());
    541                 }
    542                 AdtPlugin.printToConsole(project, message);
    543 
    544                 continueLaunch(response, project, launch, launchInfo, config);
    545                 return;
    546             }
    547 
    548             // if more than one device, we'll bring up the DeviceChooser dialog below.
    549             if (compatibleRunningAvds.size() >= 2) {
    550                 message = "Automatic Target Mode: Several compatible targets. Please select a target device.";
    551             } else if (hasDevice) {
    552                 message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device.";
    553             }
    554 
    555             AdtPlugin.printToConsole(project, message);
    556         }
    557 
    558         // bring up the device chooser.
    559         final IAndroidTarget desiredProjectTarget = projectTarget;
    560         AdtPlugin.getDisplay().asyncExec(new Runnable() {
    561             public void run() {
    562                 try {
    563                     // open the chooser dialog. It'll fill 'response' with the device to use
    564                     // or the AVD to launch.
    565                     DeviceChooserDialog dialog = new DeviceChooserDialog(
    566                             AdtPlugin.getDisplay().getActiveShell(),
    567                             response, launchInfo.getPackageName(), desiredProjectTarget);
    568                     if (dialog.open() == Dialog.OK) {
    569                         AndroidLaunchController.this.continueLaunch(response, project, launch,
    570                                 launchInfo, config);
    571                     } else {
    572                         AdtPlugin.printErrorToConsole(project, "Launch canceled!");
    573                         stopLaunch(launchInfo);
    574                         return;
    575                     }
    576                 } catch (Exception e) {
    577                     // there seems to be some case where the shell will be null. (might be
    578                     // an OS X bug). Because of this the creation of the dialog will throw
    579                     // and IllegalArg exception interrupting the launch with no user feedback.
    580                     // So we trap all the exception and display something.
    581                     String msg = e.getMessage();
    582                     if (msg == null) {
    583                         msg = e.getClass().getCanonicalName();
    584                     }
    585                     AdtPlugin.printErrorToConsole(project,
    586                             String.format("Error during launch: %s", msg));
    587                     stopLaunch(launchInfo);
    588                 }
    589             }
    590         });
    591     }
    592 
    593     /**
    594      * Find a matching AVD.
    595      */
    596     private AvdInfo findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget) {
    597         AvdInfo[] avds = avdManager.getValidAvds();
    598         AvdInfo defaultAvd = null;
    599         for (AvdInfo avd : avds) {
    600             if (projectTarget.canRunOn(avd.getTarget())) {
    601                 // at this point we can ignore the code name issue since
    602                 // IAndroidTarget.canRunOn() will already have filtered the non
    603                 // compatible AVDs.
    604                 if (defaultAvd == null ||
    605                         avd.getTarget().getVersion().getApiLevel() <
    606                             defaultAvd.getTarget().getVersion().getApiLevel()) {
    607                     defaultAvd = avd;
    608                 }
    609             }
    610         }
    611         return defaultAvd;
    612     }
    613 
    614     /**
    615      * Continues the launch based on the DeviceChooser response.
    616      * @param response the device chooser response
    617      * @param project The project being launched
    618      * @param launch The eclipse launch info
    619      * @param launchInfo The {@link DelayedLaunchInfo}
    620      * @param config The config needed to start a new emulator.
    621      */
    622     void continueLaunch(final DeviceChooserResponse response, final IProject project,
    623             final AndroidLaunch launch, final DelayedLaunchInfo launchInfo,
    624             final AndroidLaunchConfiguration config) {
    625 
    626         // Since this is called from the UI thread we spawn a new thread
    627         // to finish the launch.
    628         new Thread() {
    629             @Override
    630             public void run() {
    631                 if (response.getAvdToLaunch() != null) {
    632                     // there was no selected device, we start a new emulator.
    633                     synchronized (sListLock) {
    634                         AvdInfo info = response.getAvdToLaunch();
    635                         mWaitingForEmulatorLaunches.add(launchInfo);
    636                         AdtPlugin.printToConsole(project, String.format(
    637                                 "Launching a new emulator with Virtual Device '%1$s'",
    638                                 info.getName()));
    639                         boolean status = launchEmulator(config, info);
    640 
    641                         if (status == false) {
    642                             // launching the emulator failed!
    643                             AdtPlugin.displayError("Emulator Launch",
    644                                     "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing.");
    645 
    646                             // stop the launch and return
    647                             mWaitingForEmulatorLaunches.remove(launchInfo);
    648                             AdtPlugin.printErrorToConsole(project, "Launch canceled!");
    649                             stopLaunch(launchInfo);
    650                             return;
    651                         }
    652 
    653                         return;
    654                     }
    655                 } else if (response.getDeviceToUse() != null) {
    656                     launchInfo.setDevice(response.getDeviceToUse());
    657                     simpleLaunch(launchInfo, launchInfo.getDevice());
    658                 }
    659             }
    660         }.start();
    661     }
    662 
    663     /**
    664      * Queries for a debugger port for a specific {@link ILaunchConfiguration}.
    665      * <p/>
    666      * If the configuration and a debugger port where added through
    667      * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method
    668      * will return the debugger port, and remove the configuration from the list.
    669      * @param launchConfig the {@link ILaunchConfiguration}
    670      * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the
    671      * configuration was not setup.
    672      */
    673     static int getPortForConfig(ILaunchConfiguration launchConfig) {
    674         synchronized (sListLock) {
    675             Integer port = sRunningAppMap.get(launchConfig);
    676             if (port != null) {
    677                 sRunningAppMap.remove(launchConfig);
    678                 return port;
    679             }
    680         }
    681 
    682         return LaunchConfigDelegate.INVALID_DEBUG_PORT;
    683     }
    684 
    685     /**
    686      * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of
    687      * launch config to connect directly to a running app instead of doing full launch (sync,
    688      * launch, and connect to).
    689      * @param launchConfig the {@link ILaunchConfiguration} object.
    690      * @param port The debugger port to connect to.
    691      */
    692     private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig,
    693             int port) {
    694         synchronized (sListLock) {
    695             sRunningAppMap.put(launchConfig, port);
    696         }
    697     }
    698 
    699     /**
    700      * Checks the build information, and returns whether the launch should continue.
    701      * <p/>The value tested are:
    702      * <ul>
    703      * <li>Minimum API version requested by the application. If the target device does not match,
    704      * the launch is canceled.</li>
    705      * <li>Debuggable attribute of the application and whether or not the device requires it. If
    706      * the device requires it and it is not set in the manifest, the launch will be forced to
    707      * "release" mode instead of "debug"</li>
    708      * <ul>
    709      */
    710     private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) {
    711         if (device != null) {
    712             // check the app required API level versus the target device API level
    713 
    714             String deviceVersion = device.getProperty(IDevice.PROP_BUILD_VERSION);
    715             String deviceApiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
    716             String deviceCodeName = device.getProperty(IDevice.PROP_BUILD_CODENAME);
    717 
    718             int deviceApiLevel = -1;
    719             try {
    720                 deviceApiLevel = Integer.parseInt(deviceApiLevelString);
    721             } catch (NumberFormatException e) {
    722                 // pass, we'll keep the apiLevel value at -1.
    723             }
    724 
    725             String requiredApiString = launchInfo.getRequiredApiVersionNumber();
    726             if (requiredApiString != null) {
    727                 int requiredApi = -1;
    728                 try {
    729                     requiredApi = Integer.parseInt(requiredApiString);
    730                 } catch (NumberFormatException e) {
    731                     // pass, we'll keep requiredApi value at -1.
    732                 }
    733 
    734                 if (requiredApi == -1) {
    735                     // this means the manifest uses a codename for minSdkVersion
    736                     // check that the device is using the same codename
    737                     if (requiredApiString.equals(deviceCodeName) == false) {
    738                         AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
    739                             "ERROR: Application requires a device running '%1$s'!",
    740                             requiredApiString));
    741                         return false;
    742                     }
    743                 } else {
    744                     // app requires a specific API level
    745                     if (deviceApiLevel == -1) {
    746                         AdtPlugin.printToConsole(launchInfo.getProject(),
    747                                 "WARNING: Unknown device API version!");
    748                     } else if (deviceApiLevel < requiredApi) {
    749                         String msg = String.format(
    750                                 "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).",
    751                                 requiredApi, deviceApiLevel, deviceVersion);
    752                         AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
    753 
    754                         // abort the launch
    755                         return false;
    756                     }
    757                 }
    758             } else {
    759                 // warn the application API level requirement is not set.
    760                 AdtPlugin.printErrorToConsole(launchInfo.getProject(),
    761                         "WARNING: Application does not specify an API level requirement!");
    762 
    763                 // and display the target device API level (if known)
    764                 if (deviceApiLevel == -1) {
    765                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
    766                             "WARNING: Unknown device API version!");
    767                 } else {
    768                     AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
    769                             "Device API version is %1$d (Android %2$s)", deviceApiLevel,
    770                             deviceVersion));
    771                 }
    772             }
    773 
    774             // now checks that the device/app can be debugged (if needed)
    775             if (device.isEmulator() == false && launchInfo.isDebugMode()) {
    776                 String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE);
    777                 if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$
    778                     // the device is "secure" and requires apps to declare themselves as debuggable!
    779                     // launchInfo.getDebuggable() will return null if the manifest doesn't declare
    780                     // anything. In this case this is fine since the build system does insert
    781                     // debuggable=true. The only case to look for is if false is manually set
    782                     // in the manifest.
    783                     if (launchInfo.getDebuggable() == Boolean.FALSE) {
    784                         String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.",
    785                                 launchInfo.getPackageName());
    786                         AdtPlugin.printErrorToConsole(launchInfo.getProject(), message);
    787 
    788                         // because am -D does not check for ro.debuggable and the
    789                         // 'debuggable' attribute, it is important we do not use the -D option
    790                         // in this case or the app will wait for a debugger forever and never
    791                         // really launch.
    792                         launchInfo.setDebugMode(false);
    793                     }
    794                 }
    795             }
    796         }
    797 
    798         return true;
    799     }
    800 
    801     /**
    802      * Do a simple launch on the specified device, attempting to sync the new
    803      * package, and then launching the application. Failed sync/launch will
    804      * stop the current AndroidLaunch and return false;
    805      * @param launchInfo
    806      * @param device
    807      * @return true if succeed
    808      */
    809     private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) {
    810         // API level check
    811         if (checkBuildInfo(launchInfo, device) == false) {
    812             AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
    813             stopLaunch(launchInfo);
    814             return false;
    815         }
    816 
    817         // sync the app
    818         if (syncApp(launchInfo, device) == false) {
    819             AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
    820             stopLaunch(launchInfo);
    821             return false;
    822         }
    823 
    824         // launch the app
    825         launchApp(launchInfo, device);
    826 
    827         return true;
    828     }
    829 
    830 
    831     /**
    832      * If needed, syncs the application and all its dependencies on the device/emulator.
    833      *
    834      * @param launchInfo The Launch information object.
    835      * @param device the device on which to sync the application
    836      * @return true if the install succeeded.
    837      */
    838     private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) {
    839         boolean alreadyInstalled = ApkInstallManager.getInstance().isApplicationInstalled(
    840                 launchInfo.getProject(), launchInfo.getPackageName(), device);
    841 
    842         if (alreadyInstalled) {
    843             AdtPlugin.printToConsole(launchInfo.getProject(),
    844             "Application already deployed. No need to reinstall.");
    845         } else {
    846             if (doSyncApp(launchInfo, device) == false) {
    847                 return false;
    848             }
    849         }
    850 
    851         // The app is now installed, now try the dependent projects
    852         for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
    853             String msg = String.format("Project dependency found, installing: %s",
    854                     dependentLaunchInfo.getProject().getName());
    855             AdtPlugin.printToConsole(launchInfo.getProject(), msg);
    856             if (syncApp(dependentLaunchInfo, device) == false) {
    857                 return false;
    858             }
    859         }
    860 
    861         return true;
    862     }
    863 
    864     /**
    865      * Syncs the application on the device/emulator.
    866      *
    867      * @param launchInfo The Launch information object.
    868      * @param device the device on which to sync the application
    869      * @return true if the install succeeded.
    870      */
    871     private boolean doSyncApp(DelayedLaunchInfo launchInfo, IDevice device) {
    872         IPath path = launchInfo.getPackageFile().getLocation();
    873         String fileName = path.lastSegment();
    874         try {
    875             String message = String.format("Uploading %1$s onto device '%2$s'",
    876                     fileName, device.getSerialNumber());
    877             AdtPlugin.printToConsole(launchInfo.getProject(), message);
    878 
    879             String remotePackagePath = device.syncPackageToDevice(path.toOSString());
    880             boolean installResult = installPackage(launchInfo, remotePackagePath, device);
    881             device.removeRemotePackage(remotePackagePath);
    882 
    883             // if the installation succeeded, we register it.
    884             if (installResult) {
    885                ApkInstallManager.getInstance().registerInstallation(
    886                        launchInfo.getProject(), launchInfo.getPackageName(), device);
    887             }
    888             return installResult;
    889         }
    890         catch (IOException e) {
    891             String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", fileName,
    892                     device.getSerialNumber(), e.getMessage());
    893             AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
    894         } catch (TimeoutException e) {
    895             String msg = String.format("Failed to install %1$s on device '%2$s': timeout", fileName,
    896                     device.getSerialNumber());
    897             AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
    898         } catch (AdbCommandRejectedException e) {
    899             String msg = String.format(
    900                     "Failed to install %1$s on device '%2$s': adb rejected install command with: %3$s",
    901                     fileName, device.getSerialNumber(), e.getMessage());
    902             AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
    903         } catch (CanceledException e) {
    904             if (e.wasCanceled()) {
    905                 AdtPlugin.printToConsole(launchInfo.getProject(),
    906                         String.format("Install of %1$s canceled", fileName));
    907             } else {
    908                 String msg = String.format("Failed to install %1$s on device '%2$s': %3$s",
    909                         fileName, device.getSerialNumber(), e.getMessage());
    910                 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
    911             }
    912         }
    913 
    914         return false;
    915     }
    916 
    917     /**
    918      * For the current launchInfo, create additional DelayedLaunchInfo that should be used to
    919      * sync APKs that we are dependent on to the device.
    920      *
    921      * @param launchInfo the original launch info that we want to find the
    922      * @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
    923      */
    924     public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
    925         List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
    926 
    927         // Convert to equivalent JavaProject
    928         IJavaProject javaProject;
    929         try {
    930             //assuming this is an Android (and Java) project since it is attached to the launchInfo.
    931             javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
    932         } catch (CoreException e) {
    933             // return empty dependencies
    934             AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
    935             return dependencies;
    936         }
    937 
    938         // Get all projects that this depends on
    939         List<IJavaProject> androidProjectList;
    940         try {
    941             androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
    942         } catch (JavaModelException e) {
    943             // return empty dependencies
    944             AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
    945             return dependencies;
    946         }
    947 
    948         // for each project, parse manifest and create launch information
    949         for (IJavaProject androidProject : androidProjectList) {
    950             // Parse the Manifest to get various required information
    951             // copied from LaunchConfigDelegate
    952             ManifestData manifestData = AndroidManifestHelper.parseForData(
    953                     androidProject.getProject());
    954 
    955             if (manifestData == null) {
    956                 continue;
    957             }
    958 
    959             // Get the APK location (can return null)
    960             IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
    961             if (apk == null) {
    962                 // getApplicationPackage will have logged an error message
    963                 continue;
    964             }
    965 
    966             // Create new launchInfo as an hybrid between parent and dependency information
    967             DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
    968                     androidProject.getProject(),
    969                     manifestData.getPackage(),
    970                     manifestData.getPackage(),
    971                     launchInfo.getLaunchAction(),
    972                     apk,
    973                     manifestData.getDebuggable(),
    974                     manifestData.getMinSdkVersionString(),
    975                     launchInfo.getLaunch(),
    976                     launchInfo.getMonitor());
    977 
    978             // Add to the list
    979             dependencies.add(delayedLaunchInfo);
    980         }
    981 
    982         return dependencies;
    983     }
    984 
    985     /**
    986      * Installs the application package on the device, and handles return result
    987      * @param launchInfo The launch information
    988      * @param remotePath The remote path of the package.
    989      * @param device The device on which the launch is done.
    990      */
    991     private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath,
    992             final IDevice device) {
    993         String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName());
    994         AdtPlugin.printToConsole(launchInfo.getProject(), message);
    995         try {
    996             // try a reinstall first, because the most common case is the app is already installed
    997             String result = doInstall(launchInfo, remotePath, device, true /* reinstall */);
    998 
    999             /* For now we force to retry the install (after uninstalling) because there's no
   1000              * other way around it: adb install does not want to update a package w/o uninstalling
   1001              * the old one first!
   1002              */
   1003             return checkInstallResult(result, device, launchInfo, remotePath,
   1004                     InstallRetryMode.ALWAYS);
   1005         } catch (Exception e) {
   1006             String msg = String.format(
   1007                     "Failed to install %1$s on device '%2$s!",
   1008                     launchInfo.getPackageFile().getName(), device.getSerialNumber());
   1009             AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e.getMessage());
   1010         }
   1011 
   1012         return false;
   1013     }
   1014 
   1015     /**
   1016      * Checks the result of an installation, and takes optional actions based on it.
   1017      * @param result the result string from the installation
   1018      * @param device the device on which the installation occured.
   1019      * @param launchInfo the {@link DelayedLaunchInfo}
   1020      * @param remotePath the temporary path of the package on the device
   1021      * @param retryMode indicates what to do in case, a package already exists.
   1022      * @return <code>true<code> if success, <code>false</code> otherwise.
   1023      * @throws InstallException
   1024      */
   1025     private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo,
   1026             String remotePath, InstallRetryMode retryMode) throws InstallException {
   1027         if (result == null) {
   1028             AdtPlugin.printToConsole(launchInfo.getProject(), "Success!");
   1029             return true;
   1030         }
   1031         else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$
   1032             // this should never happen, since reinstall mode is used on the first attempt
   1033             if (retryMode == InstallRetryMode.PROMPT) {
   1034                 boolean prompt = AdtPlugin.displayPrompt("Application Install",
   1035                         "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?");
   1036                 if (prompt) {
   1037                     retryMode = InstallRetryMode.ALWAYS;
   1038                 } else {
   1039                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1040                         "Installation error! The package already exists.");
   1041                     return false;
   1042                 }
   1043             }
   1044 
   1045             if (retryMode == InstallRetryMode.ALWAYS) {
   1046                 /*
   1047                  * TODO: create a UI that gives the dev the choice to:
   1048                  * - clean uninstall on launch
   1049                  * - full uninstall if application exists.
   1050                  * - soft uninstall if application exists (keeps the app data around).
   1051                  * - always ask (choice of soft-reinstall, full reinstall)
   1052                 AdtPlugin.printErrorToConsole(launchInfo.mProject,
   1053                         "Application already exists, uninstalling...");
   1054                 String res = doUninstall(device, launchInfo);
   1055                 if (res == null) {
   1056                     AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
   1057                 } else {
   1058                     AdtPlugin.printErrorToConsole(launchInfo.mProject,
   1059                             String.format("Failed to uninstall: %1$s", res));
   1060                     return false;
   1061                 }
   1062                 */
   1063 
   1064                 AdtPlugin.printToConsole(launchInfo.getProject(),
   1065                         "Application already exists. Attempting to re-install instead...");
   1066                 String res = doInstall(launchInfo, remotePath, device, true /* reinstall */ );
   1067                 return checkInstallResult(res, device, launchInfo, remotePath,
   1068                         InstallRetryMode.NEVER);
   1069             }
   1070             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1071                     "Installation error! The package already exists.");
   1072         } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$
   1073             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1074                 "Installation failed due to invalid APK file!",
   1075                 "Please check logcat output for more details.");
   1076         } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$
   1077             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1078                 "Installation failed due to invalid URI!",
   1079                 "Please check logcat output for more details.");
   1080         } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$
   1081             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1082                 String.format("Installation failed: Could not copy %1$s to its final location!",
   1083                         launchInfo.getPackageFile().getName()),
   1084                 "Please check logcat output for more details.");
   1085         } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) {
   1086             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1087                     "Re-installation failed due to different application signatures.",
   1088                     "You must perform a full uninstall of the application. WARNING: This will remove the application data!",
   1089                     String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName()));
   1090         } else {
   1091             AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1092                 String.format("Installation error: %1$s", result),
   1093                 "Please check logcat output for more details.");
   1094         }
   1095 
   1096         return false;
   1097     }
   1098 
   1099     /**
   1100      * Performs the uninstallation of an application.
   1101      * @param device the device on which to install the application.
   1102      * @param launchInfo the {@link DelayedLaunchInfo}.
   1103      * @return a {@link String} with an error code, or <code>null</code> if success.
   1104      * @throws InstallException if the installation failed.
   1105      */
   1106     @SuppressWarnings("unused")
   1107     private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo)
   1108             throws InstallException {
   1109         try {
   1110             return device.uninstallPackage(launchInfo.getPackageName());
   1111         } catch (InstallException e) {
   1112             String msg = String.format(
   1113                     "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage());
   1114             AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
   1115             throw e;
   1116         }
   1117     }
   1118 
   1119     /**
   1120      * Performs the installation of an application whose package has been uploaded on the device.
   1121      *
   1122      * @param launchInfo the {@link DelayedLaunchInfo}.
   1123      * @param remotePath the path of the application package in the device tmp folder.
   1124      * @param device the device on which to install the application.
   1125      * @param reinstall
   1126      * @return a {@link String} with an error code, or <code>null</code> if success.
   1127      * @throws InstallException if the uninstallation failed.
   1128      */
   1129     private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath,
   1130             final IDevice device, boolean reinstall) throws InstallException {
   1131         return device.installRemotePackage(remotePath, reinstall);
   1132     }
   1133 
   1134     /**
   1135      * launches an application on a device or emulator
   1136      *
   1137      * @param info the {@link DelayedLaunchInfo} that indicates the launch action
   1138      * @param device the device or emulator to launch the application on
   1139      */
   1140     public void launchApp(final DelayedLaunchInfo info, IDevice device) {
   1141         if (info.isDebugMode()) {
   1142             synchronized (sListLock) {
   1143                 if (mWaitingForDebuggerApplications.contains(info) == false) {
   1144                     mWaitingForDebuggerApplications.add(info);
   1145                 }
   1146             }
   1147         }
   1148         if (info.getLaunchAction().doLaunchAction(info, device)) {
   1149             // if the app is not a debug app, we need to do some clean up, as
   1150             // the process is done!
   1151             if (info.isDebugMode() == false) {
   1152                 // stop the launch object, since there's no debug, and it can't
   1153                 // provide any control over the app
   1154                 stopLaunch(info);
   1155             }
   1156         } else {
   1157             // something went wrong or no further launch action needed
   1158             // lets stop the Launch
   1159             stopLaunch(info);
   1160         }
   1161 
   1162         // Monitor the logcat output on the launched device to notify
   1163         // the user if any significant error occurs that is visible from logcat
   1164         DdmsPlugin.getDefault().startLogCatMonitor(device);
   1165     }
   1166 
   1167     private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) {
   1168 
   1169         // split the custom command line in segments
   1170         ArrayList<String> customArgs = new ArrayList<String>();
   1171         boolean hasWipeData = false;
   1172         if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) {
   1173             String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$
   1174 
   1175             // we need to remove the empty strings
   1176             for (String s : segments) {
   1177                 if (s.length() > 0) {
   1178                     customArgs.add(s);
   1179                     if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) {
   1180                         hasWipeData = true;
   1181                     }
   1182                 }
   1183             }
   1184         }
   1185 
   1186         boolean needsWipeData = config.mWipeData && !hasWipeData;
   1187         if (needsWipeData) {
   1188             if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) {
   1189                 needsWipeData = false;
   1190             }
   1191         }
   1192 
   1193         // build the command line based on the available parameters.
   1194         ArrayList<String> list = new ArrayList<String>();
   1195 
   1196         String path = AdtPlugin.getOsAbsoluteEmulator();
   1197 
   1198         list.add(path);
   1199 
   1200         list.add(FLAG_AVD);
   1201         list.add(avdToLaunch.getName());
   1202 
   1203         if (config.mNetworkSpeed != null) {
   1204             list.add(FLAG_NETSPEED);
   1205             list.add(config.mNetworkSpeed);
   1206         }
   1207 
   1208         if (config.mNetworkDelay != null) {
   1209             list.add(FLAG_NETDELAY);
   1210             list.add(config.mNetworkDelay);
   1211         }
   1212 
   1213         if (needsWipeData) {
   1214             list.add(FLAG_WIPE_DATA);
   1215         }
   1216 
   1217         if (config.mNoBootAnim) {
   1218             list.add(FLAG_NO_BOOT_ANIM);
   1219         }
   1220 
   1221         list.addAll(customArgs);
   1222 
   1223         // convert the list into an array for the call to exec.
   1224         String[] command = list.toArray(new String[list.size()]);
   1225 
   1226         // launch the emulator
   1227         try {
   1228             Process process = Runtime.getRuntime().exec(command);
   1229             grabEmulatorOutput(process);
   1230         } catch (IOException e) {
   1231             return false;
   1232         }
   1233 
   1234         return true;
   1235     }
   1236 
   1237     /**
   1238      * Looks for and returns an existing {@link ILaunchConfiguration} object for a
   1239      * specified project.
   1240      * @param manager The {@link ILaunchManager}.
   1241      * @param type The {@link ILaunchConfigurationType}.
   1242      * @param projectName The name of the project
   1243      * @return an existing <code>ILaunchConfiguration</code> object matching the project, or
   1244      *      <code>null</code>.
   1245      */
   1246     private static ILaunchConfiguration findConfig(ILaunchManager manager,
   1247             ILaunchConfigurationType type, String projectName) {
   1248         try {
   1249             ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);
   1250 
   1251             for (ILaunchConfiguration config : configs) {
   1252                 if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
   1253                         "").equals(projectName)) {  //$NON-NLS-1$
   1254                     return config;
   1255                 }
   1256             }
   1257         } catch (CoreException e) {
   1258             MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(),
   1259                     "Launch Error", e.getStatus().getMessage());
   1260         }
   1261 
   1262         // didn't find anything that matches. Return null
   1263         return null;
   1264 
   1265     }
   1266 
   1267 
   1268     /**
   1269      * Connects a remote debugger on the specified port.
   1270      * @param debugPort The port to connect the debugger to
   1271      * @param launch The associated AndroidLaunch object.
   1272      * @param monitor A Progress monitor
   1273      * @return false if cancelled by the monitor
   1274      * @throws CoreException
   1275      */
   1276     @SuppressWarnings("deprecation")
   1277     public static boolean connectRemoteDebugger(int debugPort,
   1278             AndroidLaunch launch, IProgressMonitor monitor)
   1279                 throws CoreException {
   1280         // get some default parameters.
   1281         int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);
   1282 
   1283         HashMap<String, String> newMap = new HashMap<String, String>();
   1284 
   1285         newMap.put("hostname", "localhost");  //$NON-NLS-1$ //$NON-NLS-2$
   1286 
   1287         newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$
   1288 
   1289         newMap.put("timeout", Integer.toString(connectTimeout));
   1290 
   1291         // get the default VM connector
   1292         IVMConnector connector = JavaRuntime.getDefaultVMConnector();
   1293 
   1294         // connect to remote VM
   1295         connector.connect(newMap, monitor, launch);
   1296 
   1297         // check for cancellation
   1298         if (monitor.isCanceled()) {
   1299             IDebugTarget[] debugTargets = launch.getDebugTargets();
   1300             for (IDebugTarget target : debugTargets) {
   1301                 if (target.canDisconnect()) {
   1302                     target.disconnect();
   1303                 }
   1304             }
   1305             return false;
   1306         }
   1307 
   1308         return true;
   1309     }
   1310 
   1311     /**
   1312      * Launch a new thread that connects a remote debugger on the specified port.
   1313      * @param debugPort The port to connect the debugger to
   1314      * @param androidLaunch The associated AndroidLaunch object.
   1315      * @param monitor A Progress monitor
   1316      * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor)
   1317      */
   1318     public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch,
   1319             final IProgressMonitor monitor) {
   1320         new Thread("Debugger connection") { //$NON-NLS-1$
   1321             @Override
   1322             public void run() {
   1323                 try {
   1324                     connectRemoteDebugger(debugPort, androidLaunch, monitor);
   1325                 } catch (CoreException e) {
   1326                     androidLaunch.stopLaunch();
   1327                 }
   1328                 monitor.done();
   1329             }
   1330         }.start();
   1331     }
   1332 
   1333     /**
   1334      * Sent when a new {@link AndroidDebugBridge} is started.
   1335      * <p/>
   1336      * This is sent from a non UI thread.
   1337      * @param bridge the new {@link AndroidDebugBridge} object.
   1338      *
   1339      * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
   1340      */
   1341     public void bridgeChanged(AndroidDebugBridge bridge) {
   1342         // The adb server has changed. We cancel any pending launches.
   1343         String message = "adb server change: cancelling '%1$s'!";
   1344         synchronized (sListLock) {
   1345             for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) {
   1346                 AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1347                     String.format(message, launchInfo.getLaunchAction().getLaunchDescription()));
   1348                 stopLaunch(launchInfo);
   1349             }
   1350             for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) {
   1351                 AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1352                         String.format(message,
   1353                                 launchInfo.getLaunchAction().getLaunchDescription()));
   1354                 stopLaunch(launchInfo);
   1355             }
   1356 
   1357             mWaitingForReadyEmulatorList.clear();
   1358             mWaitingForDebuggerApplications.clear();
   1359         }
   1360     }
   1361 
   1362     /**
   1363      * Sent when the a device is connected to the {@link AndroidDebugBridge}.
   1364      * <p/>
   1365      * This is sent from a non UI thread.
   1366      * @param device the new device.
   1367      *
   1368      * @see IDeviceChangeListener#deviceConnected(IDevice)
   1369      */
   1370     public void deviceConnected(IDevice device) {
   1371         synchronized (sListLock) {
   1372             // look if there's an app waiting for a device
   1373             if (mWaitingForEmulatorLaunches.size() > 0) {
   1374                 // get/remove first launch item from the list
   1375                 // FIXME: what if we have multiple launches waiting?
   1376                 DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0);
   1377                 mWaitingForEmulatorLaunches.remove(0);
   1378 
   1379                 // give the launch item its device for later use.
   1380                 launchInfo.setDevice(device);
   1381 
   1382                 // and move it to the other list
   1383                 mWaitingForReadyEmulatorList.add(launchInfo);
   1384 
   1385                 // and tell the user about it
   1386                 AdtPlugin.printToConsole(launchInfo.getProject(),
   1387                         String.format("New emulator found: %1$s", device.getSerialNumber()));
   1388                 AdtPlugin.printToConsole(launchInfo.getProject(),
   1389                         String.format("Waiting for HOME ('%1$s') to be launched...",
   1390                             AdtPlugin.getDefault().getPreferenceStore().getString(
   1391                                     AdtPrefs.PREFS_HOME_PACKAGE)));
   1392             }
   1393         }
   1394     }
   1395 
   1396     /**
   1397      * Sent when the a device is connected to the {@link AndroidDebugBridge}.
   1398      * <p/>
   1399      * This is sent from a non UI thread.
   1400      * @param device the new device.
   1401      *
   1402      * @see IDeviceChangeListener#deviceDisconnected(IDevice)
   1403      */
   1404     @SuppressWarnings("unchecked")
   1405     public void deviceDisconnected(IDevice device) {
   1406         // any pending launch on this device must be canceled.
   1407         String message = "%1$s disconnected! Cancelling '%2$s'!";
   1408         synchronized (sListLock) {
   1409             ArrayList<DelayedLaunchInfo> copyList =
   1410                 (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.clone();
   1411             for (DelayedLaunchInfo launchInfo : copyList) {
   1412                 if (launchInfo.getDevice() == device) {
   1413                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1414                             String.format(message, device.getSerialNumber(),
   1415                                     launchInfo.getLaunchAction().getLaunchDescription()));
   1416                     stopLaunch(launchInfo);
   1417                 }
   1418             }
   1419             copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone();
   1420             for (DelayedLaunchInfo launchInfo : copyList) {
   1421                 if (launchInfo.getDevice() == device) {
   1422                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1423                             String.format(message, device.getSerialNumber(),
   1424                                     launchInfo.getLaunchAction().getLaunchDescription()));
   1425                     stopLaunch(launchInfo);
   1426                 }
   1427             }
   1428         }
   1429     }
   1430 
   1431     /**
   1432      * Sent when a device data changed, or when clients are started/terminated on the device.
   1433      * <p/>
   1434      * This is sent from a non UI thread.
   1435      * @param device the device that was updated.
   1436      * @param changeMask the mask indicating what changed.
   1437      *
   1438      * @see IDeviceChangeListener#deviceChanged(IDevice, int)
   1439      */
   1440     public void deviceChanged(IDevice device, int changeMask) {
   1441         // We could check if any starting device we care about is now ready, but we can wait for
   1442         // its home app to show up, so...
   1443     }
   1444 
   1445     /**
   1446      * Sent when an existing client information changed.
   1447      * <p/>
   1448      * This is sent from a non UI thread.
   1449      * @param client the updated client.
   1450      * @param changeMask the bit mask describing the changed properties. It can contain
   1451      * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
   1452      * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
   1453      * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
   1454      * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
   1455      *
   1456      * @see IClientChangeListener#clientChanged(Client, int)
   1457      */
   1458     public void clientChanged(final Client client, int changeMask) {
   1459         boolean connectDebugger = false;
   1460         if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
   1461             String applicationName = client.getClientData().getClientDescription();
   1462             if (applicationName != null) {
   1463                 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
   1464                 String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE);
   1465 
   1466                 if (home.equals(applicationName)) {
   1467 
   1468                     // looks like home is up, get its device
   1469                     IDevice device = client.getDevice();
   1470 
   1471                     // look for application waiting for home
   1472                     synchronized (sListLock) {
   1473                         for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) {
   1474                             DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i);
   1475                             if (launchInfo.getDevice() == device) {
   1476                                 // it's match, remove from the list
   1477                                 mWaitingForReadyEmulatorList.remove(i);
   1478 
   1479                                 // We couldn't check earlier the API level of the device
   1480                                 // (it's asynchronous when the device boot, and usually
   1481                                 // deviceConnected is called before it's queried for its build info)
   1482                                 // so we check now
   1483                                 if (checkBuildInfo(launchInfo, device) == false) {
   1484                                     // device is not the proper API!
   1485                                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1486                                             "Launch canceled!");
   1487                                     stopLaunch(launchInfo);
   1488                                     return;
   1489                                 }
   1490 
   1491                                 AdtPlugin.printToConsole(launchInfo.getProject(),
   1492                                         String.format("HOME is up on device '%1$s'",
   1493                                                 device.getSerialNumber()));
   1494 
   1495                                 // attempt to sync the new package onto the device.
   1496                                 if (syncApp(launchInfo, device)) {
   1497                                     // application package is sync'ed, lets attempt to launch it.
   1498                                     launchApp(launchInfo, device);
   1499                                 } else {
   1500                                     // failure! Cancel and return
   1501                                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1502                                     "Launch canceled!");
   1503                                     stopLaunch(launchInfo);
   1504                                 }
   1505 
   1506                                 break;
   1507                             } else {
   1508                                 i++;
   1509                             }
   1510                         }
   1511                     }
   1512                 }
   1513 
   1514                 // check if it's already waiting for a debugger, and if so we connect to it.
   1515                 if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
   1516                     // search for this client in the list;
   1517                     synchronized (sListLock) {
   1518                         int index = mUnknownClientsWaitingForDebugger.indexOf(client);
   1519                         if (index != -1) {
   1520                             connectDebugger = true;
   1521                             mUnknownClientsWaitingForDebugger.remove(client);
   1522                         }
   1523                     }
   1524                 }
   1525             }
   1526         }
   1527 
   1528         // if it's not home, it could be an app that is now in debugger mode that we're waiting for
   1529         // lets check it
   1530 
   1531         if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) {
   1532             ClientData clientData = client.getClientData();
   1533             String applicationName = client.getClientData().getClientDescription();
   1534             if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
   1535                 // Get the application name, and make sure its valid.
   1536                 if (applicationName == null) {
   1537                     // looks like we don't have the client yet, so we keep it around for when its
   1538                     // name becomes available.
   1539                     synchronized (sListLock) {
   1540                         mUnknownClientsWaitingForDebugger.add(client);
   1541                     }
   1542                     return;
   1543                 } else {
   1544                     connectDebugger = true;
   1545                 }
   1546             }
   1547         }
   1548 
   1549         if (connectDebugger) {
   1550             Log.d("adt", "Debugging " + client);
   1551             // now check it against the apps waiting for a debugger
   1552             String applicationName = client.getClientData().getClientDescription();
   1553             Log.d("adt", "App Name: " + applicationName);
   1554             synchronized (sListLock) {
   1555                 for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) {
   1556                     final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i);
   1557                     if (client.getDevice() == launchInfo.getDevice() &&
   1558                             applicationName.equals(launchInfo.getDebugPackageName())) {
   1559                         // this is a match. We remove the launch info from the list
   1560                         mWaitingForDebuggerApplications.remove(i);
   1561 
   1562                         // and connect the debugger.
   1563                         String msg = String.format(
   1564                                 "Attempting to connect debugger to '%1$s' on port %2$d",
   1565                                 launchInfo.getDebugPackageName(), client.getDebuggerListenPort());
   1566                         AdtPlugin.printToConsole(launchInfo.getProject(), msg);
   1567 
   1568                         new Thread("Debugger Connection") { //$NON-NLS-1$
   1569                             @Override
   1570                             public void run() {
   1571                                 try {
   1572                                     if (connectRemoteDebugger(
   1573                                             client.getDebuggerListenPort(),
   1574                                             launchInfo.getLaunch(),
   1575                                             launchInfo.getMonitor()) == false) {
   1576                                         return;
   1577                                     }
   1578                                 } catch (CoreException e) {
   1579                                     // well something went wrong.
   1580                                     AdtPlugin.printErrorToConsole(launchInfo.getProject(),
   1581                                             String.format("Launch error: %s", e.getMessage()));
   1582                                     // stop the launch
   1583                                     stopLaunch(launchInfo);
   1584                                 }
   1585 
   1586                                 launchInfo.getMonitor().done();
   1587                             }
   1588                         }.start();
   1589 
   1590                         // we're done processing this client.
   1591                         return;
   1592 
   1593                     } else {
   1594                         i++;
   1595                     }
   1596                 }
   1597             }
   1598 
   1599             // if we get here, we haven't found an app that we were launching, so we look
   1600             // for opened android projects that contains the app asking for a debugger.
   1601             // If we find one, we automatically connect to it.
   1602             IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);
   1603 
   1604             if (project != null) {
   1605                 debugRunningApp(project, client.getDebuggerListenPort());
   1606             }
   1607         }
   1608     }
   1609 
   1610     /**
   1611      * Get the stderr/stdout outputs of a process and return when the process is done.
   1612      * Both <b>must</b> be read or the process will block on windows.
   1613      * @param process The process to get the output from
   1614      */
   1615     private void grabEmulatorOutput(final Process process) {
   1616         // read the lines as they come. if null is returned, it's
   1617         // because the process finished
   1618         new Thread("") { //$NON-NLS-1$
   1619             @Override
   1620             public void run() {
   1621                 // create a buffer to read the stderr output
   1622                 InputStreamReader is = new InputStreamReader(process.getErrorStream());
   1623                 BufferedReader errReader = new BufferedReader(is);
   1624 
   1625                 try {
   1626                     while (true) {
   1627                         String line = errReader.readLine();
   1628                         if (line != null) {
   1629                             AdtPlugin.printErrorToConsole("Emulator", line);
   1630                         } else {
   1631                             break;
   1632                         }
   1633                     }
   1634                 } catch (IOException e) {
   1635                     // do nothing.
   1636                 }
   1637             }
   1638         }.start();
   1639 
   1640         new Thread("") { //$NON-NLS-1$
   1641             @Override
   1642             public void run() {
   1643                 InputStreamReader is = new InputStreamReader(process.getInputStream());
   1644                 BufferedReader outReader = new BufferedReader(is);
   1645 
   1646                 try {
   1647                     while (true) {
   1648                         String line = outReader.readLine();
   1649                         if (line != null) {
   1650                             AdtPlugin.printToConsole("Emulator", line);
   1651                         } else {
   1652                             break;
   1653                         }
   1654                     }
   1655                 } catch (IOException e) {
   1656                     // do nothing.
   1657                 }
   1658             }
   1659         }.start();
   1660     }
   1661 
   1662     /* (non-Javadoc)
   1663      * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo)
   1664      */
   1665     public void stopLaunch(DelayedLaunchInfo launchInfo) {
   1666         launchInfo.getLaunch().stopLaunch();
   1667         synchronized (sListLock) {
   1668             mWaitingForReadyEmulatorList.remove(launchInfo);
   1669             mWaitingForDebuggerApplications.remove(launchInfo);
   1670         }
   1671     }
   1672 }
   1673