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