/*
 * 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.phase09package;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.archiver.zip.ZipArchiver;

import com.android.SdkConstants;
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.AndroidExtension;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.config.PullParameter;

/**
 * Creates an Android Archive (aar) file.

 */
@Mojo(
        name = "aar",
        defaultPhase = LifecyclePhase.VERIFY,
        requiresDependencyResolution = ResolutionScope.COMPILE
)
public class AarMojo extends AbstractAndroidMojo
{
    /**
     * The name of the top level folder in the AAR where native libraries are found.
     * NOTE: This is inconsistent with APK where the folder is called "lib", and does not match APKLIB
     * layout either, where the folder is called "libs".
     */
    public static final String NATIVE_LIBRARIES_FOLDER = "jni";

    /**
     *
Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.

     */
    @Parameter
    private String classifier;

    /**
     * Specifies the application makefile to use for the build (if other than the default Application.mk).
     */
    @Parameter
    @PullParameter
    private String applicationMakefile;

    /**
     * Defines the architecture for the NDK build
     */
    @Parameter( property = "android.ndk.build.architecture" )
    @PullParameter
    private String ndkArchitecture;

    /**
     * Specifies the classifier with which the artifact should be stored in the repository
     */
    @Parameter( property = "android.ndk.build.native-classifier" )
    @PullParameter
    private String ndkClassifier;

    /**
     * Specifies the files that should be included in the classes.jar within the aar
     */
    @Parameter
    @PullParameter
    private String[] classesJarIncludes = new String[]{"**/*"};

    /**
     * Specifies the files that should be excluded from the classes.jar within the aar
     */
    @Parameter
    @PullParameter
    private String[] classesJarExcludes = new String[]{"**/R.class", "**/R$*.class"};

    /**
     * Specifies the proguard rule files to be included in the final package. All specified files will be merged into
     * one proguard.txt file.
     */
    @Parameter
    private File[] consumerProguardFiles;

    @Parameter(
            property = "android.proguard.obfuscatedJar",
            defaultValue = "${project.build.directory}/${project.build.finalName}_obfuscated.jar"
    )
    private String obfuscatedJar;

    private List<String> sourceFolders = new ArrayList<String>();

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        String out = targetDirectory.getPath();
        for ( String src : project.getCompileSourceRoots() )
        {
            if ( !src.startsWith( out ) )
            {
                sourceFolders.add( src );
            }
        }

        getLog().info( "Generating AAR file : " + project.getArtifactId() );
        generateIntermediateApk();

        final File outputFile = createAarLibraryFile( createAarClassesJar() );

        if ( classifier == null )
        {
            // Set the generated file as the main artifact (because the pom states <packaging>aar</packaging>)
            project.getArtifact().setFile( outputFile );
        }
        else
        {
            // If there is a classifier specified, attach the artifact using that
            projectHelper.attachArtifact( project, AndroidExtension.AAR, classifier, outputFile );
        }
    }

    /**
     * Creates an appropriate aar/classes.jar that does not include R
     *
     * @return File which is the AAR classes jar.
     * @throws MojoExecutionException
     */
    protected File createAarClassesJar() throws MojoExecutionException
    {
        final File obfuscatedJarFile = new File( obfuscatedJar );
        if ( obfuscatedJarFile.exists() )
        {
            attachJar( obfuscatedJarFile );
            return obfuscatedJarFile;
        }

        final File classesJar = new File( targetDirectory, finalName + ".aar.classes.jar" );
        try
        {
            JarArchiver jarArchiver = new JarArchiver();
            jarArchiver.setDestFile( classesJar );
            jarArchiver.addDirectory( projectOutputDirectory,
                    classesJarIncludes,
                    classesJarExcludes );
            jarArchiver.createArchive();
            attachJar( classesJar );
            return classesJar;
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while creating ." + classesJar + " file.", e );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException while creating ." + classesJar + " file.", e );
        }

    }

    private void attachJar( File jarFile )
    {
        if ( attachJar )
        {
            projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
        }
    }

    /**
     * @return AAR file.
     * @throws MojoExecutionException
     */
    protected File createAarLibraryFile( File classesJar ) throws MojoExecutionException
    {
        final File aarLibrary = new File( targetDirectory,
                finalName + "." + AAR );
        FileUtils.deleteQuietly( aarLibrary );

        try
        {
            final ZipArchiver zipArchiver = new ZipArchiver();
            zipArchiver.setDestFile( aarLibrary );

            zipArchiver.addFile( destinationManifestFile, "AndroidManifest.xml" );
            addDirectory( zipArchiver, assetsDirectory, "assets", false );

            // res folder must be included in the archive even if empty or non-existent.
            if ( !resourceDirectory.exists() )
            {
                resourceDirectory.mkdir();
            }
            addDirectory( zipArchiver, resourceDirectory, "res", true );

            zipArchiver.addFile( classesJar, SdkConstants.FN_CLASSES_JAR );

            final File[] overlayDirectories = getResourceOverlayDirectories();
            for ( final File resOverlayDir : overlayDirectories )
            {
                if ( resOverlayDir != null && resOverlayDir.exists() )
                {
                    addDirectory( zipArchiver, resOverlayDir, "res", false );
                }
            }

            if ( consumerProguardFiles != null )
            {
                final File mergedConsumerProguardFile = new File( targetDirectory, "consumer-proguard.txt" );
                if ( mergedConsumerProguardFile.exists() )
                {
                    FileUtils.forceDelete( mergedConsumerProguardFile );
                }
                mergedConsumerProguardFile.createNewFile();
                StringBuilder mergedConsumerProguardFileBuilder = new StringBuilder();
                for ( File consumerProguardFile : consumerProguardFiles )
                {
                    if ( consumerProguardFile.exists() )
                    {
                        getLog().info( "Adding consumer proguard file " + consumerProguardFile );
                        FileInputStream consumerProguardFileInputStream = null;
                        try
                        {
                            consumerProguardFileInputStream = new FileInputStream( consumerProguardFile );
                            mergedConsumerProguardFileBuilder.append(
                                IOUtils.toString( consumerProguardFileInputStream ) );
                            mergedConsumerProguardFileBuilder.append( SystemUtils.LINE_SEPARATOR );
                        }
                        catch ( IOException e )
                        {
                            throw new MojoExecutionException( "Error writing consumer proguard file ", e );
                        }
                        finally
                        {
                            IOUtils.closeQuietly( consumerProguardFileInputStream );
                        }
                    }
                }
                FileOutputStream mergedConsumerProguardFileOutputStream = null;
                try
                {
                    mergedConsumerProguardFileOutputStream = new FileOutputStream( mergedConsumerProguardFile );
                    IOUtils.write( mergedConsumerProguardFileBuilder, mergedConsumerProguardFileOutputStream );
                }
                catch ( IOException e )
                {
                    throw new MojoExecutionException( "Error writing consumer proguard file ", e );
                }
                finally
                {
                    IOUtils.closeQuietly( mergedConsumerProguardFileOutputStream );
                }

                zipArchiver.addFile( mergedConsumerProguardFile, "proguard.txt" );
            }

            addR( zipArchiver );

            // Lastly, add any native libraries
            addNativeLibraries( zipArchiver );

            zipArchiver.createArchive();
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while creating ." + AAR + " file.", e );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException while creating ." + AAR + " file.", e );
        }

        return aarLibrary;
    }

    private void addR( ZipArchiver zipArchiver ) throws MojoExecutionException, IOException
    {
        final File rFile = new File( targetDirectory, "R.txt" );
        if ( !rFile.exists() )
        {
            getLog().debug( "No resources - creating empty R.txt" );
            if ( !rFile.createNewFile() )
            {
                getLog().warn( "Unable to create R.txt in AAR" );
            }
        }
        zipArchiver.addFile( rFile, "R.txt" );
        getLog().debug( "Packaging R.txt in AAR" );
    }

    private void addNativeLibraries( final ZipArchiver zipArchiver ) throws MojoExecutionException
    {
        try
        {
            if ( nativeLibrariesDirectory.exists() )
            {
                getLog().info( nativeLibrariesDirectory + " exists, adding libraries." );
                addDirectory( zipArchiver, nativeLibrariesDirectory, NATIVE_LIBRARIES_FOLDER, false );
            }
            else
            {
                getLog().info( nativeLibrariesDirectory
                        + " does not exist, looking for libraries in target directory." );
                // Add native libraries built and attached in this build
                String[] ndkArchitectures = NativeHelper.getNdkArchitectures( ndkArchitecture,
                                                                              applicationMakefile,
                                                                              project.getBasedir() );
                for ( String architecture : ndkArchitectures )
                {
                    final File ndkLibsDirectory = new File( ndkOutputDirectory, architecture );
                    addSharedLibraries( zipArchiver, ndkLibsDirectory, architecture );

                    // Add native library dependencies
                    // FIXME: Remove as causes duplicate libraries when building final APK if this set includes
                    //        libraries from dependencies of the AAR
                    //final File dependentLibs = new File( ndkOutputDirectory.getAbsolutePath(), ndkArchitecture );
                    //addSharedLibraries( jarArchiver, dependentLibs, prefix );
                }
            }
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "IOException while creating ." + AAR + " file.", e );
        }
        // TODO: Next is to check for any:
        // TODO: - compiled in (as part of this build) libs
        // TODO:    - That is of course easy if the artifact is indeed attached
        // TODO:    - If not attached, it gets a little trickier  - check the target dir for any compiled .so files (generated by NDK mojo)
        // TODO:        - But where is that directory configured?
    }

    /**
     * Makes sure the string ends with "/"
     *
     * @param prefix any string, or null.
     * @return the prefix with a "/" at the end, never null.
     */
    protected String endWithSlash( String prefix )
    {
        prefix = StringUtils.defaultIfEmpty( prefix, "/" );
        if ( ! prefix.endsWith( "/" ) )
        {
            prefix = prefix + "/";
        }
        return prefix;
    }

    /**
     * Adds a directory to a {@link JarArchiver} with a directory prefix.
     *
     * @param zipArchiver   ZipArchiver to use to archive the file.
     * @param directory     The directory to add.
     * @param prefix        An optional prefix for where in the Jar file the directory's contents should go.
     * @param includeEmptyFolders   Whether to include an entry for empty folder in the archive.
     */
    protected void addDirectory( ZipArchiver zipArchiver, File directory, String prefix, boolean includeEmptyFolders )
    {
        if ( directory != null && directory.exists() )
        {
            final DefaultFileSet fileSet = new DefaultFileSet();
            fileSet.setPrefix( endWithSlash( prefix ) );
            fileSet.setDirectory( directory );
            fileSet.setIncludingEmptyDirectories( includeEmptyFolders );
            zipArchiver.addFileSet( fileSet );
            getLog().debug( "Added files from " + directory );
        }
    }

    /**
     * Adds all shared libraries (.so) to a {@link JarArchiver} under 'jni'.
     *
     * @param zipArchiver The jarArchiver to add files to
     * @param directory   The directory to scan for .so files
     * @param architecture      The prefix for where in the jar the .so files will go.
     */
    protected void addSharedLibraries( ZipArchiver zipArchiver, File directory, String architecture )
    {
        getLog().debug( "Searching for shared libraries in " + directory );
        File[] libFiles = directory.listFiles( new FilenameFilter()
        {
            public boolean accept( final File dir, final String name )
            {
                return name.startsWith( "lib" ) && name.endsWith( ".so" );
            }
        } );

        if ( libFiles != null )
        {
            for ( File libFile : libFiles )
            {
                String dest = NATIVE_LIBRARIES_FOLDER + "/" + architecture + "/" + libFile.getName();
                getLog().debug( "Adding " + libFile + " as " + dest );
                zipArchiver.addFile( libFile, dest );
            }
        }
    }

    /**
     * Generates an intermediate apk file (actually .ap_) containing the resources and assets.
     *
     * @throws MojoExecutionException
     */
    private void generateIntermediateApk() throws MojoExecutionException
    {
        // Have to generate the AAR against the dependent resources or build will fail if any local resources
        // directly reference any of the dependent resources. NB this does NOT include the dep resources in the AAR.
        List<File> dependenciesResDirectories = new ArrayList<File>();
        for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
        {
            final File apkLibResDir = getUnpackedLibResourceFolder( libraryArtifact );
            if ( apkLibResDir.exists() )
            {
                dependenciesResDirectories.add( apkLibResDir );
            }
        }

        final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( this.getLog() );

        File outputFile = new File( targetDirectory, finalName + ".ap_" );

        final AaptCommandBuilder commandBuilder = AaptCommandBuilder
                .packageResources( getLog() )
                .makePackageDirectories()
                .forceOverwriteExistingFiles()
                .setPathToAndroidManifest( destinationManifestFile )
                .addResourceDirectoriesIfExists( getResourceOverlayDirectories() )
                .addResourceDirectoryIfExists( resourceDirectory )
                .addResourceDirectoriesIfExists( dependenciesResDirectories )
                .autoAddOverlay()
                .addRawAssetsDirectoryIfExists( combinedAssets )
                .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
                .setOutputApkFile( outputFile )
                .addConfigurations( configurations )
                .setResourceConstantsFolder( genDirectory )
                .makeResourcesNonConstant()
                .generateRTextFile( targetDirectory )
                .setVerbose( aaptVerbose );

        getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        getLog().info( "Generating aar" );
        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 );
        }
    }
}


