View Javadoc

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   * &lt;activity android:name=".ExampleActivity"&gt;
64   *     &lt;intent-filter&gt;
65   *         &lt;action android:name="android.intent.action.MAIN" /&gt;
66   *         &lt;category android:name="android.intent.category.LAUNCHER" /&gt;
67   *     &lt;/intent-filter&gt;
68   * &lt;/activity&gt;
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       * &lt;run&gt;
94       *     &lt;debug&gt;true&lt;/debug&gt;
95       * &lt;/run&gt;
96       * </pre>
97       * <p>The <code>&lt;debug&gt;</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      * &lt;properties&gt;
101      *     &lt;android.run.debug&gt;true&lt;/android.run.debug&gt;
102      * &lt;/properties&gt;
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 }