View Javadoc

1   /*
2    * Copyright (C) 2009-2011 Jayway AB
3    * Copyright (C) 2007-2008 JVending Masa
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.jayway.maven.plugins.android;
18  
19  import com.android.ddmlib.AndroidDebugBridge;
20  import com.android.ddmlib.IDevice;
21  import com.android.ddmlib.InstallException;
22  import com.jayway.maven.plugins.android.common.AetherHelper;
23  import com.jayway.maven.plugins.android.common.AndroidExtension;
24  import com.jayway.maven.plugins.android.common.DeviceHelper;
25  import com.jayway.maven.plugins.android.config.ConfigPojo;
26  import com.jayway.maven.plugins.android.configuration.Ndk;
27  import com.jayway.maven.plugins.android.configuration.Sdk;
28  import org.apache.commons.jxpath.JXPathContext;
29  import org.apache.commons.jxpath.JXPathNotFoundException;
30  import org.apache.commons.jxpath.xml.DocumentContainer;
31  import org.apache.commons.lang.StringUtils;
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.project.MavenProjectHelper;
39  import org.codehaus.plexus.util.DirectoryScanner;
40  import org.sonatype.aether.RepositorySystem;
41  import org.sonatype.aether.RepositorySystemSession;
42  import org.sonatype.aether.repository.RemoteRepository;
43  
44  import java.io.File;
45  import java.net.MalformedURLException;
46  import java.net.URL;
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.LinkedHashSet;
50  import java.util.List;
51  import java.util.Scanner;
52  import java.util.Set;
53  import java.util.concurrent.atomic.AtomicBoolean;
54  
55  import static com.jayway.maven.plugins.android.common.AndroidExtension.APK;
56  import static org.apache.commons.lang.StringUtils.isBlank;
57  
58  /**
59   * Contains common fields and methods for android mojos.
60   *
61   * @author hugo.josefson@jayway.com
62   * @author Manfred Moser <manfred@simpligility.com>
63   */
64  public abstract class AbstractAndroidMojo extends AbstractMojo
65  {
66  
67      public static final List<String> SUPPORTED_PACKAGING_TYPES = new ArrayList<String>();
68  
69      static
70      {
71          SUPPORTED_PACKAGING_TYPES.add( AndroidExtension.APK );
72      }
73  
74      /**
75       * Android Debug Bridge initialization timeout in milliseconds.
76       */
77      private static final long ADB_TIMEOUT_MS = 60L * 1000;
78  
79      /**
80       * The <code>ANDROID_NDK_HOME</code> environment variable name.
81       */
82      public static final String ENV_ANDROID_NDK_HOME = "ANDROID_NDK_HOME";
83      
84      /**
85       * <p>The Android NDK to use.</p>
86       * <p>Looks like this:</p>
87       * <pre>
88       * &lt;ndk&gt;
89       *     &lt;path&gt;/opt/android-ndk-r4&lt;/path&gt;
90       * &lt;/ndk&gt;
91       * </pre>
92       * <p>The <code>&lt;path&gt;</code> parameter is optional. The default is the setting of the ANDROID_NDK_HOME
93       * environment variable. The parameter can be used to override this setting with a different environment variable
94       * like this:</p>
95       * <pre>
96       * &lt;ndk&gt;
97       *     &lt;path&gt;${env.ANDROID_NDK_HOME}&lt;/path&gt;
98       * &lt;/ndk&gt;
99       * </pre>
100      * <p>or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameter
101      * <code>-Dandroid.ndk.path</code>.</p>
102      *
103      * @parameter
104      */
105     @ConfigPojo( prefix = "ndk" )
106     private Ndk ndk;
107 
108     /**
109      * The maven project.
110      *
111      * @parameter expression="${project}"
112      * @required
113      * @readonly
114      */
115     protected MavenProject project;
116 
117     /**
118      * The maven session.
119      *
120      * @parameter expression="${session}"
121      * @required
122      * @readonly
123      */
124     protected MavenSession session;
125 
126 
127     /**
128      * The java sources directory.
129      *
130      * @parameter default-value="${project.build.sourceDirectory}"
131      * @readonly
132      */
133     protected File sourceDirectory;
134 
135     /**
136      * The android resources directory.
137      *
138      * @parameter default-value="${project.basedir}/res"
139      */
140     protected File resourceDirectory;
141 
142     /**
143      * <p>Root folder containing native libraries to include in the application package.</p>
144      *
145      * @parameter expression="${android.nativeLibrariesDirectory}" default-value="${project.basedir}/libs"
146      */
147     protected File nativeLibrariesDirectory;
148 
149 
150     /**
151      * The android resources overlay directory. This will be overridden
152      * by resourceOverlayDirectories if present.
153      *
154      * @parameter default-value="${project.basedir}/res-overlay"
155      */
156     protected File resourceOverlayDirectory;
157 
158     /**
159      * The android resources overlay directories. If this is specified,
160      * the {@link #resourceOverlayDirectory} parameter will be ignored.
161      *
162      * @parameter
163      */
164     protected File[] resourceOverlayDirectories;
165 
166     /**
167      * The android assets directory.
168      *
169      * @parameter default-value="${project.basedir}/assets"
170      */
171     protected File assetsDirectory;
172 
173     /**
174      * The <code>AndroidManifest.xml</code> file.
175      *
176      * @parameter default-value="${project.basedir}/AndroidManifest.xml"
177      */
178     protected File androidManifestFile;
179 
180     /**
181      * <p>A possibly new package name for the application. This value will be passed on to the aapt
182      * parameter --rename-manifest-package. Look to aapt for more help on this. </p>
183      *
184      * @parameter expression="${android.renameManifestPackage}"
185      */
186     protected String renameManifestPackage;
187 
188     /**
189      * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies"
190      * @readonly
191      */
192     protected File extractedDependenciesDirectory;
193 
194     /**
195      * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/res"
196      * @readonly
197      */
198     protected File extractedDependenciesRes;
199     /**
200      * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/assets"
201      * @readonly
202      */
203     protected File extractedDependenciesAssets;
204     /**
205      * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/java"
206      * @readonly
207      */
208     protected File extractedDependenciesJavaSources;
209     /**
210      * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/resources"
211      * @readonly
212      */
213     protected File extractedDependenciesJavaResources;
214 
215     /**
216      * The combined resources directory. This will contain both the resources found in "res" as well as any resources
217      * contained in a apksources dependency.
218      *
219      * @parameter expression="${project.build.directory}/generated-sources/combined-resources/res"
220      * @readonly
221      */
222     protected File combinedRes;
223 
224     /**
225      * The combined assets directory. This will contain both the assets found in "assets" as well as any assets
226      * contained in a apksources dependency.
227      *
228      * @parameter expression="${project.build.directory}/generated-sources/combined-assets/assets"
229      * @readonly
230      */
231     protected File combinedAssets;
232 
233     /**
234      * Extract the apklib dependencies here
235      *
236      * @parameter expression="${project.build.directory}/unpack/apklibs"
237      * @readonly
238      */
239     protected File unpackedApkLibsDirectory;
240 
241     /**
242      * Specifies which the serial number of the device to connect to. Using the special values "usb" or
243      * "emulator" is also valid. "usb" will connect to all actual devices connected (via usb). "emulator" will
244      * connect to all emulators connected. Multiple devices will be iterated over in terms of goals to run. All
245      * device interaction goals support this so you can e.. deploy the apk to all attached emulators and devices.
246      * Goals supporting this are devices, deploy, undeploy, redeploy, pull, push and instrument.
247      *
248      * @parameter expression="${android.device}"
249      */
250     protected String device;
251 
252     /**
253      * A selection of configurations to be included in the APK as a comma separated list. This will limit the
254      * configurations for a certain type. For example, specifying <code>hdpi</code> will exclude all resource folders
255      * with the <code>mdpi</code> or <code>ldpi</code> modifiers, but won't affect language or orientation modifiers.
256      * For more information about this option, look in the aapt command line help.
257      *
258      * @parameter expression="${android.configurations}"
259      */
260     protected String configurations;
261 
262     /**
263      * A list of extra arguments that must be passed to aapt.
264      *
265      * @parameter expression="${android.aaptExtraArgs}"
266      */
267     protected String[] aaptExtraArgs;
268 
269     /**
270      * Automatically create a ProGuard configuration file that will guard Activity classes and the like that are 
271      * defined in the AndroidManifest.xml. This files is then automatically used in the proguard mojo execution, 
272      * if enabled.
273      *
274      * @parameter expression="${android.proguardFile}"
275      */
276     protected File proguardFile;
277 
278     /**
279      * Decides whether the Apk should be generated or not. If set to false, dx and apkBuilder will not run. This is
280      * probably most useful for a project used to generate apk sources to be inherited into another application
281      * project.
282      *
283      * @parameter expression="${android.generateApk}" default-value="true"
284      */
285     protected boolean generateApk;
286 
287     /**
288      * The entry point to Aether, i.e. the component doing all the work.
289      *
290      * @component
291      */
292     protected RepositorySystem repoSystem;
293 
294     /**
295      * The current repository/network configuration of Maven.
296      *
297      * @parameter default-value="${repositorySystemSession}"
298      * @readonly
299      */
300     protected RepositorySystemSession repoSession;
301 
302     /**
303      * The project's remote repositories to use for the resolution of project dependencies.
304      *
305      * @parameter default-value="${project.remoteProjectRepositories}"
306      * @readonly
307      */
308     protected List<RemoteRepository> projectRepos;
309 
310     /**
311      * Generates R.java into a different package.
312      *
313      * @parameter expression="${android.customPackage}"
314      */
315     protected String customPackage;
316 
317     /**
318      * Maven ProjectHelper.
319      *
320      * @component
321      * @readonly
322      */
323     protected MavenProjectHelper projectHelper;
324 
325     /**
326      * <p>The Android SDK to use.</p>
327      * <p>Looks like this:</p>
328      * <pre>
329      * &lt;sdk&gt;
330      *     &lt;path&gt;/opt/android-sdk-linux&lt;/path&gt;
331      *     &lt;platform&gt;2.1&lt;/platform&gt;
332      * &lt;/sdk&gt;
333      * </pre>
334      * <p>The <code>&lt;platform&gt;</code> parameter is optional, and corresponds to the
335      * <code>platforms/android-*</code> directories in the Android SDK directory. Default is the latest available
336      * version, so you only need to set it if you for example want to use platform 1.5 but also have e.g. 2.2 installed.
337      * Has no effect when used on an Android SDK 1.1. The parameter can also be coded as the API level. Therefore valid
338      * values are 1.1, 1.5, 1.6, 2.0, 2.01, 2.1, 2.2 and so as well as 3, 4, 5, 6, 7, 8... 16. If a platform/api level 
339      * is not installed on the machine an error message will be produced. </p>
340      * <p>The <code>&lt;path&gt;</code> parameter is optional. The default is the setting of the ANDROID_HOME
341      * environment variable. The parameter can be used to override this setting with a different environment variable
342      * like this:</p>
343      * <pre>
344      * &lt;sdk&gt;
345      *     &lt;path&gt;${env.ANDROID_SDK}&lt;/path&gt;
346      * &lt;/sdk&gt;
347      * </pre>
348      * <p>or just with a hard-coded absolute path. The parameters can also be configured from command-line with
349      * parameters <code>-Dandroid.sdk.path</code> and <code>-Dandroid.sdk.platform</code>.</p>
350      *
351      * @parameter
352      */
353     private Sdk sdk;
354 
355     /**
356      * <p>Parameter designed to pick up <code>-Dandroid.sdk.path</code> in case there is no pom with an
357      * <code>&lt;sdk&gt;</code> configuration tag.</p>
358      * <p>Corresponds to {@link com.jayway.maven.plugins.android.configuration.Sdk#path}.</p>
359      *
360      * @parameter expression="${android.sdk.path}"
361      * @readonly
362      */
363     private File sdkPath;
364 
365     /**
366      * <p>Parameter designed to pick up environment variable <code>ANDROID_HOME</code> in case
367      * <code>android.sdk.path</code> is not configured.</p>
368      *
369      * @parameter expression="${env.ANDROID_HOME}"
370      * @readonly
371      */
372     private String envAndroidHome;
373 
374     /**
375      * The <code>ANDROID_HOME</code> environment variable name.
376      */
377     public static final String ENV_ANDROID_HOME = "ANDROID_HOME";
378 
379     /**
380      * <p>Parameter designed to pick up <code>-Dandroid.sdk.platform</code> in case there is no pom with an
381      * <code>&lt;sdk&gt;</code> configuration tag.</p>
382      * <p>Corresponds to {@link com.jayway.maven.plugins.android.configuration.Sdk#platform}.</p>
383      *
384      * @parameter expression="${android.sdk.platform}"
385      * @readonly
386      */
387     private String sdkPlatform;
388 
389     /**
390      * <p>Whether to undeploy an apk from the device before deploying it.</p>
391      * <p/>
392      * <p>Only has effect when running <code>mvn android:deploy</code> in an Android application project manually, or
393      * when running <code>mvn integration-test</code> (or <code>mvn install</code>) in a project with instrumentation
394      * tests.
395      * </p>
396      * <p/>
397      * <p>It is useful to keep this set to <code>true</code> at all times, because if an apk with the same package was
398      * previously signed with a different keystore, and deployed to the device, deployment will fail becuase your
399      * keystore is different.</p>
400      *
401      * @parameter default-value=false
402      * expression="${android.undeployBeforeDeploy}"
403      */
404     protected boolean undeployBeforeDeploy;
405 
406     /**
407      * <p>Whether to attach the normal .jar file to the build, so it can be depended on by for example integration-tests
408      * which may then access {@code R.java} from this project.</p>
409      * <p>Only disable it if you know you won't need it for any integration-tests. Otherwise, leave it enabled.</p>
410      *
411      * @parameter default-value=true
412      * expression="${android.attachJar}"
413      */
414     protected boolean attachJar;
415 
416     /**
417      * <p>Whether to attach sources to the build, which can be depended on by other {@code apk} projects, for including
418      * them in their builds.</p>
419      * <p>Enabling this setting is only required if this project's source code and/or res(ources) will be included in
420      * other projects, using the Maven &lt;dependency&gt; tag.</p>
421      *
422      * @parameter default-value=false
423      * expression="${android.attachSources}"
424      */
425     protected boolean attachSources;
426 
427     /**
428      * <p>Parameter designed to pick up <code>-Dandroid.ndk.path</code> in case there is no pom with an
429      * <code>&lt;ndk&gt;</code> configuration tag.</p>
430      * <p>Corresponds to {@link com.jayway.maven.plugins.android.configuration.Ndk#path}.</p>
431      *
432      * @parameter expression="${android.ndk.path}"
433      * @readonly
434      */
435     private File ndkPath;
436 
437     /**
438      * Whether to create a release build (default is false / debug build). This affect BuildConfig generation 
439      * and apk generation at this stage, but should probably affect other aspects of the build.
440      * @parameter expression="${android.release}" default-value="false"
441      */
442     protected boolean release;
443 
444 
445     /**
446      *
447      */
448     private static final Object ADB_LOCK = new Object();
449     /**
450      *
451      */
452     private static boolean adbInitialized = false;
453 
454     /**
455      * Which dependency scopes should not be included when unpacking dependencies into the apk.
456      */
457     protected static final List<String> EXCLUDED_DEPENDENCY_SCOPES = Arrays.asList( "provided", "system", "import" );
458 
459     /**
460      * @return a {@code Set} of dependencies which may be extracted and otherwise included in other artifacts. Never
461      *         {@code null}. This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} scopes.
462      */
463     protected Set<Artifact> getRelevantCompileArtifacts()
464     {
465         final List<Artifact> allArtifacts = ( List<Artifact> ) project.getCompileArtifacts();
466         final Set<Artifact> results = filterOutIrrelevantArtifacts( allArtifacts );
467         return results;
468     }
469 
470     /**
471      * @return a {@code Set} of direct project dependencies. Never {@code null}. This excludes artifacts of the {@code
472      *         EXCLUDED_DEPENDENCY_SCOPES} scopes.
473      */
474     protected Set<Artifact> getRelevantDependencyArtifacts()
475     {
476         final Set<Artifact> allArtifacts = ( Set<Artifact> ) project.getDependencyArtifacts();
477         final Set<Artifact> results = filterOutIrrelevantArtifacts( allArtifacts );
478         return results;
479     }
480 
481     /**
482      * @return a {@code List} of all project dependencies. Never {@code null}. This excludes artifacts of the {@code
483      *         EXCLUDED_DEPENDENCY_SCOPES} scopes. And
484      *         This should maintain dependency order to comply with library project resource precedence.
485      */
486     protected Set<Artifact> getAllRelevantDependencyArtifacts()
487     {
488         final Set<Artifact> allArtifacts = ( Set<Artifact> ) project.getArtifacts();
489         final Set<Artifact> results = filterOutIrrelevantArtifacts( allArtifacts );
490         return results;
491     }
492 
493     /**
494      *
495      * @param allArtifacts
496      * @return
497      */
498     private Set<Artifact> filterOutIrrelevantArtifacts( Iterable<Artifact> allArtifacts )
499     {
500         final Set<Artifact> results = new LinkedHashSet<Artifact>();
501         for ( Artifact artifact : allArtifacts )
502         {
503             if ( artifact == null )
504             {
505                 continue;
506             }
507 
508             if ( EXCLUDED_DEPENDENCY_SCOPES.contains( artifact.getScope() ) )
509             {
510                 continue;
511             }
512 
513             if ( "apk".equalsIgnoreCase( artifact.getType() ) )
514             {
515                 continue;
516             }
517 
518             results.add( artifact );
519         }
520         return results;
521     }
522 
523     /**
524      * Attempts to resolve an {@link Artifact} to a {@link File}.
525      *
526      * @param artifact to resolve
527      * @return a {@link File} to the resolved artifact, never <code>null</code>.
528      * @throws MojoExecutionException if the artifact could not be resolved.
529      */
530     protected File resolveArtifactToFile( Artifact artifact ) throws MojoExecutionException
531     {
532         Artifact resolvedArtifact = AetherHelper.resolveArtifact( artifact, repoSystem, repoSession, projectRepos );
533         final File jar = resolvedArtifact.getFile();
534         if ( jar == null )
535         {
536             throw new MojoExecutionException( "Could not resolve artifact " + artifact.getId()
537                     + ". Please install it with \"mvn install:install-file ...\" or deploy it to a repository "
538                     + "with \"mvn deploy:deploy-file ...\"" );
539         }
540         return jar;
541     }
542 
543     /**
544      * Initialize the Android Debug Bridge and wait for it to start. Does not reinitialize it if it has
545      * already been initialized (that would through and IllegalStateException...). Synchronized sine
546      * the init call in the library is also synchronized .. just in case.
547      *
548      * @return
549      */
550     protected AndroidDebugBridge initAndroidDebugBridge() throws MojoExecutionException
551     {
552         synchronized ( ADB_LOCK )
553         {
554             if ( ! adbInitialized )
555             {
556                 AndroidDebugBridge.init( false );
557                 adbInitialized = true;
558             }
559             AndroidDebugBridge androidDebugBridge = AndroidDebugBridge
560                     .createBridge( getAndroidSdk().getAdbPath(), false );
561             waitUntilConnected( androidDebugBridge );
562             return androidDebugBridge;
563         }
564     }
565 
566     /**
567      * Run a wait loop until adb is connected or trials run out. This method seems to work more reliably then using a
568      * listener.
569      *
570      * @param adb
571      */
572     private void waitUntilConnected( AndroidDebugBridge adb )
573     {
574         int trials = 10;
575         final int connectionWaitTime = 50;
576         while ( trials > 0 )
577         {
578             try
579             {
580                 Thread.sleep( connectionWaitTime );
581             }
582             catch ( InterruptedException e )
583             {
584                 e.printStackTrace();
585             }
586             if ( adb.isConnected() )
587             {
588                 break;
589             }
590             trials--;
591         }
592     }
593 
594     /**
595      * Wait for the Android Debug Bridge to return an initial device list.
596      *
597      * @param androidDebugBridge
598      * @throws MojoExecutionException
599      */
600     protected void waitForInitialDeviceList( final AndroidDebugBridge androidDebugBridge ) throws MojoExecutionException
601     {
602         if ( ! androidDebugBridge.hasInitialDeviceList() )
603         {
604             getLog().info( "Waiting for initial device list from the Android Debug Bridge" );
605             long limitTime = System.currentTimeMillis() + ADB_TIMEOUT_MS;
606             while ( ! androidDebugBridge.hasInitialDeviceList() && ( System.currentTimeMillis() < limitTime ) )
607             {
608                 try
609                 {
610                     Thread.sleep( 1000 );
611                 }
612                 catch ( InterruptedException e )
613                 {
614                     throw new MojoExecutionException(
615                             "Interrupted waiting for initial device list from Android Debug Bridge" );
616                 }
617             }
618             if ( ! androidDebugBridge.hasInitialDeviceList() )
619             {
620                 getLog().error( "Did not receive initial device list from the Android Debug Bridge." );
621             }
622         }
623     }
624 
625     /**
626      * Deploys an apk file to a connected emulator or usb device.
627      *
628      * @param apkFile the file to deploy
629      * @throws MojoExecutionException If there is a problem deploying the apk file.
630      */
631     protected void deployApk( final File apkFile ) throws MojoExecutionException, MojoFailureException
632     {
633         if ( undeployBeforeDeploy )
634         {
635             undeployApk( apkFile );
636         }
637         doWithDevices( new DeviceCallback()
638         {
639             public void doWithDevice( final IDevice device ) throws MojoExecutionException
640             {
641                 String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
642                 try
643                 {
644                     String result = device.installPackage( apkFile.getAbsolutePath(), true );
645                     // according to the docs for installPackage, not null response is error
646                     if ( result != null )
647                     {
648                         throw new MojoExecutionException( deviceLogLinePrefix 
649                                 + "Install of " + apkFile.getAbsolutePath()
650                                 + " failed - [" + result + "]" );
651                     }
652                     getLog().info( deviceLogLinePrefix + "Successfully installed " + apkFile.getAbsolutePath() + " to "
653                             + DeviceHelper.getDescriptiveName( device ) );
654                 }
655                 catch ( InstallException e )
656                 {
657                     throw new MojoExecutionException( deviceLogLinePrefix + "Install of " + apkFile.getAbsolutePath() 
658                             + " failed.", e );
659                 }
660             }
661         } );
662     }
663 
664     /**
665      *
666      * @throws MojoExecutionException
667      * @throws MojoFailureException
668      */
669     protected void deployDependencies() throws MojoExecutionException, MojoFailureException
670     {
671         Set<Artifact> directDependentArtifacts = project.getDependencyArtifacts();
672         if ( directDependentArtifacts != null )
673         {
674             for ( Artifact artifact : directDependentArtifacts )
675             {
676                 String type = artifact.getType();
677                 if ( type.equals( APK ) )
678                 {
679                     getLog().debug( "Detected apk dependency " + artifact + ". Will resolve and deploy to device..." );
680                     final File targetApkFile = resolveArtifactToFile( artifact );
681                     if ( undeployBeforeDeploy )
682                     {
683                         getLog().debug( "Attempting undeploy of " + targetApkFile + " from device..." );
684                         undeployApk( targetApkFile );
685                     }
686                     getLog().debug( "Deploying " + targetApkFile + " to device..." );
687                     deployApk( targetApkFile );
688                 }
689             }
690         }
691     }
692 
693     /**
694      *
695      * @throws MojoExecutionException
696      * @throws MojoFailureException
697      */
698     protected void deployBuiltApk() throws MojoExecutionException, MojoFailureException
699     {
700         // If we're not on a supported packaging with just skip (Issue 112)
701         // http://code.google.com/p/maven-android-plugin/issues/detail?id=112
702         if ( ! SUPPORTED_PACKAGING_TYPES.contains( project.getPackaging() ) )
703         {
704             getLog().info( "Skipping deployment on " + project.getPackaging() );
705             return;
706         }
707         File apkFile = new File( project.getBuild().getDirectory(), project.getBuild().getFinalName() + "." + APK );
708         deployApk( apkFile );
709     }
710 
711 
712     /**
713      * Performs the callback action on the devices determined by
714      * {@link #shouldDoWithThisDevice(com.android.ddmlib.IDevice)}
715      *
716      * @param deviceCallback the action to perform on each device
717      * @throws org.apache.maven.plugin.MojoExecutionException
718      *          in case there is a problem
719      * @throws org.apache.maven.plugin.MojoFailureException
720      *          in case there is a problem
721      */
722     protected void doWithDevices( final DeviceCallback deviceCallback )
723             throws MojoExecutionException, MojoFailureException
724     {
725         final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
726 
727         if ( !androidDebugBridge.isConnected() )
728         {
729             throw new MojoExecutionException( "Android Debug Bridge is not connected." );
730         }
731 
732         waitForInitialDeviceList( androidDebugBridge );
733         List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
734         int numberOfDevices = devices.size();
735         getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
736         if ( devices.size() == 0 )
737         {
738             throw new MojoExecutionException( "No online devices attached." );
739         }
740 
741         boolean shouldRunOnAllDevices = StringUtils.isBlank( device );
742         if ( shouldRunOnAllDevices )
743         {
744             getLog().info( "android.device parameter not set, using all attached devices" );
745         }
746         else
747         {
748             getLog().info( "android.device parameter set to " + device );
749         }
750 
751         ArrayList<DoThread> doThreads = new ArrayList<DoThread>();
752         for ( final IDevice idevice : devices )
753         {
754             if ( shouldRunOnAllDevices )
755             {
756                 String deviceType = idevice.isEmulator() ? "Emulator " : "Device ";
757                 getLog().info( deviceType + DeviceHelper.getDescriptiveName( idevice ) + " found." );
758             }
759             if ( shouldRunOnAllDevices || shouldDoWithThisDevice( idevice ) )
760             {
761                 DoThread deviceDoThread = new DoThread() {
762                     public void runDo() throws MojoFailureException, MojoExecutionException
763                     {
764                         deviceCallback.doWithDevice( idevice );
765                     }
766                 };
767                 doThreads.add( deviceDoThread );
768                 deviceDoThread.start();
769             }
770         }
771 
772         joinAllThreads( doThreads );
773         throwAnyDoThreadErrors( doThreads );
774 
775         if ( ! shouldRunOnAllDevices && doThreads.isEmpty() )
776         {
777             throw new MojoExecutionException( "No device found for android.device=" + device );
778         }
779     }
780 
781     private void joinAllThreads( ArrayList<DoThread> doThreads )
782     {
783         for ( Thread deviceDoThread : doThreads )
784         {
785             try
786             {
787                 deviceDoThread.join();
788             }
789             catch ( InterruptedException e )
790             {
791                 new MojoExecutionException( "Thread#join error for device: " + device );
792             }
793         }
794     }
795 
796     private void throwAnyDoThreadErrors( ArrayList<DoThread> doThreads ) throws MojoExecutionException,
797             MojoFailureException
798     {
799         for ( DoThread deviceDoThread : doThreads )
800         {
801             if ( deviceDoThread.failure != null )
802             {
803                 throw deviceDoThread.failure;
804             }
805             if ( deviceDoThread.execution != null )
806             {
807                 throw deviceDoThread.execution;
808             }
809         }
810     }
811 
812     /**
813      * Determines if this {@link IDevice}(s) should be used
814      *
815      * @param idevice the device to check
816      * @return if the device should be used
817      * @throws org.apache.maven.plugin.MojoExecutionException
818      *          in case there is a problem
819      * @throws org.apache.maven.plugin.MojoFailureException
820      *          in case there is a problem
821      */
822     private boolean shouldDoWithThisDevice( IDevice idevice ) throws MojoExecutionException, MojoFailureException
823     {
824         // use specified device or all emulators or all devices
825         if ( "emulator".equals( device ) && idevice.isEmulator() )
826         {
827             return true;
828         }
829 
830         if ( "usb".equals( device ) && ! idevice.isEmulator() )
831         {
832             return true;
833         }
834 
835         if ( idevice.isEmulator() && ( device.equalsIgnoreCase( idevice.getAvdName() ) || device
836                 .equalsIgnoreCase( idevice.getSerialNumber() ) ) )
837         {
838             return true;
839         }
840 
841         if ( ! idevice.isEmulator() && device.equals( idevice.getSerialNumber() ) )
842         {
843             return true;
844         }
845 
846         return false;
847     }
848 
849     /**
850      * Undeploys an apk from a connected emulator or usb device. Also deletes the application's data and cache
851      * directories on the device.
852      *
853      * @param apkFile the file to undeploy
854      * @return <code>true</code> if successfully undeployed, <code>false</code> otherwise.
855      */
856     protected boolean undeployApk( File apkFile ) throws MojoExecutionException, MojoFailureException
857     {
858         final String packageName;
859         packageName = extractPackageNameFromApk( apkFile );
860         return undeployApk( packageName );
861     }
862 
863     /**
864      * Undeploys an apk, specified by package name, from a connected emulator
865      * or usb device. Also deletes the application's data and cache
866      * directories on the device.
867      *
868      * @param packageName the package name to undeploy.
869      * @return <code>true</code> if successfully undeployed, <code>false</code> otherwise.
870      */
871     protected boolean undeployApk( final String packageName ) throws MojoExecutionException, MojoFailureException
872     {
873 
874         final AtomicBoolean result = new AtomicBoolean( true ); // if no devices are present, it counts as successful
875 
876         doWithDevices( new DeviceCallback()
877         {
878             public void doWithDevice( final IDevice device ) throws MojoExecutionException
879             {
880                 String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
881                 try
882                 {
883                     device.uninstallPackage( packageName );
884                     getLog().info( deviceLogLinePrefix + "Successfully uninstalled " + packageName + " from "
885                             + DeviceHelper.getDescriptiveName( device ) );
886                     result.set( true );
887                 }
888                 catch ( InstallException e )
889                 {
890                     result.set( false );
891                     throw new MojoExecutionException( deviceLogLinePrefix + "Uninstall of " + packageName 
892                             + " failed.", e );
893                 }
894             }
895         } );
896 
897         return result.get();
898     }
899 
900     /**
901      * Extracts the package name from an apk file.
902      *
903      * @param apkFile apk file to extract package name from.
904      * @return the package name from inside the apk file.
905      */
906     protected String extractPackageNameFromApk( File apkFile ) throws MojoExecutionException
907     {
908         CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
909         executor.setLogger( this.getLog() );
910         List<String> commands = new ArrayList<String>();
911         commands.add( "dump" );
912         commands.add( "xmltree" );
913         commands.add( apkFile.getAbsolutePath() );
914         commands.add( "AndroidManifest.xml" );
915         getLog().info( getAndroidSdk().getAaptPath() + " " + commands.toString() );
916         try
917         {
918             executor.executeCommand( getAndroidSdk().getAaptPath(), commands, false );
919             final String xmlTree = executor.getStandardOut();
920             return extractPackageNameFromAndroidManifestXmlTree( xmlTree );
921         }
922         catch ( ExecutionException e )
923         {
924             throw new MojoExecutionException(
925                     "Error while trying to figure out package name from inside apk file " + apkFile );
926         }
927         finally
928         {
929             String errout = executor.getStandardError();
930             if ( ( errout != null ) && ( errout.trim().length() > 0 ) )
931             {
932                 getLog().error( errout );
933             }
934         }
935     }
936 
937     /**
938      * Extracts the package name from an XmlTree dump of AndroidManifest.xml by the <code>aapt</code> tool.
939      *
940      * @param aaptDumpXmlTree output from <code>aapt dump xmltree &lt;apkFile&gt; AndroidManifest.xml
941      * @return the package name from inside the apkFile.
942      */
943     protected String extractPackageNameFromAndroidManifestXmlTree( String aaptDumpXmlTree )
944     {
945         final Scanner scanner = new Scanner( aaptDumpXmlTree );
946         // Finds the root element named "manifest".
947         scanner.findWithinHorizon( "^E: manifest", 0 );
948         // Finds the manifest element's attribute named "package".
949         scanner.findWithinHorizon( "  A: package=\"", 0 );
950         // Extracts the package value including the trailing double quote.
951         String packageName = scanner.next( ".*?\"" );
952         // Removes the double quote.
953         packageName = packageName.substring( 0, packageName.length() - 1 );
954         return packageName;
955     }
956 
957     /**
958      *
959      * @param androidManifestFile
960      * @return
961      * @throws MojoExecutionException
962      */
963     protected String extractPackageNameFromAndroidManifest( File androidManifestFile ) throws MojoExecutionException
964     {
965         final URL xmlURL;
966         try
967         {
968             xmlURL = androidManifestFile.toURI().toURL();
969         }
970         catch ( MalformedURLException e )
971         {
972             throw new MojoExecutionException(
973                     "Error while trying to figure out package name from inside AndroidManifest.xml file "
974                             + androidManifestFile, e );
975         }
976         final DocumentContainer documentContainer = new DocumentContainer( xmlURL );
977         final Object packageName = JXPathContext.newContext( documentContainer )
978                 .getValue( "manifest/@package", String.class );
979         return ( String ) packageName;
980     }
981 
982     /**
983      * Attempts to find the instrumentation test runner from inside the AndroidManifest.xml file.
984      *
985      * @param androidManifestFile the AndroidManifest.xml file to inspect.
986      * @return the instrumentation test runner declared in AndroidManifest.xml, or {@code null} if it is not declared.
987      * @throws MojoExecutionException
988      */
989     protected String extractInstrumentationRunnerFromAndroidManifest( File androidManifestFile )
990             throws MojoExecutionException
991     {
992         final URL xmlURL;
993         try
994         {
995             xmlURL = androidManifestFile.toURI().toURL();
996         }
997         catch ( MalformedURLException e )
998         {
999             throw new MojoExecutionException(
1000                     "Error while trying to figure out instrumentation runner from inside AndroidManifest.xml file "
1001                             + androidManifestFile, e );
1002         }
1003         final DocumentContainer documentContainer = new DocumentContainer( xmlURL );
1004         final Object instrumentationRunner;
1005         try
1006         {
1007             instrumentationRunner = JXPathContext.newContext( documentContainer )
1008                     .getValue( "manifest//instrumentation/@android:name", String.class );
1009         }
1010         catch ( JXPathNotFoundException e )
1011         {
1012             return null;
1013         }
1014         return ( String ) instrumentationRunner;
1015     }
1016 
1017     /**
1018      * TODO .. not used. Delete?
1019      *
1020      * @param baseDirectory
1021      * @param includes
1022      * @return
1023      * @throws MojoExecutionException
1024      */
1025     protected int deleteFilesFromDirectory( File baseDirectory, String... includes ) throws MojoExecutionException
1026     {
1027         final String[] files = findFilesInDirectory( baseDirectory, includes );
1028         if ( files == null )
1029         {
1030             return 0;
1031         }
1032 
1033         for ( String file : files )
1034         {
1035             final boolean successfullyDeleted = new File( baseDirectory, file ).delete();
1036             if ( ! successfullyDeleted )
1037             {
1038                 throw new MojoExecutionException( "Failed to delete \"" + file + "\"" );
1039             }
1040         }
1041         return files.length;
1042     }
1043 
1044     /**
1045      * Finds files.
1046      *
1047      * @param baseDirectory Directory to find files in.
1048      * @param includes      Ant-style include statements, for example <code>"** /*.aidl"</code> (but without the space
1049      *                      in the middle)
1050      * @return <code>String[]</code> of the files' paths and names, relative to <code>baseDirectory</code>. Empty
1051      *         <code>String[]</code> if <code>baseDirectory</code> does not exist.
1052      */
1053     protected String[] findFilesInDirectory( File baseDirectory, String... includes )
1054     {
1055         if ( baseDirectory.exists() )
1056         {
1057             DirectoryScanner directoryScanner = new DirectoryScanner();
1058             directoryScanner.setBasedir( baseDirectory );
1059 
1060             directoryScanner.setIncludes( includes );
1061             directoryScanner.addDefaultExcludes();
1062 
1063             directoryScanner.scan();
1064             String[] files = directoryScanner.getIncludedFiles();
1065             return files;
1066         }
1067         else
1068         {
1069             return new String[ 0 ];
1070         }
1071 
1072     }
1073 
1074 
1075     /**
1076      * <p>Returns the Android SDK to use.</p>
1077      * <p/>
1078      * <p>Current implementation looks for System property <code>android.sdk.path</code>, then
1079      * <code>&lt;sdk&gt;&lt;path&gt;</code> configuration in pom, then environment variable <code>ANDROID_HOME</code>.
1080      * <p/>
1081      * <p>This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is
1082      * based on available parameters. This method should be the only one you should need to look at to understand how
1083      * the Android SDK is chosen, and from where on disk.</p>
1084      *
1085      * @return the Android SDK to use.
1086      * @throws org.apache.maven.plugin.MojoExecutionException
1087      *          if no Android SDK path configuration is available at all.
1088      */
1089     protected AndroidSdk getAndroidSdk() throws MojoExecutionException
1090     {
1091         File chosenSdkPath;
1092         String chosenSdkPlatform;
1093 
1094         if ( sdk != null )
1095         {
1096             // An <sdk> tag exists in the pom.
1097 
1098             if ( sdk.getPath() != null )
1099             {
1100                 // An <sdk><path> tag is set in the pom.
1101 
1102                 chosenSdkPath = sdk.getPath();
1103             }
1104             else
1105             {
1106                 // There is no <sdk><path> tag in the pom.
1107 
1108                 if ( sdkPath != null )
1109                 {
1110                     // -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
1111                     chosenSdkPath = sdkPath;
1112                 }
1113                 else
1114                 {
1115                     // No -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
1116                     chosenSdkPath = new File( getAndroidHomeOrThrow() );
1117                 }
1118             }
1119 
1120             // Use <sdk><platform> from pom if it's there, otherwise try -Dandroid.sdk.platform from command line or
1121             // <properties><sdk.platform>...
1122             if ( ! isBlank( sdk.getPlatform() ) )
1123             {
1124                 chosenSdkPlatform = sdk.getPlatform();
1125             }
1126             else
1127             {
1128                 chosenSdkPlatform = sdkPlatform;
1129             }
1130         }
1131         else
1132         {
1133             // There is no <sdk> tag in the pom.
1134 
1135             if ( sdkPath != null )
1136             {
1137                 // -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
1138                 chosenSdkPath = sdkPath;
1139             }
1140             else
1141             {
1142                 // No -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
1143                 chosenSdkPath = new File( getAndroidHomeOrThrow() );
1144             }
1145 
1146             // Use any -Dandroid.sdk.platform from command line or <properties><sdk.platform>...
1147             chosenSdkPlatform = sdkPlatform;
1148         }
1149 
1150         return new AndroidSdk( chosenSdkPath, chosenSdkPlatform );
1151     }
1152 
1153     /**
1154      *
1155      * @return
1156      * @throws MojoExecutionException
1157      */
1158     private String getAndroidHomeOrThrow() throws MojoExecutionException
1159     {
1160         final String androidHome = System.getenv( ENV_ANDROID_HOME );
1161         if ( isBlank( androidHome ) )
1162         {
1163             throw new MojoExecutionException( "No Android SDK path could be found. You may configure it in the "
1164                     + "plugin configuration section in the pom file using <sdk><path>...</path></sdk> or "
1165                     + "<properties><android.sdk.path>...</android.sdk.path></properties> or on command-line "
1166                     + "using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME );
1167         }
1168         return androidHome;
1169     }
1170 
1171     /**
1172      *
1173      * @param apkLibraryArtifact
1174      * @return
1175      */
1176     protected String getLibraryUnpackDirectory( Artifact apkLibraryArtifact )
1177     {
1178         return AbstractAndroidMojo.getLibraryUnpackDirectory( unpackedApkLibsDirectory, apkLibraryArtifact );
1179     }
1180 
1181     /**
1182      *
1183      * @param unpackedApkLibsDirectory
1184      * @param apkLibraryArtifact
1185      * @return
1186      */
1187     public static String getLibraryUnpackDirectory( File unpackedApkLibsDirectory, Artifact apkLibraryArtifact )
1188     {
1189         return unpackedApkLibsDirectory.getAbsolutePath() + "/" + apkLibraryArtifact.getId().replace( ":", "_" );
1190     }
1191 
1192     /**
1193      * <p>Returns the Android NDK to use.</p>
1194      * <p/>
1195      * <p>Current implementation looks for <code>&lt;ndk&gt;&lt;path&gt;</code> configuration in pom, then System
1196      * property <code>android.ndk.path</code>, then environment variable <code>ANDROID_NDK_HOME</code>.
1197      * <p/>
1198      * <p>This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is
1199      * based on available parameters. This method should be the only one you should need to look at to understand how
1200      * the Android NDK is chosen, and from where on disk.</p>
1201      *
1202      * @return the Android NDK to use.
1203      * @throws org.apache.maven.plugin.MojoExecutionException
1204      *          if no Android NDK path configuration is available at all.
1205      */
1206     protected AndroidNdk getAndroidNdk() throws MojoExecutionException
1207     {
1208         File chosenNdkPath = null;
1209         // There is no <ndk> tag in the pom.
1210         if ( ndkPath != null )
1211         {
1212             // -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
1213             chosenNdkPath = ndkPath;
1214         }
1215         else if ( ndk != null && ndk.getPath() != null )
1216         {
1217             chosenNdkPath = ndk.getPath();
1218         }
1219         else
1220         {
1221             // No -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
1222             chosenNdkPath = new File( getAndroidNdkHomeOrThrow() );
1223         }
1224         return new AndroidNdk( chosenNdkPath );
1225     }
1226 
1227 
1228     /**
1229      *
1230      * @return
1231      * @throws MojoExecutionException
1232      */
1233     private String getAndroidNdkHomeOrThrow() throws MojoExecutionException
1234     {
1235         final String androidHome = System.getenv( ENV_ANDROID_NDK_HOME );
1236         if ( isBlank( androidHome ) )
1237         {
1238             throw new MojoExecutionException( "No Android NDK path could be found. You may configure it in the pom "
1239                     + "using <ndk><path>...</path></ndk> or <properties><ndk.path>...</ndk.path></properties> or on "
1240                     + "command-line using -Dandroid.ndk.path=... or by setting environment variable "
1241                     + ENV_ANDROID_NDK_HOME );
1242         }
1243         return androidHome;
1244     }
1245 
1246     /**
1247      * Get the resource directories if defined or the resource directory
1248      * @return
1249      */
1250     public File[] getResourceOverlayDirectories()
1251     {
1252         File[] overlayDirectories;
1253 
1254         if ( resourceOverlayDirectories == null || resourceOverlayDirectories.length == 0 )
1255         {
1256             overlayDirectories = new File[]{ resourceOverlayDirectory };
1257         }
1258         else
1259         {
1260             overlayDirectories = resourceOverlayDirectories;
1261         }
1262 
1263         return overlayDirectories;
1264     }
1265 
1266     private abstract class DoThread extends Thread
1267     {
1268         private MojoFailureException failure;
1269         private MojoExecutionException execution;
1270 
1271         public final void run()
1272         {
1273             try
1274             {
1275                 runDo();
1276             }
1277             catch ( MojoFailureException e )
1278             {
1279                 failure = e;
1280             }
1281             catch ( MojoExecutionException e )
1282             {
1283                 execution = e;
1284             }
1285         }
1286 
1287         protected abstract void runDo() throws MojoFailureException, MojoExecutionException;
1288     }
1289 }