/*
 * 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.phase09package;

import com.android.sdklib.build.ApkBuilder;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.build.SealedApkException;
import com.google.common.io.Files;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.AndroidNdk;
import com.simpligility.maven.plugins.android.AndroidSigner;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.IncludeExcludeSet;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.config.ConfigHandler;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.config.PullParameter;
import com.simpligility.maven.plugins.android.configuration.Apk;
import com.simpligility.maven.plugins.android.configuration.MetaInf;
import com.simpligility.maven.plugins.android.configuration.Sign;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FileFileFilter;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.plugins.shade.resource.ResourceTransformer;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
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;

/**
 * Creates the apk file. By default signs it with debug keystore.

 * Change that by setting configuration parameter
 * <sign><debug>false</debug></sign>.
 *
 * @author hugo.josefson@jayway.com
 */
@Mojo( name = "apk",
       defaultPhase = LifecyclePhase.PACKAGE,
       requiresDependencyResolution = ResolutionScope.COMPILE )
public class ApkMojo extends AbstractAndroidMojo
{

    /**
     *
How to sign the apk.

     *
Looks like this:

     *

     * <sign>
     *     <debug>auto</debug>
     * </sign>
     *

     *
Valid values for <debug> are:
     *

     *
true = sign with the debug keystore.
     *
false = don't sign with the debug keystore.
     *
both = create a signed as well as an unsigned apk.
     *
auto (default) = sign with debug keystore, unless another keystore is defined. (Signing with
     * other keystores is not yet implemented. See
     * Issue 2.)
     *

     *
Can also be configured from command-line with parameter -Dandroid.sign.debug.

     */
    @Parameter
    private Sign sign;

    /**
     *
Parameter designed to pick up -Dandroid.sign.debug in case there is no pom with a
     * <sign> configuration tag.

     *
Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sign#debug}.

     */
    @Parameter( property = "android.sign.debug", defaultValue = "auto", readonly = true )
    private String signDebug;

    /**
     *
Rewrite the manifest so that all of its instrumentation components target the given package.
     * This value will be passed on to the aapt parameter --rename-instrumentation-target-package.
     * Look to aapt for more help on this.

     *
     * TODO pass this into AaptExecutor
     */
    @Parameter( property = "android.renameInstrumentationTargetPackage" )
    private String renameInstrumentationTargetPackage;

    /**
     *
Allows to detect and extract the duplicate files from embedded jars. In that case, the plugin analyzes
     * the content of all embedded dependencies and checks they are no duplicates inside those dependencies. Indeed,
     * Android does not support duplicates, and all dependencies are inlined in the APK. If duplicates files are found,
     * the resource is kept in the first dependency and removes from others.
     */
    @Parameter( property = "android.extractDuplicates", defaultValue = "false" )
    private boolean extractDuplicates;

    /**
     *
Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.

     */
    @Parameter
    private String classifier;

    /**
     * The apk file produced by the apk goal. Per default the file is placed into the build directory (target
     * normally) using the build final name and apk as extension.
     */
    @Parameter( property = "android.outputApk",
                defaultValue = "${project.build.directory}/${project.build.finalName}.apk" )
    private String outputApk;

    /**
     *
Additional source directories that contain resources to be packaged into the apk.

     *
These are not source directories, that contain java classes to be compiled.
     * It corresponds to the -df option of the apkbuilder program. It allows you to specify directories,
     * that contain additional resources to be packaged into the apk.

     * So an example inside the plugin configuration could be:
     *

     * <configuration>
     *   ...
     *    <sourceDirectories>
     *      <sourceDirectory>${project.basedir}/additionals</sourceDirectory>
     *   </sourceDirectories>
     *   ...
     * </configuration>
     *

     */
    @Parameter( property = "android.sourceDirectories" )
    private File[] sourceDirectories;

