1 /*
2 * Copyright (C) 2011 Lorenzo Villani
3 *
4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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 package com.jayway.maven.plugins.android.standalonemojos;
17
18 import com.android.ddmlib.AdbCommandRejectedException;
19 import com.android.ddmlib.CollectingOutputReceiver;
20 import com.android.ddmlib.IDevice;
21 import com.android.ddmlib.ShellCommandUnresponsiveException;
22 import com.android.ddmlib.TimeoutException;
23 import com.jayway.maven.plugins.android.AbstractAndroidMojo;
24 import com.jayway.maven.plugins.android.DeviceCallback;
25 import com.jayway.maven.plugins.android.common.DeviceHelper;
26 import com.jayway.maven.plugins.android.config.ConfigHandler;
27 import com.jayway.maven.plugins.android.config.ConfigPojo;
28 import com.jayway.maven.plugins.android.config.PullParameter;
29 import com.jayway.maven.plugins.android.configuration.Run;
30 import org.apache.maven.plugin.MojoExecutionException;
31 import org.apache.maven.plugin.MojoFailureException;
32 import org.w3c.dom.Document;
33 import org.w3c.dom.NodeList;
34 import org.xml.sax.SAXException;
35
36 import javax.xml.parsers.DocumentBuilder;
37 import javax.xml.parsers.DocumentBuilderFactory;
38 import javax.xml.parsers.ParserConfigurationException;
39 import javax.xml.xpath.XPath;
40 import javax.xml.xpath.XPathConstants;
41 import javax.xml.xpath.XPathExpression;
42 import javax.xml.xpath.XPathExpressionException;
43 import javax.xml.xpath.XPathFactory;
44 import java.io.IOException;
45
46 /**
47 * Runs the first Activity shown in the top-level launcher as determined by its Intent filters.
48 * <p>
49 * Android provides a component-based architecture, which means that there is no "main" function which serves as an
50 * entry point to the APK. There's an homogeneous collection of Activity(es), Service(s), Receiver(s), etc.
51 * </p>
52 * <p>
53 * The Android top-level launcher (whose purpose is to allow users to launch other applications) uses the Intent
54 * resolution mechanism to determine which Activity(es) to show to the end user. Such activities are identified by at
55 * least:
56 * <ul>
57 * <li>Action type: <code>android.intent.action.MAIN</code></li>
58 * <li>Category: <code>android.intent.category.LAUNCHER</code></li>
59 * </ul>
60 * </p>
61 * <p>And are declared in <code>AndroidManifest.xml</code> as such:</p>
62 * <pre>
63 * <activity android:name=".ExampleActivity">
64 * <intent-filter>
65 * <action android:name="android.intent.action.MAIN" />
66 * <category android:name="android.intent.category.LAUNCHER" />
67 * </intent-filter>
68 * </activity>
69 * </pre>
70 * <p>
71 * This {@link Mojo} will try to to launch the first activity of this kind found in <code>AndroidManifest.xml</code>. In
72 * case multiple activities satisfy the requirements listed above only the first declared one is run. In case there are
73 * no "Launcher activities" declared in the manifest or no activities declared at all, this goal aborts throwing an
74 * error.
75 * </p>
76 * <p>
77 * The device parameter is taken into consideration so potentially the Activity found is started on all attached
78 * devices. The application will NOT be deployed and running will silently fail if the application is not deployed.
79 * </p>
80 *
81 * @author Lorenzo Villani <lorenzo@villani.me>
82 * @author Manfred Mosr <manfred@simpligility.com>
83 * @goal run
84 * @see "http://developer.android.com/guide/topics/fundamentals.html"
85 * @see "http://developer.android.com/guide/topics/intents/intents-filters.html"
86 */
87 public class RunMojo extends AbstractAndroidMojo
88 {
89
90 /**
91 * <p>The configuration for the run goal can be set up in the plugin configuration in the pom file as:</p>
92 * <pre>
93 * <run>
94 * <debug>true</debug>
95 * </run>
96 * </pre>
97 * <p>The <code><debug></code> parameter is optional and defaults to false.
98 * <p>The debug parameter can also be configured as property in the pom or settings file
99 * <pre>
100 * <properties>
101 * <android.run.debug>true</android.run.debug>
102 * </properties>
103 * </pre>
104 * or from command-line with parameter <code>-Dandroid.run.debug=true</code>.</p>
105 *
106 * @parameter
107 */
108 @ConfigPojo
109 private Run run;
110
111 /**
112 * Debug parameter for the the run goal. If true, the device or emulator will pause execution of the process at
113 * startup to wait for a debugger to connect. Also see the "run" parameter documentation. Default value is false.
114 *
115 * @parameter expression="${android.run.debug}"
116 */
117 protected Boolean runDebug;
118
119 /* the value for the debug flag after parsing pom and parameter */
120 @PullParameter( defaultValue = "false" )
121 private Boolean parsedDebug;
122
123 /**
124 * Holds information about the "Launcher" activity.
125 *
126 * @author Lorenzo Villani
127 */
128 private static class LauncherInfo
129 {
130 private String packageName;
131
132 private String activity;
133
134 public String getPackageName()
135 {
136 return packageName;
137 }
138
139 public void setPackageName( String packageName )
140 {
141 this.packageName = packageName;
142 }
143
144 public String getActivity()
145 {
146 return activity;
147 }
148
149 public void setActivity( String activity )
150 {
151 this.activity = activity;
152 }
153 }
154
155 // ----------------------------------------------------------------------
156 // Public methods
157 // ----------------------------------------------------------------------
158
159 /**
160 * {@inheritDoc}
161 */
162 @Override
163 public void execute() throws MojoExecutionException, MojoFailureException
164 {
165 try
166 {
167 LauncherInfo launcherInfo;
168
169 launcherInfo = getLauncherActivity();
170
171 ConfigHandler configHandler = new ConfigHandler( this );
172 configHandler.parseConfiguration();
173
174 launch( launcherInfo );
175 }
176 catch ( Exception ex )
177 {
178 throw new MojoFailureException( "Unable to run launcher Activity", ex );
179 }
180 }
181
182 // ----------------------------------------------------------------------
183 // Private methods
184 // ----------------------------------------------------------------------
185
186 /**
187 * Gets the first "Launcher" Activity by running an XPath query on <code>AndroidManifest.xml</code>.
188 *
189 * @return A {@link LauncherInfo}
190 * @throws MojoExecutionException
191 * @throws ParserConfigurationException
192 * @throws IOException
193 * @throws SAXException
194 * @throws XPathExpressionException
195 * @throws ActivityNotFoundException
196 */
197 private LauncherInfo getLauncherActivity()
198 throws ParserConfigurationException, SAXException, IOException, XPathExpressionException,
199 MojoFailureException
200 {
201 Document document;
202 DocumentBuilder documentBuilder;
203 DocumentBuilderFactory documentBuilderFactory;
204 Object result;
205 XPath xPath;
206 XPathExpression xPathExpression;
207 XPathFactory xPathFactory;
208
209 //
210 // Setup JAXP stuff
211 //
212 documentBuilderFactory = DocumentBuilderFactory.newInstance();
213
214 documentBuilder = documentBuilderFactory.newDocumentBuilder();
215
216 document = documentBuilder.parse( androidManifestFile );
217
218 xPathFactory = XPathFactory.newInstance();
219
220 xPath = xPathFactory.newXPath();
221
222 xPathExpression = xPath.compile(
223 "//manifest/application/activity/intent-filter[action[@name=\"android.intent.action.MAIN\"] "
224 + "and category[@name=\"android.intent.category.LAUNCHER\"]]/.." );
225
226 //
227 // Run XPath query
228 //
229 result = xPathExpression.evaluate( document, XPathConstants.NODESET );
230
231 if ( result instanceof NodeList )
232 {
233 NodeList activities;
234
235 activities = ( NodeList ) result;
236
237 if ( activities.getLength() > 0 )
238 {
239 // Grab the first declared Activity
240 LauncherInfo launcherInfo;
241
242 launcherInfo = new LauncherInfo();
243 String activityName = activities.item( 0 ).getAttributes().getNamedItem( "android:name" )
244 .getNodeValue();
245
246 if ( ! activityName.contains( "." ) )
247 {
248 activityName = "." + activityName;
249 }
250
251 if ( activityName.startsWith( "." ) )
252 {
253 String packageName = document.getElementsByTagName( "manifest" ).item( 0 ).getAttributes()
254 .getNamedItem( "package" ).getNodeValue();
255 activityName = packageName + activityName;
256 }
257
258 launcherInfo.activity = activityName;
259
260 launcherInfo.packageName = renameManifestPackage != null
261 ? renameManifestPackage
262 : document.getDocumentElement().getAttribute( "package" ).toString();
263
264 return launcherInfo;
265 }
266 else
267 {
268 // If we get here, we couldn't find a launcher activity.
269 throw new MojoFailureException( "Could not find a launcher activity in manifest" );
270 }
271 }
272 else
273 {
274 // If we get here we couldn't find any Activity
275 throw new MojoFailureException( "Could not find any activity in manifest" );
276 }
277 }
278
279 /**
280 * Executes the "Launcher activity".
281 *
282 * @param info A {@link LauncherInfo}.
283 * @throws MojoFailureException
284 * @throws MojoExecutionException
285 */
286 private void launch( final LauncherInfo info ) throws MojoExecutionException, MojoFailureException
287 {
288 final String command;
289
290 command = String.format( "am start %s-n %s/%s", parsedDebug ? "-D " : "", info.packageName, info.activity );
291
292 doWithDevices( new DeviceCallback()
293 {
294 @Override
295 public void doWithDevice( IDevice device ) throws MojoExecutionException, MojoFailureException
296 {
297 String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
298
299 try
300 {
301 getLog().info( deviceLogLinePrefix + "Attempting to start " + info.packageName + "/"
302 + info.activity );
303
304 CollectingOutputReceiver shellOutput = new CollectingOutputReceiver();
305 device.executeShellCommand( command, shellOutput );
306 if ( shellOutput.getOutput().contains( "Error" ) )
307 {
308 throw new MojoFailureException( shellOutput.getOutput() );
309 }
310 }
311 catch ( IOException ex )
312 {
313 throw new MojoFailureException( deviceLogLinePrefix + "Input/Output error", ex );
314 }
315 catch ( TimeoutException ex )
316 {
317 throw new MojoFailureException( deviceLogLinePrefix + "Command timeout", ex );
318 }
319 catch ( AdbCommandRejectedException ex )
320 {
321 throw new MojoFailureException( deviceLogLinePrefix + "ADB rejected the command", ex );
322 }
323 catch ( ShellCommandUnresponsiveException ex )
324 {
325 throw new MojoFailureException( deviceLogLinePrefix + "Unresponsive command", ex );
326 }
327 }
328 } );
329 }
330 }