/* * Copyright (C) 2009 Jayway AB * Copyright (C) 2007-2008 JVending Masa * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.simpligility.maven.plugins.android.phase01generatesources; import com.simpligility.maven.plugins.android.AbstractAndroidMojo; import com.simpligility.maven.plugins.android.CommandExecutor; import com.simpligility.maven.plugins.android.ExecutionException; import com.simpligility.maven.plugins.android.common.AaptCommandBuilder; import com.simpligility.maven.plugins.android.common.DependencyResolver; import com.simpligility.maven.plugins.android.common.FileRetriever; import com.simpligility.maven.plugins.android.common.AaptCommandBuilder.AaptPackageCommandBuilder; import com.simpligility.maven.plugins.android.configuration.BuildConfigConstant; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.repository.RepositorySystem; import org.codehaus.plexus.archiver.ArchiverException; import org.codehaus.plexus.archiver.UnArchiver; import org.codehaus.plexus.archiver.zip.ZipUnArchiver; import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.logging.console.ConsoleLogger; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR; import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK; import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB; import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKSOURCES; /** * Generates <code>R.java</code> based on resources specified by the <code>resources</code> configuration parameter. * Generates java files based on aidl files. * * @author hugo.josefson@jayway.com * @author Manfred Moser - manfred@simpligility.com * @author William Ferguson - william.ferguson@xandar.com.au * @author Malachi de AElfweald malachid@gmail.com */ @Mojo( name = "generate-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE ) public class GenerateSourcesMojo extends AbstractAndroidMojo { /** * <p>Whether to produce a warning if there is an aar dependency that has an apklib artifact in its dependency * tree. The combination of aar library including or depending on an apklib has been deprecated and may not be * supported by future plugin versions. Traversing the dependency graph is done for all project dependencies * present in build classpath.</p> * * <p>It is recommended to keep this set to <code>true</code> to catch possible issues as soon as possible.</p> */ @Parameter( defaultValue = "true" ) protected boolean warnOnApklibDependencies; /** * Whether to fail the build if one of the dependencies and/or the project duplicate a layout file. * <p> * Such a scenario generally means that the build will fail with a compilation error due to missing resource files. * This is because any Ids contained in the duplicate layout files can never be generated by aapt, as appt * only picks up the first resource file it finds in its path. */ @Parameter( defaultValue = "true" ) private boolean failOnConflictingLayouts; /** * Whether to fail the build if one of the dependencies and/or the project have similar package in the * AndroidManifest. * <p> * Such scenario generally means that the build will fail with a compilation error due to * missing resources in the R file generated. */ @Parameter( defaultValue = "true" ) private boolean failOnDuplicatePackages; /** * Override default generated folder containing aidl classes */ @Parameter( property = "android.genDirectoryAidl", defaultValue = "${project.build.directory}/generated-sources/aidl" ) protected File genDirectoryAidl; /** * The directory containing the aidl files. */ @Parameter( property = "android.aidlSourceDirectory", defaultValue = "${project.basedir}/src/main/aidl" ) protected File aidlSourceDirectory; /** * <p>Parameter designed to generate custom BuildConfig constants */ @Parameter( property = "android.buildConfigConstants" ) protected BuildConfigConstant[] buildConfigConstants; /** */ @Component private RepositorySystem repositorySystem; /** * Pre AMP-4 AndroidManifest file. */ @Parameter( readonly = true, defaultValue = "${project.basedir}/AndroidManifest.xml" ) private File androidManifestFilePre4; /** * Pre AMP-4 android resources folder. */ @Parameter( readonly = true, defaultValue = "${project.basedir}/res" ) private File resourceDirectoryPre4; /** * Pre AMP-4 android assets directory. */ @Parameter( readonly = true, defaultValue = "${project.basedir}/assets" ) private File assetsDirectoryPre4; /** * Pre AMP-4 native libraries folder. */ @Parameter( readonly = true, defaultValue = "${project.basedir}/libs" ) private File nativeLibrariesDirectoryPre4; /** * If any non-standard files/folders exist and have NOT been explicitly configured then fail the build. */ @Parameter( defaultValue = "true" ) private boolean failOnNonStandardStructure; /** * Which dependency scopes should not be included when unpacking dependencies */ protected static final List<String> EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION = Arrays.asList( Artifact.SCOPE_SYSTEM, Artifact.SCOPE_IMPORT ); /** * Generates the sources. * * @throws MojoExecutionException if it fails. * @throws MojoFailureException if it fails. */ public void execute() throws MojoExecutionException, MojoFailureException { // If the current POM isn't an Android-related POM, then don't do // anything. This helps work with multi-module projects. if ( !isCurrentProjectAndroid() ) { return; } validateStandardLocations(); try { targetDirectory.mkdirs(); copyManifest(); // Check for a deprecated AAR > APKLIB artifact combination if ( warnOnApklibDependencies ) { checkForApklibDependencies(); } // TODO Do we really want to continue supporting APKSOURCES? How long has it been deprecated extractSourceDependencies(); // Extract the apklib and aar dependencies into unpacked-libs so that they can be referenced in the build. extractLibraryDependencies(); // Copy project assets to combinedAssets so that aapt has a single assets folder to load. copyFolder( assetsDirectory, combinedAssets ); final String[] relativeAidlFileNames1 = findRelativeAidlFileNames( aidlSourceDirectory ); final String[] relativeAidlFileNames2 = findRelativeAidlFileNames( extractedDependenciesJavaSources ); final Map<String, String[]> relativeApklibAidlFileNames = new HashMap<String, String[]>(); if ( !isInstrumentationTest() ) { // Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk. for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) ) { final File libSourceFolder = getUnpackedApkLibSourceFolder( artifact ); final String[] apklibAidlFiles = findRelativeAidlFileNames( libSourceFolder ); relativeApklibAidlFileNames.put( artifact.getId(), apklibAidlFiles ); } } checkPackagesForDuplicates(); checkForConflictingLayouts(); generateR(); generateBuildConfig(); // When compiling AIDL for this project, // make sure we compile AIDL for dependencies as well. // This is so project A, which depends on project B, can // use AIDL info from project B in its own AIDL final Map<File, String[]> files = new HashMap<File, String[]>(); files.put( aidlSourceDirectory, relativeAidlFileNames1 ); files.put( extractedDependenciesJavaSources, relativeAidlFileNames2 ); if ( !isInstrumentationTest() ) { // Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk. for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) ) { final File unpackedLibSourceFolder = getUnpackedApkLibSourceFolder( artifact ); files.put( unpackedLibSourceFolder, relativeApklibAidlFileNames.get( artifact.getId() ) ); } } generateAidlFiles( files ); } catch ( MojoExecutionException e ) { getLog().error( "Error when generating sources.", e ); throw e; } } /** * Performs some level of validation on the configured files and folders in light of the change * to AndroidStudio/Gradle style folder structures instead of the original Ant/Eclipse structures. * <p> * Essentially we will be noisy if we see an artifact that looks like before * Android Maven Plugin 4 and is not explicitly configured. */ private void validateStandardLocations() throws MojoExecutionException { boolean hasNonStandardStructure = false; if ( androidManifestFilePre4.exists() && !androidManifestFilePre4.equals( androidManifestFile ) ) { getLog().warn( "Non-standard location of AndroidManifest.xml file found, but not configured:\n " + androidManifestFilePre4 + "\nMove to the standard location src/main/AndroidManifest.xml\n" + "Or configure androidManifestFile. \n" ); hasNonStandardStructure = true; } if ( resourceDirectoryPre4.exists() && !resourceDirectoryPre4.equals( resourceDirectory ) ) { getLog().warn( "Non-standard location of Android res folder found, but not configured:\n " + resourceDirectoryPre4 + "\nMove to the standard location src/main/res/\n" + "Or configure resourceDirectory. \n" ); hasNonStandardStructure = true; } if ( assetsDirectoryPre4.exists() && !assetsDirectoryPre4.equals( assetsDirectory ) ) { getLog().warn( "Non-standard location assets folder found, but not configured:\n " + assetsDirectoryPre4 + "\nMove to the standard location src/main/assets/\n" + "Or configure assetsDirectory. \n" ); hasNonStandardStructure = true; } if ( nativeLibrariesDirectoryPre4.exists() && !nativeLibrariesDirectoryPre4.equals( nativeLibrariesDirectory ) ) { getLog().warn( "Non-standard location native libs folder found, but not configured:\n " + nativeLibrariesDirectoryPre4 + "\nMove to the standard location src/main/libs/\n" + "Or configure nativeLibrariesDirectory. \n" ); hasNonStandardStructure = true; } if ( hasNonStandardStructure && failOnNonStandardStructure ) { throw new MojoExecutionException( "\n\nFound files or folders in non-standard locations in the project!\n" + "....This might be a side-effect of a migration to Android Maven Plugin 4+.\n" + "....Please observe the warnings for specific files and folders above.\n" + "....Ideally you should restructure your project.\n" + "....Alternatively add explicit configuration overrides for files or folders.\n" + "....Finally you could set failOnNonStandardStructure to false, potentially " + "resulting in other failures.\n\n\n" ); } } /** * Copy the AndroidManifest.xml from androidManifestFile to destinationManifestFile * * @throws MojoExecutionException */ protected void copyManifest() throws MojoExecutionException { getLog().debug( "copyManifest: " + androidManifestFile + " -> " + destinationManifestFile ); if ( androidManifestFile == null ) { getLog().debug( "Manifest copying disabled. Using default manifest only" ); return; } try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse( androidManifestFile ); Source source = new DOMSource( doc ); TransformerFactory xfactory = TransformerFactory.newInstance(); Transformer xformer = xfactory.newTransformer(); xformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" ); FileWriter writer = null; try { destinationManifestFile.getParentFile().mkdirs(); writer = new FileWriter( destinationManifestFile, false ); if ( doc.getXmlEncoding() != null && doc.getXmlVersion() != null ) { String xmldecl = String.format( "<?xml version=\"%s\" encoding=\"%s\"?>%n", doc.getXmlVersion(), doc.getXmlEncoding() ); writer.write( xmldecl ); } Result result = new StreamResult( writer ); xformer.transform( source, result ); getLog().info( "Manifest copied from " + androidManifestFile + " to " + destinationManifestFile ); } finally { IOUtils.closeQuietly( writer ); } } catch ( Exception e ) { getLog().error( "Error during copyManifest" ); throw new MojoExecutionException( "Error during copyManifest", e ); } } /** * Extract the source dependencies. * * @throws MojoExecutionException if it fails. */ protected void extractSourceDependencies() throws MojoExecutionException { for ( Artifact artifact : getDirectDependencyArtifacts() ) { String type = artifact.getType(); if ( type.equals( APKSOURCES ) ) { getLog().debug( "Detected apksources dependency " + artifact + " with file " + artifact.getFile() + ". Will resolve and extract..." ); final File apksourcesFile = resolveArtifactToFile( artifact ); getLog().debug( "Extracting " + apksourcesFile + "..." ); extractApksources( apksourcesFile ); } } if ( extractedDependenciesJavaResources.exists() ) { projectHelper.addResource( project, extractedDependenciesJavaResources.getAbsolutePath(), null, null ); project.addCompileSourceRoot( extractedDependenciesJavaSources.getAbsolutePath() ); } } /** * @deprecated Support <code>APKSOURCES</code> artifacts has been deprecated. Use APKLIB instead. */ @Deprecated private void extractApksources( File apksourcesFile ) throws MojoExecutionException { if ( apksourcesFile.isDirectory() ) { getLog().warn( "The apksources artifact points to '" + apksourcesFile + "' which is a directory; skipping unpacking it." ); return; } final UnArchiver unArchiver = new ZipUnArchiver( apksourcesFile ) { @Override protected Logger getLogger() { return new ConsoleLogger( Logger.LEVEL_DEBUG, "dependencies-unarchiver" ); } }; extractedDependenciesDirectory.mkdirs(); unArchiver.setDestDirectory( extractedDependenciesDirectory ); try { unArchiver.extract(); } catch ( ArchiverException e ) { throw new MojoExecutionException( "ArchiverException while extracting " + apksourcesFile.getAbsolutePath() + ". Message: " + e.getLocalizedMessage(), e ); } } private void extractLibraryDependencies() throws MojoExecutionException { final Collection<Artifact> artifacts = getTransitiveDependencyArtifacts( EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION ); getLog().info( "Extracting libs" ); // The only library dependencies we want included in an instrumentation test are APK and AARs. // Any APKLIB classes have already been compiled into the APK. final boolean instrumentationTest = isInstrumentationTest(); for ( Artifact artifact : artifacts ) { final String type = artifact.getType(); if ( type.equals( APKLIB ) && !instrumentationTest ) { getLog().info( "Extracting apklib " + artifact.getArtifactId() + "..." ); extractApklib( artifact ); } else if ( type.equals( AAR ) ) { getLog().info( "Extracting aar " + artifact.getArtifactId() + "..." ); extractAarLib( artifact ); } else if ( type.equals( APK ) ) { getLog().info( "Extracting apk " + artifact.getArtifactId() + "..." ); extractApkClassesJar( artifact ); } else { getLog().debug( "Not extracting " + artifact.getArtifactId() + "..." ); } } } /** * Extracts ApkLib and adds the assets and apklib sources and resources to the build. */ private void extractApklib( Artifact apklibArtifact ) throws MojoExecutionException { getUnpackedLibHelper().extractApklib( apklibArtifact ); // Copy the assets to the the combinedAssets folder. // Add the apklib source and resource to the compile. // NB apklib sources are added to compileSourceRoot because we may need to compile against them. // This means the apklib classes will be compiled into target/classes and packaged with this build. copyFolder( getUnpackedLibAssetsFolder( apklibArtifact ), combinedAssets ); final File apklibSourceFolder = getUnpackedApkLibSourceFolder( apklibArtifact ); final List<String> resourceExclusions = Arrays.asList( "**/*.java", "**/*.aidl" ); projectHelper.addResource( project, apklibSourceFolder.getAbsolutePath(), null, resourceExclusions ); project.addCompileSourceRoot( apklibSourceFolder.getAbsolutePath() ); } /** * Extracts AarLib and if this is an APK build then adds the assets and resources to the build. */ private void extractAarLib( Artifact aarArtifact ) throws MojoExecutionException { getUnpackedLibHelper().extractAarLib( aarArtifact ); // Copy the assets to the the combinedAssets folder, but only if an APK build. // Ie we only want to package assets that we own. // Assets should only live within their owners or the final APK. if ( isAPKBuild() ) { copyFolder( getUnpackedLibAssetsFolder( aarArtifact ), combinedAssets ); } // Aar lib resources should only be included if we are building an apk. // So we need to extract them into a folder that we then add to the resource classpath. if ( isAPKBuild() ) { // (If we are including SYSTEM_SCOPE artifacts when considering resources for APK packaging) // then adding the AAR resources to the project would have them added twice. getLog().debug( "Not adding AAR resources to resource classpath : " + aarArtifact ); } } /** * Copies a dependent APK jar over the top of the placeholder created for it in AarMavenLifeCycleParticipant. * <p> * This is necessary because we need the classes of the APK added to the compile classpath. * NB APK dependencies are uncommon as they should really only be used in a project that tests an apk. * * @param artifact APK dependency for this project whose classes will be copied over. */ private void extractApkClassesJar( Artifact artifact ) throws MojoExecutionException { final File apkClassesJar = getUnpackedLibHelper().getJarFileForApk( artifact ); final File unpackedClassesJar = getUnpackedLibHelper().getUnpackedClassesJar( artifact ); try { FileUtils.copyFile( apkClassesJar, unpackedClassesJar ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not copy APK classes jar " + apkClassesJar + " to " + unpackedClassesJar, e ); } getLog().debug( "Copied APK classes jar into compile classpath : " + unpackedClassesJar ); } /** * Traverses the list of project dependencies looking for "AAR depends on APKLIB" artifact combination * that has been deprecated. For each occurrence of an AAR artifact with APKLIB direct or transitive dependency, * produces a warning message to inform the user. Future plugin versions may default to skipping or not handling * unsupported artifacts during build lifecycle. * * @throws MojoExecutionException */ private void checkForApklibDependencies() throws MojoExecutionException { final boolean isAarBuild = project.getPackaging().equals( AAR ); final DependencyResolver dependencyResolver = getDependencyResolver(); final Set<Artifact> allArtifacts = project.getArtifacts(); Set<Artifact> dependencyArtifacts = getArtifactResolverHelper().getFilteredArtifacts( allArtifacts ); boolean foundApklib = false; for ( Artifact artifact : dependencyArtifacts ) { final String type = artifact.getType(); if ( type.equals( APKLIB ) && isAarBuild ) { getLog().warn( "Detected APKLIB transitive dependency: " + artifact.getId() ); foundApklib = true; } else if ( type.equals( AAR ) ) { final Set<Artifact> dependencies = dependencyResolver .getLibraryDependenciesFor( session, repositorySystem, artifact ); for ( Artifact dependency : dependencies ) { if ( dependency.getType().equals( APKLIB ) ) { getLog().warn( "Detected " + artifact.getId() + " that depends on APKLIB: " + dependency.getId() ); foundApklib = true; } } } } if ( foundApklib ) { getLog().warn( "AAR libraries should not depend or include APKLIB artifacts.\n" + "APKLIBs have been deprecated and the combination of the two may yield unexpected results.\n" + "Check the problematic AAR libraries for newer versions that use AAR packaging." ); } } /** * Checks packages in AndroidManifest.xml file of project and all dependent libraries for duplicates. * <p>Generate warning if duplicates presents. * <p>(in case of packages similarity R.java and BuildConfig files will be overridden) * * @throws MojoExecutionException */ private void checkPackagesForDuplicates() throws MojoExecutionException { Set<Artifact> dependencyArtifacts = getTransitiveDependencyArtifacts( AAR, APKLIB ); if ( dependencyArtifacts.isEmpty() ) { //if no AAR or APKLIB dependencies presents than only one package presents return; } Map<String, Set<Artifact>> packageCompareMap = getPackageCompareMap( dependencyArtifacts ); List<String> duplicatesMessageList = new ArrayList<String>(); for ( Map.Entry<String, Set<Artifact>> entry : packageCompareMap.entrySet() ) { Set<Artifact> artifacts = entry.getValue(); if ( artifacts != null && artifacts.size() > 1 ) { StringBuilder messageBuilder = new StringBuilder(); for ( Artifact item : artifacts ) { messageBuilder .append( messageBuilder.length() > 0 ? ", " : " [" ) .append( item.getArtifactId() ); } messageBuilder .append( "] have similar package='" ) .append( entry.getKey() ) .append( "'" ); duplicatesMessageList.add( messageBuilder.toString() ); } } if ( !duplicatesMessageList.isEmpty() ) { List<String> messageList = new ArrayList<String>(); messageList.add( "" ); messageList.add( "Duplicate packages detected in AndroidManifest.xml files" ); messageList.add( "" ); messageList.add( "Such scenario generally means that the build will fail with a compilation error due to" + " missing resources in R file." ); messageList.add( "You should consider renaming some of the duplicate packages listed below" + " to avoid the conflict." ); messageList.add( "" ); messageList.add( "Conflicting artifacts:" ); messageList.addAll( duplicatesMessageList ); messageList.add( "" ); if ( failOnDuplicatePackages ) { StringBuilder builder = new StringBuilder(); for ( String line : messageList ) { builder.append( line ); builder.append( "\n" ); } builder.append( "\n" ); builder.append( "You can downgrade the failure to a warning " ); builder.append( "by setting the 'failOnDuplicatePackages' plugin property to false." ); throw new MojoExecutionException( builder.toString() ); } for ( String messageLine : messageList ) { getLog().warn( messageLine ); } } } /** * Looks for dependencies that have a layout file with the same name as either one of the project layout files * or one of the other dependencies. If such a duplicate occurs then it will fail the build or generate a warning * depending upon the value of the <strong>failOnConflictingLayouts</strong> build parameter. * * @throws MojoExecutionException */ private void checkForConflictingLayouts() throws MojoExecutionException { final ConflictingLayoutDetector detector = new ConflictingLayoutDetector(); // Add layout files for this project final FileRetriever retriever = new FileRetriever( "layout*/*.xml" ); detector.addLayoutFiles( getAndroidManifestPackageName(), retriever.getFileNames( resourceDirectory ) ); // Add layout files for all dependencies. for ( final Artifact dependency : getTransitiveDependencyArtifacts( AAR, APKLIB ) ) { final String packageName = extractPackageNameFromAndroidArtifact( dependency ); final String[] layoutFiles = retriever.getFileNames( getUnpackedLibResourceFolder( dependency ) ); detector.addLayoutFiles( packageName, layoutFiles ); } final Collection<ConflictingLayout> conflictingLayouts = detector.getConflictingLayouts(); getLog().debug( "checkConflictingLayouts - conflicts : " + conflictingLayouts ); if ( !conflictingLayouts.isEmpty() ) { final List<String> sb = new ArrayList<String>(); sb.add( "" ); sb.add( "" ); sb.add( "Duplicate layout files have been detected across more than one Android package." ); sb.add( "" ); sb.add( "Such a scenario generally means that the build will fail with a compilation error due to" ); sb.add( "missing resource files. You should consider renaming some of the layout files listed below" ); sb.add( "to avoid the conflict." ); sb.add( "" ); sb.add( "However, if you believe you know better, then you can downgrade the failure to a warning" ); sb.add( "by setting the failOnConflictingLayouts plugin property to false." ); sb.add( "But you really don't want to do that." ); sb.add( "" ); sb.add( "Conflicting Layouts:" ); for ( final ConflictingLayout layout : conflictingLayouts ) { sb.add( " " + layout.getLayoutFileName() + " packages=" + layout.getPackageNames().toString() ); } sb.add( "" ); if ( failOnConflictingLayouts ) { final StringBuilder builder = new StringBuilder(); for ( final String line : sb ) { builder.append( line ); builder.append( "\n" ); } throw new MojoExecutionException( builder.toString() ); } for ( final String line : sb ) { getLog().warn( line ); } } } /** * Provides map with all provided dependencies or project itself grouped by package name * * @param dependencyArtifacts artifacts that should be grouped by package name * @return map of with package names(String) and sets of artifacts (Set<Artifact>) * that have similar package names * @throws MojoExecutionException */ Map<String, Set<Artifact>> getPackageCompareMap( Set<Artifact> dependencyArtifacts ) throws MojoExecutionException { if ( dependencyArtifacts == null ) { throw new IllegalArgumentException( "dependencies must be initialized" ); } Map<String, Set<Artifact>> packageCompareMap = new HashMap<String, Set<Artifact>>(); Set<Artifact> artifactSet = new HashSet<Artifact>(); artifactSet.add( project.getArtifact() ); packageCompareMap.put( getAndroidManifestPackageName(), artifactSet ); for ( Artifact artifact : dependencyArtifacts ) { String libPackage = extractPackageNameFromAndroidArtifact( artifact ); Set<Artifact> artifacts = packageCompareMap.get( libPackage ); if ( artifacts == null ) { artifacts = new HashSet<Artifact>(); packageCompareMap.put( libPackage, artifacts ); } artifacts.add( artifact ); } return packageCompareMap; } private void generateR() throws MojoExecutionException { getLog().info( "Generating R file for " + project.getArtifact() ); genDirectory.mkdirs(); final AaptPackageCommandBuilder commandBuilder = AaptCommandBuilder .packageResources( getLog() ) .makePackageDirectories() .setResourceConstantsFolder( genDirectory ) .forceOverwriteExistingFiles() .disablePngCrunching() .generateRIntoPackage( customPackage ) .setPathToAndroidManifest( destinationManifestFile ) .addResourceDirectoriesIfExists( getResourceOverlayDirectories() ) .addResourceDirectoryIfExists( resourceDirectory ) // Need to include any AAR or APKLIB dependencies when generating R because if any local // resources directly reference dependent resources then R generation will crash. .addResourceDirectoriesIfExists( getLibraryResourceFolders() ) .autoAddOverlay() .addRawAssetsDirectoryIfExists( combinedAssets ) .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() ) .addConfigurations( configurations ) .setVerbose( aaptVerbose ) // We need to generate R.txt for all projects as it needs to be consumed when generating R class. // It also needs to be consumed when packaging aar. .generateRTextFile( targetDirectory ) // If a proguard file is defined then output Proguard options to it. .setProguardOptionsOutputFile( proguardFile ) .makeResourcesNonConstant( AAR.equals( project.getArtifact().getType() ) ) .addExtraArguments( aaptExtraArgs ); getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() ); try { final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger( getLog() ); executor.setCaptureStdOut( true ); final List<String> commands = commandBuilder.build(); executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false ); } catch ( ExecutionException e ) { throw new MojoExecutionException( "", e ); } final ResourceClassGenerator resGenerator = new ResourceClassGenerator( this, targetDirectory, genDirectory ); generateCorrectRJavaForApklibDependencies( resGenerator ); generateCorrectRJavaForAarDependencies( resGenerator ); getLog().info( "Adding R gen folder to compile classpath: " + genDirectory ); project.addCompileSourceRoot( genDirectory.getAbsolutePath() ); } /** * Generate correct R.java for apklibs dependencies of a current project * * @throws MojoExecutionException */ private void generateCorrectRJavaForApklibDependencies( ResourceClassGenerator resourceGenerator ) throws MojoExecutionException { getLog().debug( "" ); getLog().debug( "#generateCorrectRJavaFoApklibDeps" ); // Generate R.java for apklibs // Compatibility with Apklib which isn't present in AndroidBuilder getLog().debug( "Generating Rs for apklib deps of project " + project.getArtifact() ); final Set<Artifact> apklibDependencies = getTransitiveDependencyArtifacts( APKLIB ); for ( final Artifact artifact : apklibDependencies ) { getLog().debug( "Generating apklib R.java for " + artifact.getArtifactId() + "..." ); generateRForApkLibDependency( artifact ); } // Generate corrected R.java for APKLIB dependencies, but only if this is an APK build. if ( !apklibDependencies.isEmpty() && APK.equals( project.getArtifact().getType() ) ) { // Generate R.java for each APKLIB based on R.txt getLog().debug( "" ); getLog().debug( "Rewriting R files for APKLIB dependencies : " + apklibDependencies ); resourceGenerator.generateLibraryRs( apklibDependencies ); } } /** * Generate correct R.java for aar dependencies of a current project * * @throws MojoExecutionException */ private void generateCorrectRJavaForAarDependencies( ResourceClassGenerator resourceGenerator ) throws MojoExecutionException { // Generate corrected R.java for AAR dependencies. final Set<Artifact> aarLibraries = getTransitiveDependencyArtifacts( AAR ); if ( !aarLibraries.isEmpty() ) { // Generate R.java for each AAR based on R.txt getLog().debug( "Generating R file for AAR dependencies" ); resourceGenerator.generateLibraryRs( aarLibraries ); } } private List<File> getLibraryResourceFolders() { final List<File> resourceFolders = new ArrayList<File>(); for ( Artifact artifact : getTransitiveDependencyArtifacts( AAR, APKLIB ) ) { getLog().debug( "Considering dep artifact : " + artifact ); final File resourceFolder = getUnpackedLibResourceFolder( artifact ); if ( resourceFolder.exists() ) { getLog().debug( "Adding apklib or aar resource folder : " + resourceFolder ); resourceFolders.add( resourceFolder ); } } return resourceFolders; } /** * Executes aapt to generate the R class for the given apklib. * * @param apklibArtifact apklib for which to generate the R class. * @throws MojoExecutionException if it fails. */ private void generateRForApkLibDependency( Artifact apklibArtifact ) throws MojoExecutionException { final File unpackDir = getUnpackedLibFolder( apklibArtifact ); getLog().debug( "Generating incomplete R file for apklib: " + apklibArtifact.getGroupId() + ":" + apklibArtifact.getArtifactId() ); final File apklibManifest = new File( unpackDir, "AndroidManifest.xml" ); final File apklibResDir = new File( unpackDir, "res" ); List<File> dependenciesResDirectories = new ArrayList<File>(); final Set<Artifact> apklibDeps = getDependencyResolver() .getLibraryDependenciesFor( this.session, this.repositorySystem, apklibArtifact ); getLog().debug( "apklib=" + apklibArtifact + " dependencies=" + apklibDeps ); for ( Artifact dependency : apklibDeps ) { // Add in the resources that are dependencies of the apklib. final String extension = dependency.getType(); final File dependencyResDir = getUnpackedLibResourceFolder( dependency ); if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) && dependencyResDir.exists() ) { dependenciesResDirectories.add( dependencyResDir ); } } // Create combinedAssets for this apklib dependency - can't have multiple -A args final File apklibCombAssets = new File( getUnpackedLibFolder( apklibArtifact ), "combined-assets" ); for ( Artifact dependency : apklibDeps ) { // Accumulate assets for dependencies of the apklib (if they exist). final String extension = dependency.getType(); final File dependencyAssetsDir = getUnpackedLibAssetsFolder( dependency ); if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) ) { copyFolder( dependencyAssetsDir, apklibCombAssets ); } } // Overlay the apklib dependency assets (if they exist) final File apkLibAssetsDir = getUnpackedLibAssetsFolder( apklibArtifact ); copyFolder( apkLibAssetsDir, apklibCombAssets ); final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger( getLog() ); final AaptCommandBuilder commandBuilder = AaptCommandBuilder .packageResources( getLog() ) .makeResourcesNonConstant() .makePackageDirectories() .setResourceConstantsFolder( genDirectory ) .generateRIntoPackage( extractPackageNameFromAndroidManifest( apklibManifest ) ) .setPathToAndroidManifest( apklibManifest ) .addResourceDirectoryIfExists( apklibResDir ) .addResourceDirectoriesIfExists( dependenciesResDirectories ) .autoAddOverlay() .addRawAssetsDirectoryIfExists( apklibCombAssets ) .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() ) .addConfigurations( configurations ) .setVerbose( aaptVerbose ) .addExtraArguments( aaptExtraArgs ) // We need to generate R.txt for all projects as it needs to be consumed when generating R class. // It also needs to be consumed when packaging aar. .generateRTextFile( unpackDir ); getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() ); try { executor.setCaptureStdOut( true ); final List<String> commands = commandBuilder.build(); executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false ); } catch ( ExecutionException e ) { throw new MojoExecutionException( "", e ); } } private void generateBuildConfig() throws MojoExecutionException { getLog().debug( "Generating BuildConfig file" ); // Create the BuildConfig for our package. String packageName = extractPackageNameFromAndroidManifest( destinationManifestFile ); if ( StringUtils.isNotBlank( customPackage ) ) { packageName = customPackage; } generateBuildConfigForPackage( packageName ); // Skip BuildConfig generation for dependencies if this is an AAR project if ( project.getPackaging().equals( AAR ) ) { return; } // Generate the BuildConfig for any APKLIB and some AAR dependencies. // Need to generate for AAR, because some old AARs like ActionBarSherlock do not have BuildConfig (or R) for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) ) { if ( skipBuildConfigGeneration( artifact ) ) { getLog().info( "Skip BuildConfig.java generation for " + artifact.getGroupId() + " " + artifact.getArtifactId() ); continue; } final String depPackageName = extractPackageNameFromAndroidArtifact( artifact ); generateBuildConfigForPackage( depPackageName ); } } private boolean skipBuildConfigGeneration( Artifact artifact ) throws MojoExecutionException { if ( artifact.getType().equals( AAR ) ) { String depPackageName = extractPackageNameFromAndroidArtifact( artifact ); if ( isBuildConfigPresent( artifact, depPackageName ) ) { return true; } Set<Artifact> transitiveDep = getArtifactResolverHelper() .getFilteredArtifacts( project.getArtifacts(), AAR ); for ( Artifact transitiveArtifact : transitiveDep ) { if ( isBuildConfigPresent( transitiveArtifact, depPackageName ) ) { return true; } } } return false; } /** * Check if given artifact includes a matching BuildConfig class * * @throws MojoExecutionException */ private boolean isBuildConfigPresent( Artifact artifact ) throws MojoExecutionException { String depPackageName = extractPackageNameFromAndroidArtifact( artifact ); return isBuildConfigPresent( artifact, depPackageName ); } /** * Check whether the artifact includes a BuildConfig located in a given package. * * @param artifact an AAR artifact to look for BuildConfig in * @param packageName BuildConfig package name * @throws MojoExecutionException */ private boolean isBuildConfigPresent( Artifact artifact, String packageName ) throws MojoExecutionException { try { JarFile jar = new JarFile( getUnpackedAarClassesJar( artifact ) ); JarEntry entry = jar.getJarEntry( packageName.replace( '.', '/' ) + "/BuildConfig.class" ); return ( entry != null ); } catch ( IOException e ) { getLog().error( "Error generating BuildConfig ", e ); throw new MojoExecutionException( "Error generating BuildConfig", e ); } } private void generateBuildConfigForPackage( String packageName ) throws MojoExecutionException { getLog().debug( "Creating BuildConfig for " + packageName ); File outputFolder = new File( genDirectory, packageName.replace( ".", File.separator ) ); outputFolder.mkdirs(); StringBuilder buildConfig = new StringBuilder(); buildConfig.append( "package " ).append( packageName ).append( ";\n\n" ); buildConfig.append( "public final class BuildConfig {\n" ); buildConfig.append( " public static final boolean DEBUG = " ).append( !release ).append( ";\n" ); for ( BuildConfigConstant constant : buildConfigConstants ) { String value = constant.getValue(); if ( "String".equals( constant.getType() ) ) { value = "\"" + value + "\""; } buildConfig.append( " public static final " ) .append( constant.getType() ) .append( " " ) .append( constant.getName() ) .append( " = " ) .append( value ) .append( ";\n" ); } buildConfig.append( "}\n" ); File outputFile = new File( outputFolder, "BuildConfig.java" ); try { FileUtils.writeStringToFile( outputFile, buildConfig.toString() ); } catch ( IOException e ) { getLog().error( "Error generating BuildConfig ", e ); throw new MojoExecutionException( "Error generating BuildConfig", e ); } } /** * Given a map of source directories to list of AIDL (relative) filenames within each, * runs the AIDL compiler for each, such that all source directories are available to * the AIDL compiler. * * @param files Map of source directory File instances to the relative paths to all AIDL files within * @throws MojoExecutionException If the AIDL compiler fails */ private void generateAidlFiles( Map<File /*sourceDirectory*/, String[] /*relativeAidlFileNames*/> files ) throws MojoExecutionException { List<String> protoCommands = new ArrayList<String>(); protoCommands.add( "-p" + getAndroidSdk().getPathForFrameworkAidl() ); genDirectoryAidl.mkdirs(); getLog().info( "Adding AIDL gen folder to compile classpath: " + genDirectoryAidl ); project.addCompileSourceRoot( genDirectoryAidl.getPath() ); Set<File> sourceDirs = files.keySet(); for ( File sourceDir : sourceDirs ) { protoCommands.add( "-I" + sourceDir ); } for ( File sourceDir : sourceDirs ) { for ( String relativeAidlFileName : files.get( sourceDir ) ) { File targetDirectory = new File( genDirectoryAidl, new File( relativeAidlFileName ).getParent() ); targetDirectory.mkdirs(); final String shortAidlFileName = new File( relativeAidlFileName ).getName(); final String shortJavaFileName = shortAidlFileName.substring( 0, shortAidlFileName.lastIndexOf( "." ) ) + ".java"; final File aidlFileInSourceDirectory = new File( sourceDir, relativeAidlFileName ); List<String> commands = new ArrayList<String>( protoCommands ); commands.add( aidlFileInSourceDirectory.getAbsolutePath() ); commands.add( new File( targetDirectory, shortJavaFileName ).getAbsolutePath() ); try { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger( this.getLog() ); executor.setCaptureStdOut( true ); executor.executeCommand( getAndroidSdk().getAidlPath(), commands, project.getBasedir(), false ); } catch ( ExecutionException e ) { throw new MojoExecutionException( "", e ); } } } } private String[] findRelativeAidlFileNames( File sourceDirectory ) { final FileRetriever retriever = new FileRetriever( "**/*.aidl" ); final String[] relativeAidlFileNames = retriever.getFileNames( sourceDirectory ); if ( relativeAidlFileNames.length == 0 ) { getLog().debug( "ANDROID-904-002: No aidl files found" ); } else { getLog().info( "ANDROID-904-002: Found aidl files: Count = " + relativeAidlFileNames.length ); } return relativeAidlFileNames; } /** * @return true if the pom type is APK, APKLIB, or APKSOURCES */ private boolean isCurrentProjectAndroid() { Set<String> androidArtifacts = new HashSet<String>() { { addAll( Arrays.asList( APK, APKLIB, APKSOURCES, AAR ) ); } }; return androidArtifacts.contains( project.getArtifact().getType() ); } }