    /**
     * Pattern for additional META-INF resources to be packaged into the apk.
     *

     * The APK builder filters these resources and doesn't include them into the apk.
     * This leads to bad behaviour of dependent libraries relying on these resources,
     * for instance service discovery doesn't work.

     * By specifying this pattern, the android plugin adds these resources to the final apk.
     *

     *
The pattern is relative to META-INF, i.e. one must use
     *

     *
     * <apkMetaIncludes>
     *     <metaInclude>services/**</metaInclude>
     * </apkMetaIncludes>
     *
     *

     * ... instead of
     *

     *
     * <apkMetaIncludes>
     *     <metaInclude>META-INF/services/**</metaInclude>
     * </apkMetaIncludes>
     *
     *

     *

     * See also Issue 97
     *

     *
     * @deprecated in favour of apk.metaInf
     */
    @PullParameter
    private String[] apkMetaIncludes;

    @PullParameter( defaultValueGetterMethod = "getDefaultMetaInf" )
    private MetaInf apkMetaInf;

    @Parameter( alias = "metaInf" )
    private MetaInf pluginMetaInf;

    /**
     * Defines whether or not the APK is being produced in debug mode or not.
     */
    @Parameter( property = "android.apk.debug" )
    @PullParameter( defaultValue = "false" )
    private Boolean apkDebug;

    @Parameter( property = "android.nativeToolchain" )
    @PullParameter( defaultValue = "arm-linux-androideabi-4.4.3" )
    private String apkNativeToolchain;

    /**
     * Specifies the final name of the library output by the build (this allows
     */
    @Parameter( property = "android.ndk.build.build.final-library.name" )
    private String ndkFinalLibraryName;

    /**
     * Specify a list of patterns that are matched against the names of jar file
     * dependencies. Matching jar files will not have their resources added to the
     * resulting APK.
     *
     * The patterns are standard Java regexes.
     */
    @Parameter
    private String[] excludeJarResources;

    private Pattern[] excludeJarResourcesPatterns;

    /**
     * Embedded configuration of this mojo.
     */
    @Parameter
    @ConfigPojo( prefix = "apk" )
    private Apk apk;

    /**
     * Skips transitive dependencies. May be useful if the target classes directory is populated with the
     * {@code maven-dependency-plugin} and already contains all dependency classes.
     */
    @Parameter( property = "skipDependencies", defaultValue = "false" )
    private boolean skipDependencies;

    /**
     * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
     * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
     * strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>aar</include>
     *         <includes>
     *         <excludes>
     *             <exclude>jar</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactTypeSet" )
    private IncludeExcludeSet artifactTypeSet;

    /**
     * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
     * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
     * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
     * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
     * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
     * range. Empty strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>foo-group:foo-artifact:1.0-SNAPSHOT</include>
     *             <include>bar-group:bar-artifact:1.0-SNAPSHOT</include>
     *             <include>baz-group:*</include>
     *         <includes>
     *         <excludes>
     *             <exclude>qux-group:qux-artifact:*</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactSet" )
    private IncludeExcludeSet artifactSet;

    private static final Pattern PATTERN_JAR_EXT = Pattern.compile( "^.+\\.jar$", Pattern.CASE_INSENSITIVE );

    private static final String DEX_SUFFIX = ".dex";

    private static final String CLASSES = "classes";

    /**
     *
Default hardware architecture for native library dependencies (with {@code <type>so</type>})
     * without a classifier.

     *
Valid values currently include {@code armeabi}, {@code armeabi-v7a}, {@code mips} and {@code x86}.

     */
    @Parameter( property = "android.nativeLibrariesDependenciesHardwareArchitectureDefault", defaultValue = "armeabi" )
    private String nativeLibrariesDependenciesHardwareArchitectureDefault;

    @Parameter
    private ResourceTransformer[] transformers;

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {

        // Make an early exit if we're not supposed to generate the APK
        if ( ! generateApk )
        {
            return;
        }

        ConfigHandler cfh = new ConfigHandler( this, this.session, this.execution );

        cfh.parseConfiguration();

        generateIntermediateApk();

        // Compile resource exclusion patterns, if any
        if ( excludeJarResources != null && excludeJarResources.length > 0 )
        {
          getLog().debug( "Compiling " + excludeJarResources.length + " patterns" );

          excludeJarResourcesPatterns = new Pattern[excludeJarResources.length];

          for ( int index = 0; index < excludeJarResources.length; ++index )
          {
            excludeJarResourcesPatterns[index] = Pattern.compile( excludeJarResources[index] );
          }
        }

        // Initialize apk build configuration
        File outputFile = new File( outputApk );
        final boolean signWithDebugKeyStore = getAndroidSigner().isSignWithDebugKeyStore();

        if ( getAndroidSigner().shouldCreateBothSignedAndUnsignedApk() )
        {
            getLog().info( "Creating debug key signed apk file " + outputFile );
            createApkFile( outputFile, true );
            final File unsignedOutputFile = new File( targetDirectory,
                    finalName + "-unsigned." + APK );
            getLog().info( "Creating additional unsigned apk file " + unsignedOutputFile );
            createApkFile( unsignedOutputFile, false );
            projectHelper.attachArtifact( project, unsignedOutputFile,
                    classifier == null ? "unsigned" : classifier + "_unsigned" );
        }
        else
        {
            createApkFile( outputFile, signWithDebugKeyStore );
        }

        if ( classifier == null )
        {
            // Set the generated .apk file as the main artifact (because the pom states <packaging>apk</packaging>)
            project.getArtifact().setFile( outputFile );
        }
        else
        {
            // If there is a classifier specified, attach the artifact using that
            projectHelper.attachArtifact( project, AndroidExtension.APK, classifier, outputFile );
        }
    }

    void createApkFile( File outputFile, boolean signWithDebugKeyStore ) throws MojoExecutionException
    {
        //this needs to come from DexMojo
        File dexFile = new File( targetDirectory, "classes.dex" );
        if ( !dexFile.exists() )
        {
            dexFile = new File( targetDirectory, "classes.zip" );
        }

        File zipArchive = new File( targetDirectory, finalName + ".ap_" );
        ArrayList<File> sourceFolders = new ArrayList<File>();
        if ( sourceDirectories != null )
        {
            sourceFolders.addAll( Arrays.asList( sourceDirectories ) );
        }
        ArrayList<File> jarFiles = new ArrayList<File>();

        // Process the native libraries, looking both in the current build directory as well as
        // at the dependencies declared in the pom.  Currently, all .so files are automatically included
        final Collection<File> nativeFolders = getNativeLibraryFolders();
        getLog().info( "Adding native libraries : " + nativeFolders );

        doAPKWithAPKBuilder( outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders,
                signWithDebugKeyStore );

        if ( this.apkMetaInf != null )
        {
            File outputJar = new File( outputApk.substring( 0, outputApk.length() - 3 ) + "jar" );
            if ( outputJar.exists() )
            {
                jarFiles.add( outputJar );
            }
            else
            {
                getLog().warn( "Output jar doesn't exist:" + outputJar );
            }
            try
            {
                addMetaInf( outputFile, jarFiles );
            }
            catch ( IOException e )
            {
                throw new MojoExecutionException( "Could not add META-INF resources.", e );
            }
        }
    }

    private void addMetaInf( File outputFile, ArrayList<File> jarFiles ) throws IOException
    {
        File tmp = File.createTempFile( outputFile.getName(), ".add", outputFile.getParentFile() );

        FileOutputStream fos = new FileOutputStream( tmp );
        JarOutputStream zos = new JarOutputStream( fos );
        Set<String> entries = new HashSet<String>();

        updateWithMetaInf( zos, outputFile, entries, false );

        for ( File f : jarFiles )
        {
            updateWithMetaInf( zos, f, entries, true );
        }

        if ( transformers != null )
        {
            for ( ResourceTransformer transformer : transformers )
            {
                if ( transformer.hasTransformedResource() )
                {
                    transformer.modifyOutputStream( zos );
                }
            }
        }

        zos.close();

        outputFile.delete();

        if ( ! tmp.renameTo( outputFile ) )
        {
            throw new IOException( String.format( "Cannot rename %s to %s", tmp, outputFile.getName() ) );
        }
    }

    private void updateWithMetaInf( ZipOutputStream zos, File jarFile, Set<String> entries, boolean metaInfOnly )
            throws IOException
    {
        ZipFile zin = new ZipFile( jarFile );

        for ( Enumeration<? extends ZipEntry> en = zin.entries(); en.hasMoreElements(); )
        {
            ZipEntry ze = en.nextElement();

            if ( ze.isDirectory() )
            {
                continue;
            }

            String zn = ze.getName();

            if ( metaInfOnly )
            {
                if ( ! zn.startsWith( "META-INF/" ) )
                {
                    continue;
                }

                if ( ! this.apkMetaInf.isIncluded( zn ) )
                {
                    continue;
                }
            }

            boolean resourceTransformed = false;

            if ( transformers != null )
            {
                for ( ResourceTransformer transformer : transformers )
                {
                    if ( transformer.canTransformResource( zn ) )
                    {
                        getLog().info( "Transforming " + zn + " using " + transformer.getClass().getName() );
                        InputStream is = zin.getInputStream( ze );
                        transformer.processResource( zn, is, null );
                        is.close();
                        resourceTransformed = true;
                        break;
                    }
                }
            }

            if ( !resourceTransformed )
            {
                // Avoid duplicates that aren't accounted for by the resource transformers
                if ( metaInfOnly && this.extractDuplicates && ! entries.add( zn ) )
                {
                    continue;
                }

                InputStream is = zin.getInputStream( ze );

                final ZipEntry ne;
                if ( ze.getMethod() == ZipEntry.STORED )
                {
                    ne = new ZipEntry( ze );
                }
                else
                {
                    ne = new ZipEntry( zn );
                }

                zos.putNextEntry( ne );

                copyStreamWithoutClosing( is, zos );

                is.close();
                zos.closeEntry();
            }
        }

        zin.close();
    }

