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