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 * <ndk>
89 * <path>/opt/android-ndk-r4</path>
90 * </ndk>
91 * </pre>
92 * <p>The <code><path></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 * <ndk>
97 * <path>${env.ANDROID_NDK_HOME}</path>
98 * </ndk>
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 * <sdk>
330 * <path>/opt/android-sdk-linux</path>
331 * <platform>2.1</platform>
332 * </sdk>
333 * </pre>
334 * <p>The <code><platform></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><path></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 * <sdk>
345 * <path>${env.ANDROID_SDK}</path>
346 * </sdk>
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><sdk></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><sdk></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 <dependency> 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><ndk></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 <apkFile> 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><sdk><path></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><ndk><path></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 }