    private Map<String, List<File>> jars = new HashMap<String, List<File>>();

    private void computeDuplicateFiles( File jar ) throws IOException
    {
        ZipFile file = new ZipFile( jar );
        Enumeration<? extends ZipEntry> list = file.entries();
        while ( list.hasMoreElements() )
        {
            ZipEntry ze = list.nextElement();
            if ( ! ( ze.getName().contains( "META-INF/" ) || ze.isDirectory() ) )
            { // Exclude META-INF and Directories
                List<File> l = jars.get( ze.getName() );
                if ( l == null )
                {
                    l = new ArrayList<File>();
                    jars.put( ze.getName(), l );
                }
                l.add( jar );
            }
        }
    }

    private void computeDuplicateFilesInSource( File folder )
    {
        String rPath = folder.getAbsolutePath();
        for ( File file : Files.fileTreeTraverser().breadthFirstTraversal( folder ).toList() )
        {
            String lPath = file.getAbsolutePath();
            if ( lPath.equals( rPath ) )
            {
                continue; //skip the root
            }
            lPath = lPath.substring( rPath.length() + 1 ); //strip root folder to make relative path

            if ( jars.get( lPath ) == null )
            {
                jars.put( lPath, new ArrayList<File>() );
            }
            jars.get( lPath ).add( folder );
        }
    }

    private void extractDuplicateFiles( List<File> jarFiles, Collection<File> sourceFolders ) throws IOException
    {
        getLog().debug( "Extracting duplicates" );
        List<String> duplicates = new ArrayList<String>();
        List<File> jarToModify = new ArrayList<File>();
        for ( String s : jars.keySet() )
        {
            List<File> l = jars.get( s );
            if ( l.size() > 1 )
            {
                getLog().warn( "Duplicate file " + s + " : " + l );
                duplicates.add( s );
                for ( int i = 0; i < l.size(); i++ )
                {
                    if ( ! jarToModify.contains( l.get( i ) ) )
                    {
                        jarToModify.add( l.get( i ) );
                    }
                }
            }
        }

        // Rebuild jars.  Remove duplicates from ALL jars, then add them back into a duplicate-resources.jar
        File tmp = new File( targetDirectory.getAbsolutePath(), "unpacked-embedded-jars" );
        tmp.mkdirs();
        File duplicatesJar = new File( tmp, "duplicate-resources.jar" );
        Set<String> duplicatesAdded = new HashSet<String>();

        duplicatesJar.createNewFile();
        final FileOutputStream fos = new FileOutputStream( duplicatesJar );
        final JarOutputStream zos = new JarOutputStream( fos );

        for ( File file : jarToModify )
        {
            final int index = jarFiles.indexOf( file );
            if ( index != -1 )
            {
                final File newJar = removeDuplicatesFromJar( file, duplicates, duplicatesAdded, zos, index );
                getLog().debug( "Removed duplicates from " + newJar );
                if ( newJar != null )
                {
                    jarFiles.set( index, newJar );
                }
            }
            else
            {
                removeDuplicatesFromFolder( file, file, duplicates, duplicatesAdded, zos );
                getLog().debug( "Removed duplicates from " + file );
            }
        }
        //add transformed resources to duplicate-resources.jar
        if ( transformers != null )
        {
            for ( ResourceTransformer transformer : transformers )
            {
                if ( transformer.hasTransformedResource() )
                {
                    transformer.modifyOutputStream( zos );
                }
            }
        }
        zos.close();
        fos.close();

        if ( !jarToModify.isEmpty() && duplicatesJar.length() > 0 )
        {
            jarFiles.add( duplicatesJar );
        }
    }

    /**
     * Creates the APK file using the internal APKBuilder.
     *
     * @param outputFile            the output file
     * @param dexFile               the dex file
     * @param zipArchive            the classes folder
     * @param sourceFolders         the resources
     * @param jarFiles              the embedded java files
     * @param nativeFolders         the native folders
     * @param signWithDebugKeyStore enables the signature of the APK using the debug key
     * @throws MojoExecutionException if the APK cannot be created.
     */
    private void doAPKWithAPKBuilder( File outputFile, File dexFile, File zipArchive, Collection<File> sourceFolders,
                                      List<File> jarFiles, Collection<File> nativeFolders,
                                      boolean signWithDebugKeyStore ) throws MojoExecutionException
    {
        getLog().debug( "Building APK with internal APKBuilder" );

        //A when jack is running the classes directory will not get filled (usually)
        // so let's skip it if it wasn't created by something else
        if ( projectOutputDirectory.exists() || !getJack().isEnabled() )
        {
            sourceFolders.add( projectOutputDirectory );
        }

        for ( Artifact artifact : filterArtifacts( getRelevantCompileArtifacts(), skipDependencies,
                artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
                artifactSet.getExcludes() ) )
        {
            getLog().debug( "Found artifact for APK :" + artifact );
            if ( extractDuplicates )
            {
                try
                {
                    computeDuplicateFiles( artifact.getFile() );
                }
                catch ( Exception e )
                {
                    getLog().warn( "Cannot compute duplicates files from " + artifact.getFile().getAbsolutePath(), e );
                }
            }
            jarFiles.add( artifact.getFile() );
        }

        for ( File src : sourceFolders )
        {
            computeDuplicateFilesInSource( src );
        }

        // Check duplicates.
        if ( extractDuplicates )
        {
            try
            {
                extractDuplicateFiles( jarFiles, sourceFolders );
            }
            catch ( IOException e )
            {
                getLog().error( "Could not extract duplicates to duplicate-resources.jar", e );
            }
        }

        try
        {
            final String debugKeyStore = signWithDebugKeyStore ? ApkBuilder.getDebugKeystore() : null;
            final ApkBuilder apkBuilder = new ApkBuilder( outputFile, zipArchive, dexFile, debugKeyStore, null );
            if ( apkDebug )
            {
                apkBuilder.setDebugMode( true );
            }

            for ( File sourceFolder : sourceFolders )
            {
                getLog().debug( "Adding source folder : " + sourceFolder );
                // Use ApkBuilder#addFile() to explicitly add resource files so that we can add META-INF/services.
                addResourcesFromFolder( apkBuilder, sourceFolder );
            }

            for ( File jarFile : jarFiles )
            {
                boolean excluded = false;

                if ( excludeJarResourcesPatterns != null )
                {
                    final String name = jarFile.getName();
                    getLog().debug( "Checking " + name + " against patterns" );
                    for ( Pattern pattern : excludeJarResourcesPatterns )
                    {
                        final Matcher matcher = pattern.matcher( name );
                        if ( matcher.matches() )
                        {
                            getLog().debug( "Jar " + name + " excluded by pattern " + pattern );
                            excluded = true;
                            break;
                        }
                        else
                        {
                            getLog().debug( "Jar " + name + " not excluded by pattern " + pattern );
                        }
                    }
                }

                if ( excluded )
                {
                    continue;
                }

                if ( jarFile.isDirectory() )
                {
                    getLog().debug( "Adding resources from jar folder : " + jarFile );
                    final String[] filenames = jarFile.list( new FilenameFilter()
                    {
                        public boolean accept( File dir, String name )
                        {
                            return PATTERN_JAR_EXT.matcher( name ).matches();
                        }
                    } );

                    for ( String filename : filenames )
                    {
                        final File innerJar = new File( jarFile, filename );
                        getLog().debug( "Adding resources from innerJar : " + innerJar );
                        apkBuilder.addResourcesFromJar( innerJar );
                    }
                }
                else
                {
                    getLog().debug( "Adding resources from : " + jarFile );
                    apkBuilder.addResourcesFromJar( jarFile );
                }
            }

            addSecondaryDexes( dexFile, apkBuilder );

            for ( File nativeFolder : nativeFolders )
            {
                getLog().debug( "Adding native library : " + nativeFolder );
                apkBuilder.addNativeLibraries( nativeFolder );
            }
            apkBuilder.sealApk();
        }
        catch ( ApkCreationException | SealedApkException | IOException e )
        {
            throw new MojoExecutionException( e.getMessage(), e );
        }
        catch ( DuplicateFileException e )
        {
            final String msg = String.format( "Duplicated file: %s, found in archive %s and %s",
                    e.getArchivePath(), e.getFile1(), e.getFile2() );
            throw new MojoExecutionException( msg, e );
        }
    }

