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.ide.common.xml.ManifestData;
21 import com.android.ide.common.xml.ManifestData.Activity;
22 import com.android.ide.eclipse.adt.AdtConstants;
23 import com.android.ide.eclipse.adt.AdtPlugin;
24 import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
25 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
26 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
27 
28 import org.eclipse.core.resources.IFile;
29 import org.eclipse.core.resources.IProject;
30 import org.eclipse.core.resources.IWorkspace;
31 import org.eclipse.core.resources.ResourcesPlugin;
32 import org.eclipse.core.runtime.CoreException;
33 import org.eclipse.core.runtime.IProgressMonitor;
34 import org.eclipse.debug.core.ILaunch;
35 import org.eclipse.debug.core.ILaunchConfiguration;
36 import org.eclipse.debug.core.model.LaunchConfigurationDelegate;
37 import org.eclipse.jdt.core.JavaCore;
38 import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
39 
40 /**
41  * Implementation of an eclipse LauncConfigurationDelegate to launch android
42  * application in debug.
43  */
44 public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
45     final static int INVALID_DEBUG_PORT = -1;
46 
47     public final static String ANDROID_LAUNCH_TYPE_ID =
48         "com.android.ide.eclipse.adt.debug.LaunchConfigType"; //$NON-NLS-1$
49 
50     /** Target mode parameters: true is automatic, false is manual */
51     public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
52     public static final TargetMode DEFAULT_TARGET_MODE = TargetMode.AUTO;
53 
54     /** Flag indicating whether the last used device should be used for future launches. */
55     public static final String ATTR_REUSE_LAST_USED_DEVICE =
56             AdtPlugin.PLUGIN_ID + ".reuse.last.used.device"; //$NON-NLS-1$
57 
58     /** Device on which the last launch happened. */
59     public static final String ATTR_LAST_USED_DEVICE =
60             AdtPlugin.PLUGIN_ID + ".last.used.device"; //$NON-NLS-1$
61 
62     /**
63      * Launch action:
64      * <ul>
65      * <li>0: launch default activity</li>
66      * <li>1: launch specified activity. See {@link #ATTR_ACTIVITY}</li>
67      * <li>2: Do Nothing</li>
68      * </ul>
69      */
70     public final static String ATTR_LAUNCH_ACTION = AdtPlugin.PLUGIN_ID + ".action"; //$NON-NLS-1$
71 
72     /** Default launch action. This launches the activity that is setup to be found in the HOME
73      * screen.
74      */
75     public final static int ACTION_DEFAULT = 0;
76     /** Launch action starting a specific activity. */
77     public final static int ACTION_ACTIVITY = 1;
78     /** Launch action that does nothing. */
79     public final static int ACTION_DO_NOTHING = 2;
80     /** Default launch action value. */
81     public final static int DEFAULT_LAUNCH_ACTION = ACTION_DEFAULT;
82 
83     /**
84      * Activity to be launched if {@link #ATTR_LAUNCH_ACTION} is 1
85      */
86     public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$
87 
88     public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$
89 
90     public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$
91 
92     /**
93      * Index of the default network speed setting for the emulator.<br>
94      * Get the emulator option with <code>EmulatorConfigTab.getSpeed(index)</code>
95      */
96     public static final int DEFAULT_SPEED = 0;
97 
98     public static final String ATTR_DELAY = AdtPlugin.PLUGIN_ID + ".delay"; //$NON-NLS-1$
99 
100     /**
101      * Index of the default network latency setting for the emulator.<br>
102      * Get the emulator option with <code>EmulatorConfigTab.getDelay(index)</code>
103      */
104     public static final int DEFAULT_DELAY = 0;
105 
106     public static final String ATTR_COMMANDLINE = AdtPlugin.PLUGIN_ID + ".commandline"; //$NON-NLS-1$
107 
108     public static final String ATTR_WIPE_DATA = AdtPlugin.PLUGIN_ID + ".wipedata"; //$NON-NLS-1$
109     public static final boolean DEFAULT_WIPE_DATA = false;
110 
111     public static final String ATTR_NO_BOOT_ANIM = AdtPlugin.PLUGIN_ID + ".nobootanim"; //$NON-NLS-1$
112     public static final boolean DEFAULT_NO_BOOT_ANIM = false;
113 
114     public static final String ATTR_DEBUG_PORT =
115         AdtPlugin.PLUGIN_ID + ".debugPort"; //$NON-NLS-1$
116 
117     @Override
launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor)118     public void launch(ILaunchConfiguration configuration, String mode,
119             ILaunch launch, IProgressMonitor monitor) throws CoreException {
120         // We need to check if it's a standard launch or if it's a launch
121         // to debug an application already running.
122         int debugPort = AndroidLaunchController.getPortForConfig(configuration);
123 
124         // get the project
125         IProject project = getProject(configuration);
126 
127         // first we make sure the launch is of the proper type
128         AndroidLaunch androidLaunch = null;
129         if (launch instanceof AndroidLaunch) {
130             androidLaunch = (AndroidLaunch)launch;
131         } else {
132             // wrong type, not sure how we got there, but we don't do
133             // anything else
134             AdtPlugin.printErrorToConsole(project, "Wrong Launch Type!");
135             return;
136         }
137 
138         // if we have a valid debug port, this means we're debugging an app
139         // that's already launched.
140         if (debugPort != INVALID_DEBUG_PORT) {
141             AndroidLaunchController.launchRemoteDebugger(debugPort, androidLaunch, monitor);
142             return;
143         }
144 
145         if (project == null) {
146             AdtPlugin.printErrorToConsole("Couldn't get project object!");
147             androidLaunch.stopLaunch();
148             return;
149         }
150 
151         // make sure the project and its dependencies are built
152         // and PostCompilerBuilder runs.
153         // This is a synchronous call which returns when the
154         // build is done.
155         ProjectHelper.doFullIncrementalDebugBuild(project, monitor);
156 
157         // check if the project has errors, and abort in this case.
158         if (ProjectHelper.hasError(project, true)) {
159             AdtPlugin.displayError("Android Launch",
160                     "Your project contains error(s), please fix them before running your application.");
161             return;
162         }
163 
164         AdtPlugin.printToConsole(project, "------------------------------"); //$NON-NLS-1$
165         AdtPlugin.printToConsole(project, "Android Launch!");
166 
167         // check if the project is using the proper sdk.
168         // if that throws an exception, we simply let it propagate to the caller.
169         if (checkAndroidProject(project) == false) {
170             AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
171             androidLaunch.stopLaunch();
172             return;
173         }
174 
175         // Check adb status and abort if needed.
176         AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
177         if (bridge == null || bridge.isConnected() == false) {
178             try {
179                 int connections = -1;
180                 int restarts = -1;
181                 if (bridge != null) {
182                     connections = bridge.getConnectionAttemptCount();
183                     restarts = bridge.getRestartAttemptCount();
184                 }
185 
186                 // if we get -1, the device monitor is not even setup (anymore?).
187                 // We need to ask the user to restart eclipse.
188                 // This shouldn't happen, but it's better to let the user know in case it does.
189                 if (connections == -1 || restarts == -1) {
190                     AdtPlugin.printErrorToConsole(project,
191                             "The connection to adb is down, and a severe error has occured.",
192                             "You must restart adb and Eclipse.",
193                             String.format(
194                                     "Please ensure that adb is correctly located at '%1$s' and can be executed.",
195                                     AdtPlugin.getOsAbsoluteAdb()));
196                     return;
197                 }
198 
199                 if (restarts == 0) {
200                     AdtPlugin.printErrorToConsole(project,
201                             "Connection with adb was interrupted.",
202                             String.format("%1$s attempts have been made to reconnect.", connections),
203                             "You may want to manually restart adb from the Devices view.");
204                 } else {
205                     AdtPlugin.printErrorToConsole(project,
206                             "Connection with adb was interrupted, and attempts to reconnect have failed.",
207                             String.format("%1$s attempts have been made to restart adb.", restarts),
208                             "You may want to manually restart adb from the Devices view.");
209 
210                 }
211                 return;
212             } finally {
213                 androidLaunch.stopLaunch();
214             }
215         }
216 
217         // since adb is working, we let the user know
218         // TODO have a verbose mode for launch with more info (or some of the less useful info we now have).
219         AdtPlugin.printToConsole(project, "adb is running normally.");
220 
221         // make a config class
222         AndroidLaunchConfiguration config = new AndroidLaunchConfiguration();
223 
224         // fill it with the config coming from the ILaunchConfiguration object
225         config.set(configuration);
226 
227         // get the launch controller singleton
228         AndroidLaunchController controller = AndroidLaunchController.getInstance();
229 
230         // get the application package
231         IFile applicationPackage = ProjectHelper.getApplicationPackage(project);
232         if (applicationPackage == null) {
233             androidLaunch.stopLaunch();
234             return;
235         }
236 
237         // we need some information from the manifest
238         ManifestData manifestData = AndroidManifestHelper.parseForData(project);
239 
240         if (manifestData == null) {
241             AdtPlugin.printErrorToConsole(project, "Failed to parse AndroidManifest: aborting!");
242             androidLaunch.stopLaunch();
243             return;
244         }
245 
246         doLaunch(configuration, mode, monitor, project, androidLaunch, config, controller,
247                 applicationPackage, manifestData);
248     }
249 
doLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor, IProject project, AndroidLaunch androidLaunch, AndroidLaunchConfiguration config, AndroidLaunchController controller, IFile applicationPackage, ManifestData manifestData)250     protected void doLaunch(ILaunchConfiguration configuration, String mode,
251             IProgressMonitor monitor, IProject project, AndroidLaunch androidLaunch,
252             AndroidLaunchConfiguration config, AndroidLaunchController controller,
253             IFile applicationPackage, ManifestData manifestData) {
254 
255        String activityName = null;
256 
257         if (config.mLaunchAction == ACTION_ACTIVITY) {
258             // Get the activity name defined in the config
259             activityName = getActivityName(configuration);
260 
261             // Get the full activity list and make sure the one we got matches.
262             Activity[] activities = manifestData.getActivities();
263 
264             // first we check that there are, in fact, activities.
265             if (activities.length == 0) {
266                 // if the activities list is null, then the manifest is empty
267                 // and we can't launch the app. We'll revert to a sync-only launch
268                 AdtPlugin.printErrorToConsole(project,
269                         "The Manifest defines no activity!",
270                         "The launch will only sync the application package on the device!");
271                 config.mLaunchAction = ACTION_DO_NOTHING;
272             } else if (activityName == null) {
273                 // if the activity we got is null, we look for the default one.
274                 AdtPlugin.printErrorToConsole(project,
275                         "No activity specified! Getting the launcher activity.");
276                 Activity launcherActivity = manifestData.getLauncherActivity();
277                 if (launcherActivity != null) {
278                     activityName = launcherActivity.getName();
279                 }
280 
281                 // if there's no default activity. We revert to a sync-only launch.
282                 if (activityName == null) {
283                     revertToNoActionLaunch(project, config);
284                 }
285             } else {
286 
287                 // check the one we got from the config matches any from the list
288                 boolean match = false;
289                 for (Activity a : activities) {
290                     if (a != null && a.getName().equals(activityName)) {
291                         match = true;
292                         break;
293                     }
294                 }
295 
296                 // if we didn't find a match, we revert to the default activity if any.
297                 if (match == false) {
298                     AdtPlugin.printErrorToConsole(project,
299                             "The specified activity does not exist! Getting the launcher activity.");
300                     Activity launcherActivity = manifestData.getLauncherActivity();
301                     if (launcherActivity != null) {
302                         activityName = launcherActivity.getName();
303                     } else {
304                         // if there's no default activity. We revert to a sync-only launch.
305                         revertToNoActionLaunch(project, config);
306                     }
307                 }
308             }
309         } else if (config.mLaunchAction == ACTION_DEFAULT) {
310             Activity launcherActivity = manifestData.getLauncherActivity();
311             if (launcherActivity != null) {
312                 activityName = launcherActivity.getName();
313             }
314 
315             // if there's no default activity. We revert to a sync-only launch.
316             if (activityName == null) {
317                 revertToNoActionLaunch(project, config);
318             }
319         }
320 
321         IAndroidLaunchAction launchAction = null;
322         if (config.mLaunchAction == ACTION_DO_NOTHING || activityName == null) {
323             launchAction = new EmptyLaunchAction();
324         } else {
325             launchAction = new ActivityLaunchAction(activityName, controller);
326         }
327 
328         // everything seems fine, we ask the launch controller to handle
329         // the rest
330         controller.launch(project, mode, applicationPackage,manifestData.getPackage(),
331                 manifestData.getPackage(), manifestData.getDebuggable(),
332                 manifestData.getMinSdkVersionString(), launchAction, config, androidLaunch,
333                 monitor);
334     }
335 
336     @Override
buildForLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor)337     public boolean buildForLaunch(ILaunchConfiguration configuration,
338             String mode, IProgressMonitor monitor) throws CoreException {
339         // if this returns true, this forces a full workspace rebuild which is not
340         // what we want.
341         // Instead in the #launch method, we'll rebuild only the launching project.
342         return false;
343     }
344 
345     /**
346      * {@inheritDoc}
347      * @throws CoreException
348      */
349     @Override
getLaunch(ILaunchConfiguration configuration, String mode)350     public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
351             throws CoreException {
352         return new AndroidLaunch(configuration, mode, null);
353     }
354 
355     /**
356      * Returns the IProject object matching the name found in the configuration
357      * object under the name
358      * <code>IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME</code>
359      * @param configuration
360      * @return The IProject object or null
361      */
getProject(ILaunchConfiguration configuration)362     private IProject getProject(ILaunchConfiguration configuration){
363         // get the project name from the config
364         String projectName;
365         try {
366             projectName = configuration.getAttribute(
367                     IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "");
368         } catch (CoreException e) {
369             return null;
370         }
371 
372         // get the current workspace
373         IWorkspace workspace = ResourcesPlugin.getWorkspace();
374 
375         // and return the project with the name from the config
376         return workspace.getRoot().getProject(projectName);
377     }
378 
379     /**
380      * Checks the project is an android project.
381      * @param project The project to check
382      * @return true if the project is an android SDK.
383      * @throws CoreException
384      */
checkAndroidProject(IProject project)385     private boolean checkAndroidProject(IProject project) throws CoreException {
386         // check if the project is a java and an android project.
387         if (project.hasNature(JavaCore.NATURE_ID) == false) {
388             String msg = String.format("%1$s is not a Java project!", project.getName());
389             AdtPlugin.displayError("Android Launch", msg);
390             return false;
391         }
392 
393         if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
394             String msg = String.format("%1$s is not an Android project!", project.getName());
395             AdtPlugin.displayError("Android Launch", msg);
396             return false;
397         }
398 
399         return true;
400     }
401 
402 
403     /**
404      * Returns the name of the activity.
405      */
getActivityName(ILaunchConfiguration configuration)406     private String getActivityName(ILaunchConfiguration configuration) {
407         String empty = "";
408         String activityName;
409         try {
410             activityName = configuration.getAttribute(ATTR_ACTIVITY, empty);
411         } catch (CoreException e) {
412             return null;
413         }
414 
415         return (activityName != empty) ? activityName : null;
416     }
417 
revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config)418     private final void revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config) {
419         AdtPlugin.printErrorToConsole(project,
420                 "No Launcher activity found!",
421                 "The launch will only sync the application package on the device!");
422         config.mLaunchAction = ACTION_DO_NOTHING;
423     }
424 }
425