• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
AndroidLaunchController()143     private AndroidLaunchController() {
144         AndroidDebugBridge.addDebugBridgeChangeListener(this);
145         AndroidDebugBridge.addDeviceChangeListener(this);
146         AndroidDebugBridge.addClientChangeListener(this);
147     }
148 
149     /**
150      * Returns the singleton reference.
151      */
getInstance()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      */
debugRunningApp(IProject project, int debugPort)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      */
getLaunchConfig(IProject project)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      */
getResourcesToMap(IProject project)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      */
launch(final IProject project, String mode, IFile apk, String packageName, String debugPackageName, Boolean debuggable, String requiredApiVersionNumber, final IAndroidLaunchAction launchAction, final AndroidLaunchConfiguration config, final AndroidLaunch launch, IProgressMonitor monitor)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      */
findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget)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      */
continueLaunch(final DeviceChooserResponse response, final IProject project, final AndroidLaunch launch, final DelayedLaunchInfo launchInfo, final AndroidLaunchConfiguration config)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      */
getPortForConfig(ILaunchConfiguration launchConfig)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      */
setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig, int port)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      */
checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device)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      */
simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device)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      */
syncApp(DelayedLaunchInfo launchInfo, IDevice device)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      */
doSyncApp(DelayedLaunchInfo launchInfo, IDevice device)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      */
getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo)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      */
installPackage(DelayedLaunchInfo launchInfo, final String remotePath, final IDevice device)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      */
checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo, String remotePath, InstallRetryMode retryMode)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")
doUninstall(IDevice device, DelayedLaunchInfo launchInfo)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      */
doInstall(DelayedLaunchInfo launchInfo, final String remotePath, final IDevice device, boolean reinstall)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      */
launchApp(final DelayedLaunchInfo info, IDevice device)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 
launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch)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      */
findConfig(ILaunchManager manager, ILaunchConfigurationType type, String projectName)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")
connectRemoteDebugger(int debugPort, AndroidLaunch launch, IProgressMonitor monitor)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      */
launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch, final IProgressMonitor monitor)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      */
bridgeChanged(AndroidDebugBridge bridge)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      */
deviceConnected(IDevice device)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")
deviceDisconnected(IDevice device)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      */
deviceChanged(IDevice device, int changeMask)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      */
clientChanged(final Client client, int changeMask)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      */
grabEmulatorOutput(final Process process)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      */
stopLaunch(DelayedLaunchInfo launchInfo)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