    /**
     * Collect all Files from Folder (recursively) that are not class files.
     */
    private void collectFiles( File folder, final List<File> collectedFiles )
    {
        folder.listFiles( new FileFilter()
        {
            @Override
            public boolean accept( File file )
            {
                if ( file.isDirectory() )
                {
                    collectFiles( file, collectedFiles );
                }
                else if ( file.isFile() )
                {
                    if ( !file.getName().endsWith( ".class" ) )
                    {
                        collectedFiles.add( file );
                    }
                }
                return false;
            }
        } );

    }
    /**
     * Adds all non-class files from folder, so that we can add META-INF/services resources.
     */
    private void addResourcesFromFolder( ApkBuilder builder,  File folder )
            throws SealedApkException, DuplicateFileException, ApkCreationException, IOException
    {
        final int folderPathLength = folder.getCanonicalPath().length();

        final List<File> resourceFiles = new ArrayList<>(  );
        collectFiles( folder, resourceFiles );

        for ( final File resourceFile : resourceFiles )
        {
            final String resourceName = resourceFile
                    .getCanonicalPath()
                    .substring( folderPathLength + 1 )
                    .replaceAll( "\\\\", "/" );
            getLog().info( "Adding resource " + resourceFile + " : " + resourceName );
            builder.addFile( resourceFile,  resourceName );
        }
    }

    private void addSecondaryDexes( File dexFile, ApkBuilder apkBuilder ) throws ApkCreationException,
            SealedApkException, DuplicateFileException
    {
        int dexNumber = 2;
        String dexFileName = getNextDexFileName( dexNumber );
        File secondDexFile = createNextDexFile( dexFile, dexFileName );
        while ( secondDexFile.exists() )
        {
            apkBuilder.addFile( secondDexFile, dexFileName );
            dexNumber++;
            dexFileName = getNextDexFileName( dexNumber );
            secondDexFile = createNextDexFile( dexFile, dexFileName );
        }
    }

    private File createNextDexFile( File dexFile, String dexFileName )
    {
        return new File( dexFile.getParentFile(), dexFileName );
    }

    private String getNextDexFileName( int dexNumber )
    {
        return CLASSES + dexNumber + DEX_SUFFIX;
    }

    private File removeDuplicatesFromJar( File in, List<String> duplicates,
                                          Set<String> duplicatesAdded, ZipOutputStream duplicateZos, int num )
    {
        String target = targetDirectory.getAbsolutePath();
        File tmp = new File( target, "unpacked-embedded-jars" );
        tmp.mkdirs();
        String jarName = String.format( "%s-%d.%s",
           Files.getNameWithoutExtension( in.getName() ), num, Files.getFileExtension( in.getName() ) );
        File out = new File( tmp, jarName );

        if ( out.exists() )
        {
            return out;
        }
        else
        {
            try
            {
                out.createNewFile();
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }

        // Create a new Jar file
        final FileOutputStream fos;
        final ZipOutputStream jos;
        try
        {
            fos = new FileOutputStream( out );
            jos = new ZipOutputStream( fos );
        }
        catch ( FileNotFoundException e1 )
        {
            getLog().error( "Cannot remove duplicates : the output file " + out.getAbsolutePath() + " does not found" );
            return null;
        }

        final ZipFile inZip;
        try
        {
            inZip = new ZipFile( in );
            Enumeration<? extends ZipEntry> entries = inZip.entries();
            while ( entries.hasMoreElements() )
            {
                ZipEntry entry = entries.nextElement();
                // If the entry is not a duplicate, copy.
                if ( ! duplicates.contains( entry.getName() ) )
                {
                    // copy the entry header to jos
                    jos.putNextEntry( entry );
                    InputStream currIn = inZip.getInputStream( entry );
                    copyStreamWithoutClosing( currIn, jos );
                    currIn.close();
                    jos.closeEntry();
                }
                //if it is duplicate, check the resource transformers
                else
                {
                    boolean resourceTransformed = false;
                    if ( transformers != null )
                    {
                        for ( ResourceTransformer transformer : transformers )
                        {
                            if ( transformer.canTransformResource( entry.getName() ) )
                            {
                                getLog().info( "Transforming " + entry.getName()
                                        + " using " + transformer.getClass().getName() );
                                InputStream currIn = inZip.getInputStream( entry );
                                transformer.processResource( entry.getName(), currIn, null );
                                currIn.close();
                                resourceTransformed = true;
                                break;
                            }
                        }
                    }
                    //if not handled by transformer, add (once) to duplicates jar
                    if ( !resourceTransformed )
                    {
                        if ( !duplicatesAdded.contains( entry.getName() ) )
                        {
                            duplicatesAdded.add( entry.getName() );
                            duplicateZos.putNextEntry( entry );
                            InputStream currIn = inZip.getInputStream( entry );
                            copyStreamWithoutClosing( currIn, duplicateZos );
                            currIn.close();
                            duplicateZos.closeEntry();
                        }
                    }
                }
            }
        }
        catch ( IOException e )
        {
            getLog().error( "Cannot removing duplicates : " + e.getMessage() );
            return null;
        }

        try
        {
            inZip.close();
            jos.close();
            fos.close();
        }
        catch ( IOException e )
        {
            // ignore it.
        }
        getLog().info( in.getName() + " rewritten without duplicates : " + out.getAbsolutePath() );
        return out;
    }

    private void removeDuplicatesFromFolder( File root, File in, List<String> duplicates,
       Set<String> duplicatesAdded, ZipOutputStream duplicateZos )
    {
        String rPath = root.getAbsolutePath();
        try
        {
            for ( File f : in.listFiles() )
            {
                if ( f.isDirectory() )
                {
                    removeDuplicatesFromFolder( root, f, duplicates, duplicatesAdded, duplicateZos );
                }
                else
                {
                    String lName = f.getAbsolutePath();
                    lName = lName.substring( rPath.length() + 1 ); //make relative path
                    if ( duplicates.contains( lName ) )
                    {
                        boolean resourceTransformed = false;
                        if ( transformers != null )
                        {
                            for ( ResourceTransformer transformer : transformers )
                            {
                                if ( transformer.canTransformResource( lName ) )
                                {
                                    getLog().info( "Transforming " + lName
                                       + " using " + transformer.getClass().getName() );
                                    InputStream currIn = new FileInputStream( f );
                                    transformer.processResource( lName, currIn, null );
                                    currIn.close();
                                    resourceTransformed = true;
                                    break;
                                }
                            }
                        }
                        //if not handled by transformer, add (once) to duplicates jar
                        if ( !resourceTransformed )
                        {
                            if ( !duplicatesAdded.contains( lName ) )
                            {
                                duplicatesAdded.add( lName );
                                ZipEntry entry = new ZipEntry( lName );
                                duplicateZos.putNextEntry( entry );
                                InputStream currIn = new FileInputStream( f );
                                copyStreamWithoutClosing( currIn, duplicateZos );
                                currIn.close();
                                duplicateZos.closeEntry();
                            }
                        }
                        f.delete();
                    }
                }
            }
        }
        catch ( IOException e )
        {
            getLog().error( "Cannot removing duplicates : " + e.getMessage() );
        }
    }

    /**
     * Copies an input stream into an output stream but does not close the streams.
     *
     * @param in  the input stream
     * @param out the output stream
     * @throws IOException if the stream cannot be copied
     */
    private static void copyStreamWithoutClosing( InputStream in, OutputStream out ) throws IOException
    {
        final int bufferSize = 4096;
        byte[] b = new byte[ bufferSize ];
        int n;
        while ( ( n = in.read( b ) ) != - 1 )
        {
            out.write( b, 0, n );
        }
    }

    private Collection<File> getNativeLibraryFolders() throws MojoExecutionException
    {
        final List<File> natives = new ArrayList<File>();

        if ( nativeLibrariesDirectory.exists() )
        {
            // If we have prebuilt native libs then copy them over to the native output folder.
            // NB they will be copied over the top of any native libs generated as part of the NdkBuildMojo
            copyLocalNativeLibraries( nativeLibrariesDirectory, ndkOutputDirectory );
        }

        final Set<Artifact> artifacts = getNativeLibraryArtifacts();
        for ( Artifact resolvedArtifact : artifacts )
        {
            if ( APKLIB.equals( resolvedArtifact.getType() ) || AAR.equals( resolvedArtifact.getType() ) )
            {
                // If the artifact is an AAR or APKLIB then add their native libs folder to the result.
                final File folder = getUnpackedLibNativesFolder( resolvedArtifact );
                getLog().debug( "Adding native library folder " + folder );
                natives.add( folder );
            }

            // Copy the native lib dependencies into the native lib output folder
            for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
            {
                if ( NativeHelper.artifactHasHardwareArchitecture( resolvedArtifact,
                        ndkArchitecture, nativeLibrariesDependenciesHardwareArchitectureDefault ) )
                {
                    // If the artifact is a native lib then copy it into the native libs output folder.
                    copyNativeLibraryArtifact( resolvedArtifact, ndkOutputDirectory, ndkArchitecture );
                }
            }
        }

        if ( apkDebug )
        {
            // Copy the gdbserver binary into the native libs output folder (for each architecture).
            for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
            {
                copyGdbServer( ndkOutputDirectory, ndkArchitecture );
            }
        }

        if ( ndkOutputDirectory.exists() )
        {
            // If we have any native libs in the native output folder then add the output folder to the result.
            getLog().debug( "Adding built native library folder " + ndkOutputDirectory );
            natives.add( ndkOutputDirectory );
        }

        return natives;
    }

    /**
     * @return Any native dependencies or attached artifacts. This may include artifacts from the ndk-build MOJO.
     * @throws MojoExecutionException
     */
    private Set<Artifact> getNativeLibraryArtifacts() throws MojoExecutionException
    {
        return getNativeHelper().getNativeDependenciesArtifacts( this, getUnpackedLibsDirectory(), true );
    }

    private void copyNativeLibraryArtifact( Artifact artifact,
                                            File destinationDirectory,
                                            String ndkArchitecture ) throws MojoExecutionException
    {

        final File artifactFile = getArtifactResolverHelper().resolveArtifactToFile( artifact );
        try
        {
            final String artifactId = artifact.getArtifactId();
            String filename = artifactId.startsWith( "lib" )
                    ? artifactId + ".so"
                    : "lib" + artifactId + ".so";
            if ( ndkFinalLibraryName != null
                    && artifact.getFile().getName().startsWith( "lib" + ndkFinalLibraryName ) )
            {
                // The artifact looks like one we built with the NDK in this module
                // preserve the name from the NDK build
                filename = artifact.getFile().getName();
            }

            final File folder = new File( destinationDirectory, ndkArchitecture );
            final File file = new File( folder, filename );
            getLog().debug( "Copying native dependency " + artifactId + " (" + artifact.getGroupId() + ") to " + file );
            FileUtils.copyFile( artifactFile, file );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Could not copy native dependency.", e );
        }
    }

    /**
     * Copy the Ndk GdbServer into the architecture output folder if the folder exists but the GdbServer doesn't.
     */
    private void copyGdbServer( File destinationDirectory, String architecture ) throws MojoExecutionException
    {

        try
        {
            final File destDir = new File( destinationDirectory, architecture );
            if ( destDir.exists() )
            {
                // Copy the gdbserver binary to libs/<architecture>/
                final File gdbServerFile = getAndroidNdk().getGdbServer( architecture );
                final File destFile = new File( destDir, "gdbserver" );
                if ( ! destFile.exists() )
                {
                    getLog().debug( "Copying gdbServer to " + destFile );
                    FileUtils.copyFile( gdbServerFile, destFile );
                }
                else
                {
                    getLog().info( "Note: gdbserver binary already exists at destination, will not copy over" );
                }
            }
        }
        catch ( Exception e )
        {
            getLog().error( "Error while copying gdbserver: " + e.getMessage(), e );
            throw new MojoExecutionException( "Error while copying gdbserver: " + e.getMessage(), e );
        }

    }

    private void copyLocalNativeLibraries( final File localNativeLibrariesDirectory, final File destinationDirectory )
            throws MojoExecutionException
    {
        getLog().debug( "Copying existing native libraries from " + localNativeLibrariesDirectory );
        try
        {

            IOFileFilter libSuffixFilter = FileFilterUtils.suffixFileFilter( ".so" );

            IOFileFilter gdbserverNameFilter = FileFilterUtils.nameFileFilter( "gdbserver" );
            IOFileFilter orFilter = FileFilterUtils.or( libSuffixFilter, gdbserverNameFilter );

            IOFileFilter libFiles = FileFilterUtils.and( FileFileFilter.FILE, orFilter );
            FileFilter filter = FileFilterUtils.or( DirectoryFileFilter.DIRECTORY, libFiles );
            org.apache.commons.io.FileUtils
                    .copyDirectory( localNativeLibrariesDirectory, destinationDirectory, filter );

        }
        catch ( IOException e )
        {
            getLog().error( "Could not copy native libraries: " + e.getMessage(), e );
            throw new MojoExecutionException( "Could not copy native dependency.", e );
        }
    }

    /**
     * Generates an intermediate apk file (actually .ap_) containing the resources and assets.
     *
     * @throws MojoExecutionException
     */
    private void generateIntermediateApk() throws MojoExecutionException
    {
        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( this.getLog() );
        File[] overlayDirectories = getResourceOverlayDirectories();

        File androidJar = getAndroidSdk().getAndroidJar();
        File outputFile = new File( targetDirectory, finalName + ".ap_" );

        List<File> dependencyArtifactResDirectoryList = new ArrayList<File>();
        for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
        {
            final File libraryResDir = getUnpackedLibResourceFolder( libraryArtifact );
            if ( libraryResDir.exists() )
            {
                dependencyArtifactResDirectoryList.add( libraryResDir );
            }
        }

        AaptCommandBuilder commandBuilder = AaptCommandBuilder
                .packageResources( getLog() )
                .forceOverwriteExistingFiles()
                .setPathToAndroidManifest( destinationManifestFile )
                .addResourceDirectoriesIfExists( overlayDirectories )
                .addResourceDirectoryIfExists( resourceDirectory )
                .addResourceDirectoriesIfExists( dependencyArtifactResDirectoryList )
                .autoAddOverlay()
                // NB aapt only accepts a single assets parameter - combinedAssets is a merge of all assets
                .addRawAssetsDirectoryIfExists( combinedAssets )
                .renameManifestPackage( renameManifestPackage )
                .renameInstrumentationTargetPackage( renameInstrumentationTargetPackage )
                .addExistingPackageToBaseIncludeSet( androidJar )
                .setOutputApkFile( outputFile )
                .addConfigurations( configurations )
                .setVerbose( aaptVerbose )
                .setDebugMode( !release )
                .addExtraArguments( aaptExtraArgs );

        getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        try
        {
            executor.setCaptureStdOut( true );
            List<String> commands = commandBuilder.build();
            executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

    protected AndroidSigner getAndroidSigner()
    {
        if ( sign == null )
        {
            return new AndroidSigner( signDebug );
        }
        else
        {
            return new AndroidSigner( sign.getDebug() );
        }
    }

    /**
     * Used to populated the {@link #apkMetaInf} attribute via reflection.
     */
    private MetaInf getDefaultMetaInf()
    {
        // check for deprecated first
        if ( apkMetaIncludes != null && apkMetaIncludes.length > 0 )
        {
            return new MetaInf().include( apkMetaIncludes );
        }

        return this.pluginMetaInf;
    }
}


/*
 * 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.standalonemojos;

import java.io.IOException;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.MonkeyTestRunner;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.AndroidTestRunListener;
import com.simpligility.maven.plugins.android.DeviceCallback;
import com.simpligility.maven.plugins.android.common.DeviceHelper;
import com.simpligility.maven.plugins.android.config.ConfigHandler;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.config.PullParameter;
import com.simpligility.maven.plugins.android.configuration.Monkey;

/**
 * Can execute tests using UI/Application Exerciser Monkey.

 * Implements parsing parameters from pom or command line arguments and sets useful defaults as well. This goal will
 * invoke Android Monkey exerciser. If the application crashes during the exercise, this goal can fail the build.

 * A typical usage of this goal can be found at Quality tools for Android project.
 *
 * @see Monkey docs by Google
 * @see Stack Over Flow thread for parsing monkey output.
 * @author Stéphane Nicolas - snicolas@octo.com
 */
@SuppressWarnings( "unused" )
@Mojo( name = "monkey" )
public class MonkeyMojo extends AbstractAndroidMojo
{
    /**
     * -Dmaven.test.skip is commonly used with Maven to skip tests. We honor it.
     */
    @Parameter( property = "maven.test.skip", defaultValue = "false", readonly = true )
    private boolean mavenTestSkip;

    /**
     * -DskipTests is commonly used with Maven to skip tests. We honor it too.
     */
    @Parameter( property = "skipTests", defaultValue = "false", readonly = true )
    private boolean mavenSkipTests;

    /**
     * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
     * honor it too.
     */
    @Parameter( property = "maven.test.failure.ignore", defaultValue = "false", readonly = true )
    private boolean mavenTestFailureIgnore;

    /**
     * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
     * honor it too.
     */
    @Parameter( property = "testFailureIgnore", defaultValue = "false", readonly = true )
    private boolean mavenIgnoreTestFailure;

    /**
     * The configuration for the ui automator goal. As soon as a lint goal is invoked the command will be executed
     * unless the skip parameter is set. A minimal configuration that will run lint and produce a XML report in
     * ${project.build.directory}/lint/lint-results.xml is
     *
     *

     * <monkey>
     *   <skip>false</skip>
     * </monkey>
     *

     *
     * Full configuration can use these parameters.
     *
     *

     *  <monkey>
     *    <skip>false</skip>
     *    <eventCount>5000</eventCount>
     *    <seed>123456</seed>
     *    <throttle>10</throttle>
     *    <percentTouch>10</percentTouch>
     *    <percentMotion>10</percentMotion>
     *    <percentTrackball>10</percentTrackball>
     *    <percentNav>10</percentNav>
     *    <percentMajorNav>10</percentMajorNav>
     *    <percentSyskeys>10</percentSyskeys>
     *    <percentAppswitch>10</percentAppswitch>
     *    <percentAnyevent>10</percentAnyevent>
     *    <packages>
     *        <package>com.foo</package>
     *        <package>com.bar</package>
     *    </packages>
     *    <categories>
     *        <category>foo</category>
     *        <category>bar</category>
     *    </categories>
     *    <debugNoEvents>true</debugNoEvents>
     *    <hprof>true</hprof>
     *    <ignoreCrashes>true</ignoreCrashes>
     *    <ignoreTimeouts>true</ignoreTimeouts>
     *    <ignoreSecurityExceptions>true</ignoreSecurityExceptions>
     *    <killProcessAfterError>true</killProcessAfterError>
     *    <monitorNativeCrashes>true</monitorNativeCrashes>
     *    <createReport>true</createReport>
     *  </monkey>
     *

     *
     * Alternatively to the plugin configuration values can also be configured as properties on the command line as
     * android.lint.* or in pom or settings file as properties like lint*.
     */
    @Parameter
    @ConfigPojo
    private Monkey monkey;

    /**
     * Enables or disables monkey test goal. If true it will be skipped; if false, it will be
     * run. Defaults to true.
     */
    @Parameter( property = "android.monkey.skip" )
    private Boolean monkeySkip;

    @PullParameter( defaultValue = "true" )
    private Boolean parsedSkip;

    /**
     * Number of generated events. Defaults to 1000.
     */
    @Parameter( property = "android.monkey.eventCount" )
    private Integer monkeyEventCount;

    @PullParameter( required = true, defaultValue = "1000" )
    private Integer parsedEventCount;

    /**
     * Seed value for pseudo-random number generator. If you re-run the Monkey with the same seed value, it will
     * generate the same sequence of events.
     */
    @Parameter( property = "android.monkey.seed" )
    private Long monkeySeed;

    @PullParameter( required = false, defaultValueGetterMethod = "getSeed" )
    private Long parsedSeed;

    /**
     * Inserts a fixed delay between events. You can use this option to slow down the Monkey. If not specified, there is
     * no delay and the events are generated as rapidly as possible.
     */
    @Parameter( property = "android.monkey.throttle" )
    private Long monkeyThrottle;

    @PullParameter( required = false, defaultValueGetterMethod = "getThrottle" )
    private Long parsedThrottle;

    /**
     * Adjust percentage of touch events. (Touch events are a down-up event in a single place on the screen.)
     */
    @Parameter( property = "android.monkey.percentTouch" )
    private Integer monkeyPercentTouch;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentTouch" )
    private Integer parsedPercentTouch;

    /**
     * Adjust percentage of motion events. (Motion events consist of a down event somewhere on the screen, a series of
     * pseudo-random movements, and an up event.)
     */
    @Parameter( property = "android.monkey.percentMotion" )
    private Integer monkeyPercentMotion;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentMotion" )
    private Integer parsedPercentMotion;

    /**
     * Adjust percentage of trackball events. (Trackball events consist of one or more random movements, sometimes
     * followed by a click.)
     */
    @Parameter( property = "android.monkey.percentTrackball" )
    private Integer monkeyPercentTrackball;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentTrackball" )
    private Integer parsedPercentTrackball;

    /**
     * Adjust percentage of "basic" navigation events. (Navigation events consist of up/down/left/right, as input from a
     * directional input device.)
     */
    @Parameter( property = "android.monkey.percentNav" )
    private Integer monkeyPercentNav;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentNav" )
    private Integer parsedPercentNav;

    /**
     * Adjust percentage of "major" navigation events. (These are navigation events that will typically cause actions
     * within your UI, such as the center button in a 5-way pad, the back key, or the menu key.)
     */
    @Parameter( property = "android.monkey.percentMajorNav" )
    private Integer monkeyPercentMajorNav;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentMajorNav" )
    private Integer parsedPercentMajorNav;

    /**
     * Adjust percentage of "system" key events. (These are keys that are generally reserved for use by the system, such
     * as Home, Back, Start Call, End Call, or Volume controls.) Defaults to null.
     */
    @Parameter( property = "android.monkey.percentSyskeys" )
    private Integer monkeyPercentSyskeys;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentSyskeys" )
    private Integer parsedPercentSyskeys;

    /**
     * Adjust percentage of activity launches. At random intervals, the Monkey will issue a startActivity() call, as a
     * way of maximizing coverage of all activities within your package.
     */
    @Parameter( property = "android.monkey.percentAppswitch" )
    private Integer monkeyPercentAppswitch;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentAppswitch" )
    private Integer parsedPercentAppswitch;

    /**
     * Adjust percentage of other types of events. This is a catch-all for all other types of events such as keypresses,
     * other less-used buttons on the device, and so forth.
     */
    @Parameter( property = "android.monkey.percentAnyevent" )
    private Integer monkeyPercentAnyEvent;

    @PullParameter( required = false, defaultValueGetterMethod = "getPercentAnyevent" )
    private Integer parsedPercentAnyevent;

    /**
     * If you specify one or more packages this way, the Monkey will only allow the system to visit activities within
     * those packages. If your application requires access to activities in other packages (e.g. to select a contact)
     * you'll need to specify those packages as well. If you don't specify any packages, the Monkey will allow the
     * system to launch activities in all packages.
     */
    @Parameter( property = "android.monkey.packages" )
    private String[] monkeyPackages;

    @PullParameter( required = false, defaultValueGetterMethod = "getPackages" )
    private String[] parsedPackages;

    /**
     * If you specify one or more categories this way, the Monkey will only allow the system to visit activities that
     * are listed with one of the specified categories. If you don't specify any categories, the Monkey will select
     * activities listed with the category Intent.CATEGORY_LAUNCHER or Intent.CATEGORY_MONKEY.
     */
    @Parameter( property = "android.monkey.categories" )
    private String[] monkeyCategories;

    @PullParameter( required = false, defaultValueGetterMethod = "getCategories" )
    private String[] parsedCategories;

    /**
     * When specified, the Monkey will perform the initial launch into a test activity, but will not generate any
     * further events. For best results, combine with -v, one or more package constraints, and a non-zero throttle to
     * keep the Monkey running for 30 seconds or more. This provides an environment in which you can monitor package
     * transitions invoked by your application.
     */
    @Parameter( property = "android.monkey.debugNoEvents" )
    private Boolean monkeyDebugNoEvents;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedDebugNoEvents;

    /**
     * If set, this option will generate profiling reports immediately before and after the Monkey event sequence. This
     * will generate large (~5Mb) files in data/misc, so use with care. See Traceview for more information on trace
     * files.
     */
    @Parameter( property = "android.monkey.Hprof" )
    private Boolean monkeyHprof;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedHprof;

    /**
     * Normally, the Monkey will stop when the application crashes or experiences any type of unhandled exception. If
     * you specify this option, the Monkey will continue to send events to the system, until the count is completed.
     * Settings this option is different to setting testFailureIgnore or maven.test.failure.ignore to true, it will
     * impact monkey run but not the result of the maven build.
     */
    @Parameter( property = "android.monkey.ignoreCrashes" )
    private Boolean monkeyIgnoreCrashes;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedIgnoreCrashes;

    /**
     * Normally, the Monkey will stop when the application experiences any type of timeout error such as a
     * "Application Not Responding" dialog. If you specify this option, the Monkey will continue to send events to the
     * system, until the count is completed.
     *
     * Defaults to false.
     */
    @Parameter( property = "android.monkey.IgnoreTimeouts" )
    private Boolean monkeyIgnoreTimeouts;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedIgnoreTimeouts;

    /**
     * Normally, the Monkey will stop when the application experiences any type of permissions error, for example if it
     * attempts to launch an activity that requires certain permissions. If you specify this option, the Monkey will
     * continue to send events to the system, until the count is completed. *
     *
     * Defaults to false.
     */
    @Parameter( property = "android.monkey.IgnoreSecurityExceptions" )
    private Boolean monkeyIgnoreSecurityExceptions;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedIgnoreSecurityExceptions;

    /**
     * Normally, when the Monkey stops due to an error, the application that failed will be left running. When this
     * option is set, it will signal the system to stop the process in which the error occurred. Note, under a normal
     * (successful) completion, the launched process(es) are not stopped, and the device is simply left in the last
     * state after the final event.
     *
     * Defaults to false.
     */
    @Parameter( property = "android.monkey.KillProcessAfterError" )
    private Boolean monkeyKillProcessAfterError;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedKillProcessAfterError;

    /**
     * Watches for and reports crashes occurring in the Android system native code. If --kill-process-after-error is
     * set, the system will stop.
     *
     * Defaults to false.
     */
    @Parameter( property = "android.monkey.MonitorNativeCrashes" )
    private Boolean monkeyMonitorNativeCrashes;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedMonitorNativeCrashes;

    /**
     * Create a junit xml format compatible output file containing the test results for each device the instrumentation
     * tests run on.

     *

     * The files are stored in target/surefire-reports and named TEST-deviceid.xml. The deviceid for an emulator is
     * deviceSerialNumber_avdName_manufacturer_model. The serial number is commonly emulator-5554 for the first emulator
     * started with numbers increasing. avdName is as defined in the SDK tool. The manufacturer is typically "unknown"
     * and the model is typically "sdk".

     * The deviceid for an actual devices is deviceSerialNumber_manufacturer_model.

     *

     * The file contains system properties from the system running the Android Maven Plugin (JVM) and device properties
     * from the device/emulator the tests are running on.

     *

     * The file contains a single TestSuite for all tests and a TestCase for each test method. Errors and failures are
     * logged in the file and the system log with full stack traces and other details available.
     *
     * Defaults to false.
     */
    @Parameter( property = "android.monkey.createReport" )
    private Boolean monkeyCreateReport;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedCreateReport;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
        configHandler.parseConfiguration();

        if ( isEnableIntegrationTest() )
        {
            exerciseApp();
        }
    }

    /**
     * Whether or not tests are enabled.
     *
     * @return a boolean indicating whether or not tests are enabled.
     */
    protected boolean isEnableIntegrationTest()
    {
        return !parsedSkip && !mavenTestSkip && !mavenSkipTests;
    }

    /**
     * Whether or not test failures should be ignored.
     *
     * @return a boolean indicating whether or not test failures should be ignored.
     */
    protected boolean isIgnoreTestFailures()
    {
        return mavenIgnoreTestFailure || mavenTestFailureIgnore;
    }

    /**
     * Actually plays tests.
     *
     * @throws MojoExecutionException
     *             if exercising app threw an exception and isIgnoreTestFailures is false..
     * @throws MojoFailureException
     *             if exercising app failed and isIgnoreTestFailures is false.
     */
    protected void exerciseApp() throws MojoExecutionException, MojoFailureException
    {

        getLog().debug( "Parsed values for Android Monkey invocation: " );
        getLog().debug( "seed:" + parsedSeed );

        DeviceCallback instrumentationTestExecutor = new DeviceCallback()
        {
            @Override
            public void doWithDevice( final IDevice device ) throws MojoExecutionException, MojoFailureException
            {
                String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );

                MonkeyTestRunner monkeyTestRunner = new MonkeyTestRunner( parsedEventCount, device );

                monkeyTestRunner.setRunName( "ui monkey tests" );
                if ( parsedSeed != null )
                {
                    monkeyTestRunner.setSeed( parsedSeed );
                }
                if ( parsedPercentTouch != null )
                {
                    monkeyTestRunner.setPercentTouch( parsedPercentTouch );
                }
                if ( parsedPercentMotion != null )
                {
                    monkeyTestRunner.setPercentTouch( parsedPercentMotion );
                }
                if ( parsedPercentTrackball != null )
                {
                    monkeyTestRunner.setPercentTrackball( parsedPercentTrackball );
                }
                if ( parsedPercentNav != null )
                {
                    monkeyTestRunner.setPercentNav( parsedPercentNav );
                }
                if ( parsedPercentMajorNav != null )
                {
                    monkeyTestRunner.setPercentMajorNav( parsedPercentMajorNav );
                }
                if ( parsedPercentSyskeys != null )
                {
                    monkeyTestRunner.setPercentSyskeys( parsedPercentSyskeys );
                }
                if ( parsedPercentAppswitch != null )
                {
                    monkeyTestRunner.setPercentAppswitch( parsedPercentAppswitch );
                }
                if ( parsedPercentAnyevent != null )
                {
                    monkeyTestRunner.setPercentAnyEvent( parsedPercentAnyevent );
                }
                if ( parsedPackages != null )
                {
                    monkeyTestRunner.setPackages( parsedPackages );
                }
                if ( parsedCategories != null )
                {
                    monkeyTestRunner.setCategories( parsedCategories );
                }
                monkeyTestRunner.setDebugNoEvents( parsedDebugNoEvents );
                monkeyTestRunner.setHprof( parsedHprof );
                monkeyTestRunner.setIgnoreCrashes( parsedIgnoreCrashes );
                monkeyTestRunner.setIgnoreTimeouts( parsedIgnoreTimeouts );
                monkeyTestRunner.setIgnoreSecurityExceptions( parsedIgnoreSecurityExceptions );
                monkeyTestRunner.setKillProcessAfterError( parsedKillProcessAfterError );
                monkeyTestRunner.setMonitorNativeCrash( parsedMonitorNativeCrashes );

                getLog().info( deviceLogLinePrefix + "Running ui monkey tests" );
                try
                {
                    AndroidTestRunListener testRunListener = new AndroidTestRunListener( device, getLog(),
                            parsedCreateReport, false, "", "", targetDirectory );
                    monkeyTestRunner.run( testRunListener );
                    if ( testRunListener.hasFailuresOrErrors() && !isIgnoreTestFailures() )
                    {
                        throw new MojoFailureException( deviceLogLinePrefix + "Tests failed on device." );
                    }
                    if ( testRunListener.testRunFailed() )
                    {
                        throw new MojoFailureException( deviceLogLinePrefix + "Test run failed to complete: "
                                + testRunListener.getTestRunFailureCause() );
                    }
                    if ( testRunListener.threwException() && !isIgnoreTestFailures() )
                    {
                        throw new MojoFailureException( deviceLogLinePrefix + testRunListener.getExceptionMessages() );
                    }
                }
                catch ( TimeoutException e )
                {
                    throw new MojoExecutionException( deviceLogLinePrefix + "timeout", e );
                }
                catch ( AdbCommandRejectedException e )
                {
                    throw new MojoExecutionException( deviceLogLinePrefix + "adb command rejected", e );
                }
                catch ( ShellCommandUnresponsiveException e )
                {
                    throw new MojoExecutionException( deviceLogLinePrefix + "shell command " + "unresponsive", e );
                }
                catch ( IOException e )
                {
                    throw new MojoExecutionException( deviceLogLinePrefix + "IO problem", e );
                }
            }
        };

        doWithDevices( instrumentationTestExecutor );
    }

    /**
     * @return default seed.
     */
    // used via PullParameter annotation - do not remove
    private Long getSeed()
    {
        return parsedSeed;
    }

    /**
     * @return default throttle.
     */
    // used via PullParameter annotation - do not remove
    private Long getThrottle()
    {
        return parsedThrottle;
    }

    /**
     * @return default percentTouch.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentTouch()
    {
        return parsedPercentTouch;
    }

    /**
     * @return default percentMotion.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentMotion()
    {
        return parsedPercentMotion;
    }

    /**
     * @return default percentTrackball.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentTrackball()
    {
        return parsedPercentTrackball;
    }

    /**
     * @return default percentNav.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentNav()
    {
        return parsedPercentNav;
    }

    /**
     * @return default percentMajorNav.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentMajorNav()
    {
        return parsedPercentMajorNav;
    }

    /**
     * @return default percentSyskeys.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentSyskeys()
    {
        return parsedPercentSyskeys;
    }

    /**
     * @return default percentAppSwitch.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentAppswitch()
    {
        return parsedPercentAppswitch;
    }

    /**
     * @return default percentAnyEvent.
     */
    // used via PullParameter annotation - do not remove
    private Integer getPercentAnyevent()
    {
        return parsedPercentAnyevent;
    }

    /**
     * @return default packages.
     */
    public String[] getPackages()
    {
        return parsedPackages;
    }

    /**
     * @return default categories.
     */
    public String[] getCategories()
    {
        return parsedCategories;
    }
}