/*
 * Copyright (C) 2009-2011 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;

import com.android.builder.core.DefaultManifestParser;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.ArtifactResolverHelper;
import com.simpligility.maven.plugins.android.common.DependencyResolver;
import com.simpligility.maven.plugins.android.common.DeviceHelper;
import com.simpligility.maven.plugins.android.common.MavenToPlexusLogAdapter;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.common.UnpackedLibHelper;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.configuration.Ndk;
import com.simpligility.maven.plugins.android.configuration.Sdk;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathNotFoundException;
import org.apache.commons.jxpath.xml.DocumentContainer;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
import com.simpligility.maven.plugins.android.configuration.Jack;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Contains common fields and methods for android mojos.
 *
 * @author hugo.josefson@jayway.com
 * @author Manfred Moser - manfred@simpligility.com
 * @author William Ferguson - william.ferguson@xandar.com.au
 * @author Malachi de AElfweald malachid@gmail.com
 * @author Roy Clarkson - rclarkson@gopivotal.com
 */
public abstract class AbstractAndroidMojo extends AbstractMojo
{

    public static final List<String> SUPPORTED_PACKAGING_TYPES = new ArrayList<String>();

    static
    {
        SUPPORTED_PACKAGING_TYPES.add( AndroidExtension.APK );
    }

    /**
     * Android Debug Bridge initialization timeout in milliseconds.
     */
    private static final long ADB_TIMEOUT_MS = 60L * 1000;

    /**
     * The ANDROID_NDK_HOME environment variable name.
     */
    public static final String ENV_ANDROID_NDK_HOME = "ANDROID_NDK_HOME";

    /**
     *
The Android NDK to use.

     *
Looks like this:

     *

     * <ndk>
     *     <path>/opt/android-ndk-r4</path>
     * </ndk>
     *

     *
The <path> parameter is optional. The default is the setting of the ANDROID_NDK_HOME
     * environment variable. The parameter can be used to override this setting with a different environment variable
     * like this:

     *

     * <ndk>
     *     <path>${env.ANDROID_NDK_HOME}</path>
     * </ndk>
     *

     *
or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameter
     * -Dandroid.ndk.path.

     */
    @Parameter
    @ConfigPojo( prefix = "ndk" )
    private Ndk ndk;

    /**
     * The maven project.
     */
    @Component
    protected MavenProject project;

    /**
     * The maven session.
     */
    @Component
    protected MavenSession session;

    /**
     */
    @Component
    protected MojoExecution execution;

    /**
     * The java sources directory.
     */
    @Parameter( defaultValue = "${project.build.sourceDirectory}", readonly = true )
    protected File sourceDirectory;

    /**
     * The project build directory. Ie target.
     */
    @Parameter( defaultValue = "${project.build.directory}", readonly = true )
    protected File targetDirectory;

    /**
     * The output directory. Ie target/classes.
     */
    @Parameter( defaultValue = "${project.build.outputDirectory}", readonly = true )
    protected File projectOutputDirectory;

    /**
     * The project resources. By default a list containing src/main/resources.
     */
    @Parameter( defaultValue = "${project.build.resources}", readonly = true )
    protected List<Resource> resources;

    /**
     * The final name of the artifact.
     */
    @Parameter( defaultValue = "${project.build.finalName}", readonly = true )
    protected String finalName;

    /**
     * The Android resources (src/main/res) directory. Note that this is different from the
     * Maven/Java resources directory (src/main/resources) and should not be set to be the same
     * since different processing is carried out on these folder by different plugins and tools.
     */
    @Parameter( defaultValue = "${project.basedir}/src/main/res" )
    protected File resourceDirectory;

    /**
     * The project source encoding. It will use the platform default encoding if the property is not set.
     */
    @Parameter( defaultValue = "${project.build.sourceEncoding}", readonly = true )
    protected String sourceEncoding;

    /**
     * Override default generated folder containing R.java
     */
    @Parameter( property = "android.genDirectory", defaultValue = "${project.build.directory}/generated-sources/r" )
    protected File genDirectory;

    /**
     *
Root folder containing native libraries to include in the application package.

     */
    @Parameter( property = "android.nativeLibrariesDirectory", defaultValue = "${project.basedir}/src/main/libs" )
    protected File nativeLibrariesDirectory;

    /**
     * Folder in which the ndk libraries are collected ready for packaging.
     */
    @Parameter( defaultValue = "${project.build.directory}/ndk-libs", readonly = true )
    protected File ndkOutputDirectory;

    /**
     * The android resources overlay directory. This will be overridden
     * by resourceOverlayDirectories if present.
     */
    @Parameter( defaultValue = "${project.basedir}/res-overlay" )
    protected File resourceOverlayDirectory;

    /**
     * The android resources overlay directories. If this is specified,
     * the {@link #resourceOverlayDirectory} parameter will be ignored.
     */
    @Parameter
    protected File[] resourceOverlayDirectories;

    /**
     * The android assets directory.
     */
    @Parameter( defaultValue = "${project.basedir}/src/main/assets" )
    protected File assetsDirectory;

    /**
     * The AndroidManifest.xml file.
     */
    @Parameter( property = "android.manifestFile", defaultValue = "${project.basedir}/src/main/AndroidManifest.xml" )
    protected File androidManifestFile;

    /**
     * Path to which to save the result of updating/merging/processing the source AndroidManifest.xml
     * file ({@link androidManifestFile}).
     */
    @Parameter( property = "destination.manifestFile", defaultValue = "${project.build.directory}/AndroidManifest.xml" )
    protected File destinationManifestFile;

    /**
     *
A possibly new package name for the application. This value will be passed on to the aapt
     * parameter --rename-manifest-package. Look to aapt for more help on this.

     */
    @Parameter( property = "android.renameManifestPackage" )
    protected String renameManifestPackage;

    @Parameter( defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies", readonly = true )
    protected File extractedDependenciesDirectory;

    @Parameter(
            defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies/src/main/java",
            readonly = true
    )
    protected File extractedDependenciesJavaSources;

    @Parameter(
            defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies/src/main/resources",
            readonly = true
    )
    protected File extractedDependenciesJavaResources;

    /**
     * The combined assets directory. This will contain both the assets found in "assets" as well as any assets
     * contained in a apksources, apklib or aar dependencies.
     */
    @Parameter( defaultValue = "${project.build.directory}/generated-sources/combined-assets", readonly = true )
    protected File combinedAssets;

    /**
     *
Include jars stored in the libs folder of an apklib as dependencies.
     * Do not delete or change name as it is used in the LifeCycleParticipant.

     *
     * @see ClasspathModifierLifecycleParticipant
     */
    @Parameter( defaultValue = "false" )
    private boolean includeLibsJarsFromApklib;

    /**
     *
Include jars stored in the libs folder of an aar as dependencies.
     * Do not delete or change name as it is used in the LifeCycleParticipant.

     *
     * @see ClasspathModifierLifecycleParticipant
     */
    @Parameter( defaultValue = "true" )
    private boolean includeLibsJarsFromAar;

    /**
     * Specifies which the serial number of the device to connect to. Using the special values "usb" or
     * "emulator" is also valid. "usb" will connect to all actual devices connected (via usb). "emulator" will
     * connect to all emulators connected. Multiple devices will be iterated over in terms of goals to run. All
     * device interaction goals support this so you can e.. deploy the apk to all attached emulators and devices.
     * Goals supporting this are devices, deploy, undeploy, redeploy, pull, push and instrument.
     */
    @Parameter( property = "android.device" )
    protected String device;

    /**
     *
Specifies a list of serial numbers of each device you want to connect to. Using the special values "usb" or
     * "emulator" is also valid. "usb" will connect to all actual devices connected (via usb). "emulator" will
     * connect to all emulators connected. Multiple devices will be iterated over in terms of goals to run. All
     * device interaction goals support this so you can e.. deploy the apk to all attached emulators and devices.
     * Goals supporting this are devices, deploy, undeploy, redeploy, pull, push and instrument.

     *

     * <devices>
     *     <device>usb</device>
     *     <device>emulator-5554</device>
     * </devices>
     *

     *
This parameter can also be configured from command-line with
     * parameter -Dandroid.devices=usb,emulator.

     */
    @Parameter( property = "android.devices" )
    protected String[] devices;

    /**
     *
Specifies the number of threads to use for deploying and testing on attached devices.
     *
     *
This parameter can also be configured from command-line with
     * parameter -Dandroid.deviceThreads=2.

     */
    @Parameter( property = "android.deviceThreads" )
    protected int deviceThreads;

    /**
     *
External IP addresses. The connect goal of the android maven plugin  will execute an adb connect on
     * each IP address. If you have external dervice, you should call this connect goal before any other goal :
     * mvn clean android:connect install.

     *
The Maven plugin will automatically add all these IP addresses into the the devices parameter.
     * If you want to disconnect the IP addresses after the build, you can call the disconnect goal :
     * mvn clean android:connect install android:disconnect

     *
     *

     * <ips>
     *     <ip>127.0.0.1:5556</ip>
     * </ips>
     *

     */
    @Parameter( property = "android.ips" )
    protected String[] ips;

    /**
     * A selection of configurations to be included in the APK as a comma separated list. This will limit the
     * configurations for a certain type. For example, specifying hdpi will exclude all resource folders
     * with the mdpi or ldpi modifiers, but won't affect language or orientation modifiers.
     * For more information about this option, look in the aapt command line help.
     */
    @Parameter( property = "android.configurations" )
    protected String configurations;

    /**
     * A list of extra arguments that must be passed to aapt.
     */
    @Parameter( property = "android.aaptExtraArgs" )
    protected String[] aaptExtraArgs;

    /**
     * Activate verbose output for the aapt execution in Maven debug mode. Defaults to "false"
     */
    @Parameter( property = "android.aaptVerbose" )
    protected boolean aaptVerbose;

    /**
     * Automatically create a ProGuard configuration file that will guard Activity classes and the like that are
     * defined in the AndroidManifest.xml. This files is then automatically used in the proguard mojo execution,
     * if enabled.
     */
    @Parameter( property = "android.proguardFile" )
    protected File proguardFile;

    /**
     * Decides whether the Apk should be generated or not. If set to false, dx and apkBuilder will not run. This is
     * probably most useful for a project used to generate apk sources to be inherited into another application
     * project.
     */
    @Parameter( property = "android.generateApk", defaultValue = "true" )
    protected boolean generateApk;

    @Component
    private ArtifactResolver artifactResolver;

    @Component
    private ArtifactHandler artifactHandler;

    /**
     * Generates R.java into a different package.
     */
    @Parameter( property = "android.customPackage" )
    protected String customPackage;

    /**
     * Maven ProjectHelper.
     */
    @Component
    protected MavenProjectHelper projectHelper;

    /**
     *
The Android SDK to use.

     *
Looks like this:

     *

     * <sdk>
     *     <path>/opt/android-sdk-linux</path>
     *     <platform>2.1</platform>
     * </sdk>
     *

     *
The <platform> parameter is optional, and corresponds to the
     * platforms/android-* directories in the Android SDK directory. Default is the latest available
     * version, so you only need to set it if you for example want to use platform 1.5 but also have e.g. 2.2 installed.
     * Has no effect when used on an Android SDK 1.1. The parameter can also be coded as the API level. Therefore valid
     * values are 1.1, 1.5, 1.6, 2.0, 2.01, 2.1, 2.2 and so as well as 3, 4, 5, 6, 7, 8... 19. If a platform/api level
     * is not installed on the machine an error message will be produced.

     *
The <path> parameter is optional. The default is the setting of the ANDROID_HOME
     * environment variable. The parameter can be used to override this setting with a different environment variable
     * like this:

     *

     * <sdk>
     *     <path>${env.ANDROID_SDK}</path>
     * </sdk>
     *

     *
or just with a hard-coded absolute path. The parameters can also be configured from command-line with
     * parameters -Dandroid.sdk.path and -Dandroid.sdk.platform.

     */
    @Parameter
    private Sdk sdk;

    /**
     *
Parameter designed to pick up -Dandroid.sdk.path in case there is no pom with an
     * <sdk> configuration tag.

     *
Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sdk#path}.

     */
    @Parameter( property = "android.sdk.path", readonly = true )
    private File sdkPath;

    /**
     *
Parameter designed to pick up environment variable ANDROID_HOME in case
     * android.sdk.path is not configured.

     */
    @Parameter( defaultValue = "${env.ANDROID_HOME}", readonly = true )
    private String envAndroidHome;

    /**
     * The ANDROID_HOME environment variable name.
     */
    public static final String ENV_ANDROID_HOME = "ANDROID_HOME";

    /**
     *
Parameter designed to pick up -Dandroid.sdk.platform in case there is no pom with an
     * <sdk> configuration tag.

     *
Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sdk#platform}.

     */
    @Parameter( property = "android.sdk.platform", readonly = true )
    private String sdkPlatform;

    /**
     *
Whether to undeploy an apk from the device before deploying it.

     *
     *
Only has effect when running mvn android:deploy in an Android application project manually, or
     * when running mvn integration-test (or mvn install) in a project with instrumentation
     * tests.
     *

     *
     *
It is useful to keep this set to true at all times, because if an apk with the same package was
     * previously signed with a different keystore, and deployed to the device, deployment will fail because your
     * keystore is different.

     */
    @Parameter( property = "android.undeployBeforeDeploy", defaultValue = "false" )
    protected boolean undeployBeforeDeploy;

    /**
     *
Whether to attach the normal .jar file to the build, so it can be depended on by for example integration-tests
     * which may then access {@code R.java} from this project.

     *
Only disable it if you know you won't need it for any integration-tests. Otherwise, leave it enabled.

     */
    @Parameter( property = "android.attachJar", defaultValue = "true" )
    protected boolean attachJar;

    /**
     *
Whether to attach sources to the build, which can be depended on by other {@code apk} projects, for including
     * them in their builds.

     *
Enabling this setting is only required if this project's source code and/or res(ources) will be included in
     * other projects, using the Maven <dependency> tag.

     */
    @Parameter( property = "android.attachSources", defaultValue = "false" )
    protected boolean attachSources;

    /**
     *
Parameter designed to pick up -Dandroid.ndk.path in case there is no pom with an
     * <ndk> configuration tag.

     *
Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Ndk#path}.

     */
    @Parameter( property = "android.ndk.path", readonly = true )
    private File ndkPath;

    /**
     * Whether to create a release build (default is false / debug build). This affect BuildConfig generation
     * and apk generation at this stage, but should probably affect other aspects of the build.
     */
    @Parameter( property = "android.release", defaultValue = "false" )
    protected boolean release;

    /**
     * The timeout value for an adb connection in milliseconds.
     */
    @Parameter( property = "android.adb.connectionTimeout", defaultValue = "5000" )
    protected int adbConnectionTimeout;

    /**
     * Folder in which AAR library dependencies will be unpacked.
     */
    @Parameter( property = "unpackedLibsFolder", defaultValue = "${project.build.directory}/unpacked-libs" )
    private File unpackedLibsFolder;

    /**
     * Whether the plugin should show a warning if conflicting dependencies with the Android provided ones exist.
     *
     * @see ClasspathModifierLifecycleParticipant
     */
    @Parameter( defaultValue = "false" )
    private File disableConflictingDependenciesWarning;

    /**
    * configure the Jack compiler
    */
    @Parameter
    private Jack jack;

    private UnpackedLibHelper unpackedLibHelper;
    private ArtifactResolverHelper artifactResolverHelper;
    private NativeHelper nativeHelper;

    /**
     *
     */
    private static final Object ADB_LOCK = new Object();

    /**
     *
     */
    private static boolean adbInitialized = false;

    /**
     * Dependency graph builder component.
     */
    @Component( hint = "default" )
    protected DependencyGraphBuilder dependencyGraphBuilder;

    protected final DependencyResolver getDependencyResolver()
    {
        return new DependencyResolver( new MavenToPlexusLogAdapter( getLog() ), dependencyGraphBuilder );
    }

    /**
     * @return a {@code Set} of dependencies which may be extracted and otherwise included in other artifacts. Never
     *         {@code null}. This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} scopes.
     */
    protected Set<Artifact> getRelevantCompileArtifacts()
    {
        final List<Artifact> allArtifacts = project.getCompileArtifacts();
        return getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );
    }

    /**
     * @return a {@code Set} of direct project dependencies. Never {@code null}. This excludes artifacts of the {@code
     *         EXCLUDED_DEPENDENCY_SCOPES} scopes.
     */
    protected Set<Artifact> getDirectDependencyArtifacts()
    {
        final Set<Artifact> allArtifacts = project.getDependencyArtifacts();
        return getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );
    }

    /**
     * Provides transitive dependency artifacts having types defined by {@code types} argument
     * or all types if {@code types} argument is empty
     *
     * @param types artifact types to be selected
     * @return a {@code List} of all project dependencies. Never {@code null}.
     *         This excludes artifacts of the {@link ArtifactResolverHelper.EXCLUDE_NON_PACKAGED_SCOPES} scopes.
     *         This should maintain dependency order to comply with library project resource precedence.
     */
    protected Set<Artifact> getTransitiveDependencyArtifacts( String... types )
    {
        return getArtifactResolverHelper().getFilteredArtifacts( project.getArtifacts(), types );
    }

    /**
     * Provides transitive dependency artifacts only defined types based on {@code types} argument
     * or all types if {@code types} argument is empty
     *
     * @param filteredScopes    List of scopes to be removed (ie filtered out).
     * @param types             Zero or more artifact types to be selected.
     * @return a {@code List} of all project dependencies. Never {@code null}.
     *         This should maintain dependency order to comply with library project resource precedence.
     */
    protected Set<Artifact> getTransitiveDependencyArtifacts( List<String> filteredScopes, String... types )
    {
        return getArtifactResolverHelper().getFilteredArtifacts( filteredScopes, project.getArtifacts(), types );
    }

    /**
     * Attempts to resolve an {@link Artifact} to a {@link File}.
     *
     * @param artifact to resolve
     * @return a {@link File} to the resolved artifact, never null.
     * @throws MojoExecutionException if the artifact could not be resolved.
     */
    protected File resolveArtifactToFile( Artifact artifact ) throws MojoExecutionException
    {
        return getArtifactResolverHelper().resolveArtifactToFile( artifact );
    }

    /**
     * Initialize the Android Debug Bridge and wait for it to start. Does not reinitialize it if it has
     * already been initialized (that would through and IllegalStateException...). Synchronized sine
     * the init call in the library is also synchronized .. just in case.
     */
    protected AndroidDebugBridge initAndroidDebugBridge() throws MojoExecutionException
    {
        synchronized ( ADB_LOCK )
        {
            if ( ! adbInitialized )
            {
                DdmPreferences.setTimeOut( adbConnectionTimeout );
                AndroidDebugBridge.init( false );
                adbInitialized = true;
            }
            AndroidDebugBridge androidDebugBridge = AndroidDebugBridge
                    .createBridge( getAndroidSdk().getAdbPath(), false );
            waitUntilConnected( androidDebugBridge );
            return androidDebugBridge;
        }
    }

    /**
     * Run a wait loop until adb is connected or trials run out. This method seems to work more reliably then using a
     * listener.
     */
    private void waitUntilConnected( AndroidDebugBridge adb )
    {
        int trials = 10;
        final int connectionWaitTime = 50;
        while ( trials > 0 )
        {
            try
            {
                Thread.sleep( connectionWaitTime );
            }
            catch ( InterruptedException e )
            {
                e.printStackTrace();
            }
            if ( adb.isConnected() )
            {
                break;
            }
            trials--;
        }
    }

    /**
     * Wait for the Android Debug Bridge to return an initial device list.
     */
    protected void waitForInitialDeviceList( final AndroidDebugBridge androidDebugBridge ) throws MojoExecutionException
    {
        if ( ! androidDebugBridge.hasInitialDeviceList() )
        {
            getLog().info( "Waiting for initial device list from the Android Debug Bridge" );
            long limitTime = System.currentTimeMillis() + ADB_TIMEOUT_MS;
            while ( ! androidDebugBridge.hasInitialDeviceList() && ( System.currentTimeMillis() < limitTime ) )
            {
                try
                {
                    Thread.sleep( 1000 );
                }
                catch ( InterruptedException e )
                {
                    throw new MojoExecutionException(
                            "Interrupted waiting for initial device list from Android Debug Bridge" );
                }
            }
            if ( ! androidDebugBridge.hasInitialDeviceList() )
            {
                getLog().error( "Did not receive initial device list from the Android Debug Bridge." );
            }
        }
    }

    /**
     * Deploys an apk file to a connected emulator or usb device.
     *
     * @param apkFile the file to deploy
     * @throws MojoExecutionException If there is a problem deploying the apk file.
     */
    protected void deployApk( final File apkFile ) throws MojoExecutionException, MojoFailureException
    {
        if ( undeployBeforeDeploy )
        {
            undeployApk( apkFile );
        }
        doWithDevices( new DeviceCallback()
        {
            public void doWithDevice( final IDevice device ) throws MojoExecutionException
            {
                String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
                try
                {
                    device.installPackage( apkFile.getAbsolutePath(), true );
                    getLog().info( deviceLogLinePrefix + "Successfully installed " + apkFile.getAbsolutePath() );
                    getLog().debug( " to " + DeviceHelper.getDescriptiveName( device ) );
                }
                catch ( InstallException e )
                {
                    throw new MojoExecutionException( deviceLogLinePrefix + "Install of " + apkFile.getAbsolutePath()
                            + " failed.", e );
                }
            }
        } );
    }

    /**
     *
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    protected void deployDependencies() throws MojoExecutionException, MojoFailureException
    {
        Set<Artifact> directDependentArtifacts = project.getDependencyArtifacts();
        if ( directDependentArtifacts != null )
        {
            for ( Artifact artifact : directDependentArtifacts )
            {
                String type = artifact.getType();
                if ( type.equals( APK ) )
                {
                    getLog().debug( "Detected apk dependency " + artifact + ". Will resolve and deploy to device..." );
                    final File targetApkFile = resolveArtifactToFile( artifact );
                    getLog().debug( "Deploying " + targetApkFile + " to device..." );
                    deployApk( targetApkFile );
                }
            }
        }
    }

    /**
     * Deploy the apk built with the current projects to all attached devices and emulators.
     * Skips other projects in a multi-module build without terminating.
     *
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    protected void deployBuiltApk() throws MojoExecutionException, MojoFailureException
    {
        if ( project.getPackaging().equals( APK ) )
        {
            File apkFile = new File( targetDirectory, finalName + "." + APK );
            deployApk( apkFile );
        }
        else
        {
            getLog().info( "Project packaging is not apk, skipping deployment." );
        }
    }

    /**
     * Performs the callback action on the devices determined by
     * {@link #shouldDoWithThisDevice(com.android.ddmlib.IDevice)}
     *
     * @param deviceCallback the action to perform on each device
     * @throws org.apache.maven.plugin.MojoExecutionException
     *          in case there is a problem
     * @throws org.apache.maven.plugin.MojoFailureException
     *          in case there is a problem
     */
    protected void doWithDevices( final DeviceCallback deviceCallback )
            throws MojoExecutionException, MojoFailureException
    {
        final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();

        if ( !androidDebugBridge.isConnected() )
        {
            throw new MojoExecutionException( "Android Debug Bridge is not connected." );
        }

        waitForInitialDeviceList( androidDebugBridge );
        List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
        int numberOfDevices = devices.size();
        getLog().debug( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
        if ( devices.size() == 0 )
        {
            throw new MojoExecutionException( "No online devices attached." );
        }

        int threadCount = getDeviceThreads();
        if ( getDeviceThreads() == 0 )
        {
            getLog().info( "android.devicesThreads parameter not set, using a thread for each attached device" );
            threadCount = numberOfDevices;
        }
        else
        {
            getLog().info( "android.devicesThreads parameter set to " + getDeviceThreads() );
        }

        boolean shouldRunOnAllDevices = getDevices().size() == 0;
        if ( shouldRunOnAllDevices )
        {
            getLog().info( "android.devices parameter not set, using all attached devices" );
        }
        else
        {
            getLog().info( "android.devices parameter set to " + getDevices().toString() );
        }

        ArrayList<DoThread> doThreads = new ArrayList<DoThread>();
        ExecutorService executor = Executors.newFixedThreadPool( threadCount );
        for ( final IDevice idevice : devices )
        {
            if ( shouldRunOnAllDevices )
            {
                String deviceType = idevice.isEmulator() ? "Emulator " : "Device ";
                getLog().info( deviceType + DeviceHelper.getDescriptiveName( idevice ) + " found." );
            }
            if ( shouldRunOnAllDevices || shouldDoWithThisDevice( idevice ) )
            {
                DoThread deviceDoThread = new DoThread() {
                    public void runDo() throws MojoFailureException, MojoExecutionException
                    {
                        deviceCallback.doWithDevice( idevice );
                    }
                };
                doThreads.add( deviceDoThread );
                executor.execute( deviceDoThread );
            }
        }
        executor.shutdown();
        while ( ! executor.isTerminated() )
        {
            // waiting for threads finish
        }
        throwAnyDoThreadErrors( doThreads );

        if ( ! shouldRunOnAllDevices && doThreads.isEmpty() )
        {
            throw new MojoExecutionException( "No device found for android.device=" + getDevices().toString() );
        }
    }

    private void throwAnyDoThreadErrors( ArrayList<DoThread> doThreads ) throws MojoExecutionException,
            MojoFailureException
    {
        for ( DoThread deviceDoThread : doThreads )
        {
            if ( deviceDoThread.failure != null )
            {
                throw deviceDoThread.failure;
            }
            if ( deviceDoThread.execution != null )
            {
                throw deviceDoThread.execution;
            }
        }
    }

    /**
     * Determines if this {@link IDevice}(s) should be used
     *
     * @param idevice the device to check
     * @return if the device should be used
     * @throws org.apache.maven.plugin.MojoExecutionException
     *          in case there is a problem
     * @throws org.apache.maven.plugin.MojoFailureException
     *          in case there is a problem
     */
    private boolean shouldDoWithThisDevice( IDevice idevice ) throws MojoExecutionException, MojoFailureException
    {

        for ( String device : getDevices() )
        {
            // use specified device or all emulators or all devices
            if ( "emulator".equals( device ) && idevice.isEmulator() )
            {
                return true;
            }

            if ( "usb".equals( device ) && ! idevice.isEmulator() )
            {
                return true;
            }

            if ( idevice.isEmulator() && ( device.equalsIgnoreCase( idevice.getAvdName() ) || device
                    .equalsIgnoreCase( idevice.getSerialNumber() ) ) )
            {
                return true;
            }

            if ( ! idevice.isEmulator() && device.equals( idevice.getSerialNumber() ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Undeploys an apk from a connected emulator or usb device. Also deletes the application's data and cache
     * directories on the device.
     *
     * @param apkFile the file to undeploy
     * @return true if successfully undeployed, false otherwise.
     */
    protected boolean undeployApk( File apkFile ) throws MojoExecutionException, MojoFailureException
    {
        final String packageName;
        packageName = extractPackageNameFromApk( apkFile );
        return undeployApk( packageName );
    }

    /**
     * Undeploys an apk, specified by package name, from a connected emulator
     * or usb device. Also deletes the application's data and cache
     * directories on the device.
     *
     * @param packageName the package name to undeploy.
     * @return true if successfully undeployed, false otherwise.
     */
    protected boolean undeployApk( final String packageName ) throws MojoExecutionException, MojoFailureException
    {

        final AtomicBoolean result = new AtomicBoolean( true ); // if no devices are present, it counts as successful

        doWithDevices( new DeviceCallback()
        {
            public void doWithDevice( final IDevice device ) throws MojoExecutionException
            {
                String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
                try
                {
                    device.uninstallPackage( packageName );
                    getLog().info( deviceLogLinePrefix + "Successfully uninstalled " + packageName );
                    getLog().debug( " from " + DeviceHelper.getDescriptiveName( device ) );
                    result.set( true );
                }
                catch ( InstallException e )
                {
                    result.set( false );
                    throw new MojoExecutionException( deviceLogLinePrefix + "Uninstall of " + packageName
                            + " failed.", e );
                }
            }
        } );

        return result.get();
    }

    /**
     * Extracts the package name from an apk file.
     *
     * @param apkFile apk file to extract package name from.
     * @return the package name from inside the apk file.
     */
    protected String extractPackageNameFromApk( File apkFile ) throws MojoExecutionException
    {
        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( this.getLog() );
        executor.setCaptureStdOut( true );
        executor.setCaptureStdErr( true );

        AaptCommandBuilder commandBuilder = AaptCommandBuilder
                .dump( getLog() )
                .xmlTree()
                .setPathToApk( apkFile.getAbsolutePath() )
                .addAssetFile( "AndroidManifest.xml" );

        getLog().info( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        try
        {
            executor.executeCommand( getAndroidSdk().getAaptPath(), commandBuilder.build(), false );
            final String xmlTree = executor.getStandardOut();
            return extractPackageNameFromAndroidManifestXmlTree( xmlTree );
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException(
                    "Error while trying to figure out package name from inside apk file " + apkFile );
        }
        finally
        {
            String errout = executor.getStandardError();
            if ( ( errout != null ) && ( errout.trim().length() > 0 ) )
            {
                getLog().error( errout );
            }
        }
    }

    /**
     * Extracts the package name from an XmlTree dump of AndroidManifest.xml by the aapt tool.
     *
     * @param aaptDumpXmlTree output from aapt dump xmltree <apkFile> AndroidManifest.xml
     * @return the package name from inside the apkFile.
     */
    protected String extractPackageNameFromAndroidManifestXmlTree( String aaptDumpXmlTree )
    {
        final Scanner scanner = new Scanner( aaptDumpXmlTree );
        // Finds the root element named "manifest".
        scanner.findWithinHorizon( "^E: manifest", 0 );
        // Finds the manifest element's attribute named "package".
        scanner.findWithinHorizon( "  A: package=\"", 0 );
        // Extracts the package value including the trailing double quote.
        String packageName = scanner.next( ".*?\"" );
        // Removes the double quote.
        packageName = packageName.substring( 0, packageName.length() - 1 );
        return packageName;
    }

    /**
     * Provides package name for android artifact.
     *
     * @param artifact android artifact which package have to be extracted
     * @return package name
     * @throws MojoExecutionException if there is no AndroidManifest.xml for provided artifact
     *      or appears error while parsing in {@link #extractPackageNameFromAndroidManifest(File)}
     *
     * @see #extractPackageNameFromAndroidManifest(File)
     */
    protected String extractPackageNameFromAndroidArtifact( Artifact artifact ) throws MojoExecutionException
    {
        final File unpackedLibFolder = getUnpackedLibFolder( artifact );
        final File manifest = new File( unpackedLibFolder, "AndroidManifest.xml" );
        if ( !manifest.exists() )
        {
            throw new MojoExecutionException(
                    "AndroidManifest.xml file wasn't found in next place: " + unpackedLibFolder );
        }
        return extractPackageNameFromAndroidManifest( manifest );
    }

    protected String extractPackageNameFromAndroidManifest( File manifestFile )
    {
        return new DefaultManifestParser( manifestFile ).getPackage();
    }

    /**
     * @return the package name from this project's Android Manifest.
     */
    protected final String getAndroidManifestPackageName()
    {
        return extractPackageNameFromAndroidManifest( destinationManifestFile );
    }

    /**
     * Attempts to find the instrumentation test runner from inside the AndroidManifest.xml file.
     *
     * @param manifestFile the AndroidManifest.xml file to inspect.
     * @return the instrumentation test runner declared in AndroidManifest.xml, or {@code null} if it is not declared.
     * @throws MojoExecutionException
     */
    protected String extractInstrumentationRunnerFromAndroidManifest( File manifestFile )
            throws MojoExecutionException
    {
        final URL xmlURL;
        try
        {
            xmlURL = manifestFile.toURI().toURL();
        }
        catch ( MalformedURLException e )
        {
            throw new MojoExecutionException(
                    "Error while trying to figure out instrumentation runner from inside AndroidManifest.xml file "
                            + manifestFile, e );
        }
        final DocumentContainer documentContainer = new DocumentContainer( xmlURL );
        final Object instrumentationRunner;
        try
        {
            instrumentationRunner = JXPathContext.newContext( documentContainer )
                    .getValue( "manifest//instrumentation/@android:name", String.class );
        }
        catch ( JXPathNotFoundException e )
        {
            return null;
        }
        return ( String ) instrumentationRunner;
    }

    protected final boolean isInstrumentationTest() throws MojoExecutionException
    {
        return extractInstrumentationRunnerFromAndroidManifest( destinationManifestFile ) != null;
    }

    /**
     *
Returns the Android SDK to use.

     *
     *
Current implementation looks for System property android.sdk.path, then
     * <sdk><path> configuration in pom, then environment variable ANDROID_HOME.
     *
     *
This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is
     * based on available parameters. This method should be the only one you should need to look at to understand how
     * the Android SDK is chosen, and from where on disk.

     *
     * @return the Android SDK to use.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *          if no Android SDK path configuration is available at all.
     */
    protected AndroidSdk getAndroidSdk() throws MojoExecutionException
    {
        File chosenSdkPath;
        String chosenSdkPlatform;
        String buildToolsVersion = null;

        if ( sdk != null )
        {
            // An <sdk> tag exists in the pom.
            buildToolsVersion = sdk.getBuildTools();

            if ( sdk.getPath() != null )
            {
                // An <sdk><path> tag is set in the pom.

                chosenSdkPath = sdk.getPath();
            }
            else
            {
                // There is no <sdk><path> tag in the pom.

                if ( sdkPath != null )
                {
                    // -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
                    chosenSdkPath = sdkPath;
                }
                else
                {
                    // No -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
                    chosenSdkPath = new File( getAndroidHomeOrThrow() );
                }
            }

            // Use <sdk><platform> from pom if it's there, otherwise try -Dandroid.sdk.platform from command line or
            // <properties><sdk.platform>...
            if ( ! isBlank( sdk.getPlatform() ) )
            {
                chosenSdkPlatform = sdk.getPlatform();
            }
            else
            {
                chosenSdkPlatform = sdkPlatform;
            }
        }
        else
        {
            // There is no <sdk> tag in the pom.

            if ( sdkPath != null )
            {
                // -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
                chosenSdkPath = sdkPath;
            }
            else
            {
                // No -Dandroid.sdk.path is set on command line, or via <properties><android.sdk.path>...
                chosenSdkPath = new File( getAndroidHomeOrThrow() );
            }

            // Use any -Dandroid.sdk.platform from command line or <properties><sdk.platform>...
            chosenSdkPlatform = sdkPlatform;
        }

        return new AndroidSdk( chosenSdkPath, chosenSdkPlatform, buildToolsVersion );
    }

    protected Jack getJack()
    {
        if ( jack == null )
        {
            return new Jack( super.getPluginContext() );
        }
        else
        {
            return jack;
        }
    }

    private String getAndroidHomeOrThrow() throws MojoExecutionException
    {
        final String androidHome = System.getenv( ENV_ANDROID_HOME );
        if ( isBlank( androidHome ) )
        {
            throw new MojoExecutionException( "No Android SDK path could be found. You may configure it in the "
                    + "plugin configuration section in the pom file using <sdk><path>...</path></sdk> or "
                    + "<properties><android.sdk.path>...</android.sdk.path></properties> or on command-line "
                    + "using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME );
        }
        return androidHome;
    }

    protected final File getUnpackedLibsDirectory()
    {
        return getUnpackedLibHelper().getUnpackedLibsFolder();
    }

    public final File getUnpackedLibFolder( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedLibFolder( artifact );
    }

    protected final File getUnpackedAarClassesJar( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedClassesJar( artifact );
    }

    protected final File getUnpackedApkLibSourceFolder( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedApkLibSourceFolder( artifact );
    }

    protected final File getUnpackedLibResourceFolder( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedLibResourceFolder( artifact );
    }

    protected final File getUnpackedLibAssetsFolder( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedLibAssetsFolder( artifact );
    }

    /**
     * @param artifact  Android dependency that is being referenced.
     * @return Folder where the unpacked native libraries are located.
     */
    public final File getUnpackedLibNativesFolder( Artifact artifact )
    {
        return getUnpackedLibHelper().getUnpackedLibNativesFolder( artifact );
    }

    // TODO Replace this with a non-static method (could even replace it with one of the methods above).
    public static File getLibraryUnpackDirectory( File unpackedApkLibsDirectory, Artifact artifact )
    {
        return new File( unpackedApkLibsDirectory.getAbsolutePath(), artifact.getArtifactId() );
    }

    /**
     *
Returns the Android NDK to use.

     *
     *
Current implementation looks for <ndk><path> configuration in pom, then System
     * property android.ndk.path, then environment variable ANDROID_NDK_HOME.
     *
     *
This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is
     * based on available parameters. This method should be the only one you should need to look at to understand how
     * the Android NDK is chosen, and from where on disk.

     *
     * @return the Android NDK to use.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *          if no Android NDK path configuration is available at all.
     */
    protected AndroidNdk getAndroidNdk() throws MojoExecutionException
    {
        File chosenNdkPath;
        // There is no <ndk> tag in the pom.
        if ( ndkPath != null )
        {
            // -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
            chosenNdkPath = ndkPath;
        }
        else if ( ndk != null && ndk.getPath() != null )
        {
            chosenNdkPath = ndk.getPath();
        }
        else
        {
            // No -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
            chosenNdkPath = new File( getAndroidNdkHomeOrThrow() );
        }
        return new AndroidNdk( chosenNdkPath );
    }

    private String getAndroidNdkHomeOrThrow() throws MojoExecutionException
    {
        final String androidHome = System.getenv( ENV_ANDROID_NDK_HOME );
        if ( isBlank( androidHome ) )
        {
            throw new MojoExecutionException( "No Android NDK path could be found. You may configure it in the pom "
                    + "using <ndk><path>...</path></ndk> or <properties><ndk.path>...</ndk.path></properties> or on "
                    + "command-line using -Dandroid.ndk.path=... or by setting environment variable "
                    + ENV_ANDROID_NDK_HOME );
        }
        return androidHome;
    }

    /**
     * @return the resource directories if defined or the resource directory.
     */
    public File[] getResourceOverlayDirectories()
    {
        File[] overlayDirectories;

        if ( resourceOverlayDirectories == null || resourceOverlayDirectories.length == 0 )
        {
            overlayDirectories = new File[]{ resourceOverlayDirectory };
        }
        else
        {
            overlayDirectories = resourceOverlayDirectories;
        }

        return overlayDirectories;
    }

    private Set<String> getDevices()
    {
        Set<String> list = new HashSet<String>();

        if ( StringUtils.isNotBlank( device ) )
        {
            list.add( device );
        }

        list.addAll( Arrays.asList( devices ) );

        list.addAll( Arrays.asList( ips ) );

        return list;
    }

    private int getDeviceThreads()
    {
        return deviceThreads;
    }

    private abstract class DoThread extends Thread
    {
        private MojoFailureException failure;
        private MojoExecutionException execution;

        public final void run()
        {
            try
            {
                runDo();
            }
            catch ( MojoFailureException e )
            {
                failure = e;
            }
            catch ( MojoExecutionException e )
            {
                execution = e;
            }
        }

        protected abstract void runDo() throws MojoFailureException, MojoExecutionException;
    }

    /**
     * @return True if this project constructs an APK as opposed to an AAR or APKLIB.
     */
    protected final boolean isAPKBuild()
    {
        return getUnpackedLibHelper().isAPKBuild( project );
    }

    /**
     * Copies the files contained within the source folder to the target folder.
     *

     * The the target folder doesn't exist it will be created.
     *

     *
     * @param sourceFolder      Folder from which to copy the resources.
     * @param targetFolder      Folder to which to copy the files.
     * @throws MojoExecutionException if the files cannot be copied.
     */
    protected final void copyFolder( File sourceFolder, File targetFolder ) throws MojoExecutionException
    {
        copyFolder( sourceFolder, targetFolder, TrueFileFilter.TRUE );
    }

    private void copyFolder( File sourceFolder, File targetFolder, FileFilter filter ) throws MojoExecutionException
    {
        if ( !sourceFolder.exists() )
        {
            return;
        }

        try
        {
            getLog().debug( "Copying " + sourceFolder + " to " + targetFolder );
            if ( ! targetFolder.exists() )
            {
                if ( ! targetFolder.mkdirs() )
                {
                    throw new MojoExecutionException( "Could not create target directory " + targetFolder );
                }
            }
            FileUtils.copyDirectory( sourceFolder, targetFolder, filter );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Could not copy source folder to target folder", e );
        }

    }

    protected final UnpackedLibHelper getUnpackedLibHelper()
    {
        if ( unpackedLibHelper == null )
        {
            unpackedLibHelper = new UnpackedLibHelper(
                getArtifactResolverHelper(),
                project,
                new MavenToPlexusLogAdapter( getLog() ),
                unpackedLibsFolder
            );
        }
        return unpackedLibHelper;
    }

    protected final ArtifactResolverHelper getArtifactResolverHelper()
    {
        if ( artifactResolverHelper == null )
        {
            artifactResolverHelper = new ArtifactResolverHelper(
                    artifactResolver,
                    new MavenToPlexusLogAdapter( getLog() ),
                    project.getRemoteArtifactRepositories()
            );
        }
        return artifactResolverHelper;
    }

    protected final NativeHelper getNativeHelper()
    {
        if ( nativeHelper == null )
        {
            nativeHelper = new NativeHelper( project, dependencyGraphBuilder, getLog() );
        }
        return nativeHelper;
    }
}


/*
 * 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.File;
import java.io.IOException;

import org.apache.commons.lang3.StringUtils;
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.UIAutomatorRemoteAndroidTestRunner;
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.ScreenshotServiceWrapper;
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.UIAutomator;

/**
 * Can execute tests using ui uiautomator.

 * Implements parsing parameters from pom or command line arguments and sets useful defaults as well. This goal is meant
 * to execute a special java project dedicated to UI testing via UIAutomator. It will build the jar of the
 * project, dex it and send it to dalvik cache of a rooted device or to an emulator. If you use a rooted device, refer
 * to this thread on stack over flow.

 *

 * The tests are executed via ui automator. A surefire compatible test report can be generated and its location will be
 * logged during build.

 *

 * To use this goal, you will need to place the uiautomator.jar file (part of the Android SDK >= 16) on a nexus
 * repository.

 *

 * A typical usage of this goal can be found at Quality tools for Android project.
 *
 * @see Android UI testing doc
 * @see UI Automator manual page
 * @author Stéphane Nicolas - snicolas@octo.com
 */
@SuppressWarnings( "unused" )
@Mojo( name = "uiautomator", requiresProject = false )
public class UIAutomatorMojo 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. Builds will still fail if tests can't complete or throw an exception.
     */
    @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. Builds will still fail if tests can't complete or throw an exception.
     */
    @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
     *
     *

     * <uiautomator>
     *   <skip>false</skip>
     * </uiautomator>
     *

     *
     * Full configuration can use these parameters.
     *
     *

     * <uiautomator>
     *   <skip>false</skip>
     *   <testClassOrMethods>
     *     <testClassOrMethod>com.foo.SampleTest</testClassOrMethod>
     *     <testClassOrMethod>com.bar.CalculatorTest#testCalculatorApp</testClassOrMethod>
     *   </testClassOrMethods>
     *   <createReport>true</createReport>
     *   <takeScreenshotOnFailure>true</takeScreenshotOnFailure>
     *   <screenshotsPathOnDevice>/sdcard/uiautomator-screenshots/</screenshotsPathOnDevice>
     *   <propertiesKeyPrefix>UIA</propertiesKeyPrefix>
     * </uiautomator>
     *

     *
     *
     * 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 UIAutomator uiautomator;

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

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

    /**
     * Jar file that will be run during ui uiautomator tests.
     */
    @Parameter( property = "android.uiautomator.jarFile" )
    private String uiautomatorJarFile;

    @PullParameter( defaultValueGetterMethod = "getJarFile" )
    private String parsedJarFile;

    /**
     * Test class or methods to execute during uiautomator tests. Each class or method must be fully qualified with the
     * package name, in one of these formats:
     *

     *
package_name.class_name
     *
package_name.class_name#method_name
     *

     */
    @Parameter( property = "android.uiautomator.testClassOrMethod" )
    private String[] uiautomatorTestClassOrMethods;

    @PullParameter( required = false, defaultValueGetterMethod = "getTestClassOrMethods" )
    private String[] parsedTestClassOrMethods;

    /**
     * Decides whether to run the test to completion on the device even if its parent process is terminated (for
     * example, if the device is disconnected).
     */
    @Parameter( property = "android.uiautomator.noHup" )
    private Boolean uiautomatorNoHup;

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

    /**
     * Decides whether to wait for debugger to connect before starting.
     */
    @Parameter( property = "android.uiautomator.debug" )
    private Boolean uiautomatorDebug = false;

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

    /**
     * Decides whether to use a dump file or not.
     */
    @Parameter( property = "android.uiautomator.useDump" )
    private Boolean uiautomatorUseDump;

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

    /**
     * Generate an XML file with a dump of the current UI hierarchy. If a filepath is not specified, by default, the
     * generated dump file is stored on the device in this location /storage/sdcard0/window_dump.xml.
     */
    @Parameter( property = "android.uiautomator.dumpFilePath" )
    private String uiautomatorDumpFilePath;

    @PullParameter( required = false, defaultValue = "/storage/sdcard0/window_dump.xml" )
    private String parsedDumpFilePath;

    /**
     * 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.
     */
    @Parameter( property = "android.uiautomator.createReport" )
    private Boolean uiautomatorCreateReport;

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

    /**
     * Adds a suffix to the report name. For example if parameter reportSuffix is "-mySpecialReport",
     * the name of the report will be TEST-deviceid-mySpecialReport.xml
     *
     * Defaults to null. Hence, in the default case, the name of the report will be TEST-deviceid.xml.
     */
    @Parameter( property = "android.uiautomator.reportSuffix" )
    private String uiautomatorReportSuffix;

    @PullParameter( required = false, defaultValueGetterMethod = "getReportSuffix" )
    private String parsedReportSuffix;

    /**
     * Decides whether or not to take screenshots when tests execution results in failure or error. Screenshots use the
     * utiliy screencap that is usually available within emulator/devices with SDK >= 16.
     */
    @Parameter( property = "android.uiautomator.takeScreenshotOnFailure" )
    private Boolean uiautomatorTakeScreenshotOnFailure;

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

    /**
     * Location of the screenshots on device. This value is only taken into account if takeScreenshotOnFailure = true.
     * If a filepath is not specified, by default, the screenshots will be located at /sdcard/uiautomator-screenshots/.
     */
    @Parameter( property = "android.uiautomator.screenshotsPathOnDevice" )
    private String uiautomatorScreenshotsPathOnDevice;

    @PullParameter( required = false, defaultValue = "/sdcard/uiautomator-screenshots/" )
    private String parsedScreenshotsPathOnDevice;

    /**
     *
Specifies a prefix for custom user properties that will be sent
     * through to UIAutomator with the "-e key value" parameter.

     *
     *
If any user property is needed in a test case, this is the way to send it through.
     * User credentials for example.

     *
     *
If no prefix value is specified no user property will be sent.

     *
     *
Usage example:

     *
<propertiesKeyPrefix>UIA</propertiesKeyPrefix>

     *
And run it with:

     *
> mvn <goal> "-DUIAkey=value"

     *
would become "-e key value" as it would be runned from adb

     */
    @Parameter( property = "android.uiautomator.propertiesKeyPrefix" )
    private String uiautomatorPropertiesKeyPrefix;

    @PullParameter( required = false, defaultValueGetterMethod = "getPropertiesKeyPrefix" )
    private String parsedPropertiesKeyPrefix;

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

        if ( isEnableIntegrationTest() )
        {
            playTests();
        }
    }

    /**
     * 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 at least a test threw an exception and isIgnoreTestFailures is false..
     * @throws MojoFailureException
     *             if at least a test failed and isIgnoreTestFailures is false.
     */
    protected void playTests() throws MojoExecutionException, MojoFailureException
    {

        getLog().debug( "Parsed values for Android UI UIAutomator invocation: " );
        getLog().debug( "jarFile:" + parsedJarFile );
        String testClassOrMethodString = buildSpaceSeparatedString( parsedTestClassOrMethods );
        getLog().debug( "testClassOrMethod:" + testClassOrMethodString );
        getLog().debug( "createReport:" + parsedCreateReport );

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

                UIAutomatorRemoteAndroidTestRunner automatorRemoteAndroidTestRunner //
                = new UIAutomatorRemoteAndroidTestRunner( parsedJarFile, device );

                automatorRemoteAndroidTestRunner.setRunName( "ui uiautomator tests" );
                automatorRemoteAndroidTestRunner.setDebug( uiautomatorDebug );
                automatorRemoteAndroidTestRunner.setTestClassOrMethods( parsedTestClassOrMethods );
                automatorRemoteAndroidTestRunner.setNoHup( parsedNoHup );
                automatorRemoteAndroidTestRunner.setUserProperties( session.getUserProperties(),
                        parsedPropertiesKeyPrefix );

                if ( parsedUseDump )
                {
                    automatorRemoteAndroidTestRunner.setDumpFilePath( parsedDumpFilePath );
                }

                getLog().info( deviceLogLinePrefix + "Running ui uiautomator tests in" + parsedJarFile );
                try
                {
                    AndroidTestRunListener testRunListener = new AndroidTestRunListener( device, getLog(),
                            parsedCreateReport, parsedTakeScreenshotOnFailure, parsedScreenshotsPathOnDevice,
                            parsedReportSuffix, targetDirectory );
                    automatorRemoteAndroidTestRunner.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 );
                }
            }
        };

        instrumentationTestExecutor = new ScreenshotServiceWrapper( instrumentationTestExecutor, project, getLog() );

        doWithDevices( instrumentationTestExecutor );
    }

    private String getJarFile()
    {
        if ( parsedJarFile == null )
        {
            File jarFilePath = new File( targetDirectory + File.separator
                    + finalName + ".jar" );
            return jarFilePath.getName();
        }
        return parsedJarFile;
    }

    private String[] getTestClassOrMethods()
    {
        // null if not overriden by configuration
        return parsedTestClassOrMethods;
    }

    private String getReportSuffix()
    {
        return parsedReportSuffix;
    }

    private String getPropertiesKeyPrefix()
    {
        return parsedPropertiesKeyPrefix;
    }

    /**
     * Helper method to build a comma separated string from a list. Blank strings are filtered out
     *
     * @param lines
     *            A list of strings
     * @return Comma separated String from given list
     */
    protected static String buildSpaceSeparatedString( String[] lines )
    {
        if ( lines == null || lines.length == 0 )
        {
            return null;
        }

        return StringUtils.join( lines, " " );
    }
}


package com.simpligility.maven.plugins.android.common;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.codehaus.plexus.logging.Logger;

import java.util.HashSet;
import java.util.Set;

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;

/**
 * Resolves the aar and apklib dependencies for an Artifact.
 *
 * @author William Ferguson - william.ferguson@xandar.com.au
 */
public final class DependencyResolver
{
    private final Logger log;
    private final DependencyGraphBuilder dependencyGraphBuilder;

    public DependencyResolver( Logger log, DependencyGraphBuilder dependencyGraphBuilder )
    {
        this.log = log;
        this.dependencyGraphBuilder = dependencyGraphBuilder;
    }

    /**
     * @param project   MavenProject for which to return the dependencies.
     * @param session   MavenSession in which to look for reactor dependencies.
     * @return all the dependencies for a project.
     * @throws DependencyGraphBuilderException if the dependency graph can't be built.
     */
    public Set<Artifact> getProjectDependenciesFor( MavenProject project, MavenSession session )
            throws DependencyGraphBuilderException
    {
        // No need to filter our search. We want to resolve all artifacts.
        final DependencyNode node = dependencyGraphBuilder.buildDependencyGraph( project, null, session.getProjects() );

        final DependencyCollector collector = new DependencyCollector( log, project.getArtifact() );
        collector.visit( node, false );
        return collector.getDependencies();
    }

    /**
     * Returns the Set of APKLIB, AAR, APK (direct or transitive) dependencies of the supplied artifact.
     *
     * The project is searched until artifact is found and then the library dependencies are looked for recursively.
     *
     * @param session           MavenSession in which to resolve the artifacts.
     * @param repositorySystem  RepositorySystem with which to resolve the artifacts.
     * @param artifact          Artifact for whom to get the dependencies.
     * @return Set of APK, APKLIB and AAR dependencies.
     * @throws org.apache.maven.plugin.MojoExecutionException if it couldn't resolve any of the dependencies.
     */
    public Set<Artifact> getLibraryDependenciesFor( MavenSession session,
                                                    RepositorySystem repositorySystem,
                                                    Artifact artifact )
            throws MojoExecutionException
    {
        // Set a filter that should only return interesting artifacts.
        final ArtifactFilter filter = new ArtifactFilter()
        {
            @Override
            public boolean include( Artifact found )
            {
                final String type = found.getType();
                return ( type.equals( APKLIB ) || type.equals( AAR ) || type.equals( APK ) );
            }
        };

        log.debug( "MavenSession = " + session + "  repositorySystem = " + repositorySystem );

        final ArtifactResolutionRequest request = new ArtifactResolutionRequest();
        request.setArtifact( artifact );
        request.setResolveRoot( false );        // Don't include source artifact in result
        request.setResolveTransitively( true ); // Include direct plus transitive dependencies.
        request.setServers( session.getRequest().getServers() );
        request.setMirrors( session.getRequest().getMirrors() );
        request.setProxies( session.getRequest().getProxies() );
        request.setLocalRepository( session.getLocalRepository() );
        request.setRemoteRepositories( session.getCurrentProject().getRemoteArtifactRepositories() );

        final ArtifactResolutionResult result = repositorySystem.resolve( request );

        final Set<Artifact> libraryDeps = new HashSet<Artifact>();
        for ( final Artifact depArtifact : result.getArtifacts() )
        {
            if ( filter.include( depArtifact ) )
            {
                libraryDeps.add( depArtifact );
            }
        }

        return libraryDeps;
    }
}


package com.simpligility.maven.plugins.android.phase01generatesources;

import com.android.builder.core.DefaultManifestParser;
import com.android.builder.symbols.RGeneration;
import com.android.builder.symbols.SymbolIo;
import com.android.builder.symbols.SymbolTable;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.logging.Log;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Generates R classes containing appropriate resource values for dependent libraries.
 *
 * @author William Ferguson - william.ferguson@xandar.com.au
 */
final class ResourceClassGenerator
{
    private final GenerateSourcesMojo mojo;
    private final File targetDirectory;
    private final File genDirectory;
    private final Log log;
    private final ClassLoader compileClassLoader;

    ResourceClassGenerator( final GenerateSourcesMojo mojo,
                            final File targetDirectory,
                            final File genDirectory,
                            final ClassLoader compileClassLoader
    )
    {
        this.mojo = mojo;
        this.targetDirectory = targetDirectory;
        this.genDirectory = genDirectory;
        this.log = mojo.getLog();
        this.compileClassLoader = compileClassLoader;
    }

    /**
     * Generates R java files those libraries that do not already have their R java in the compile classpath.
     *
     * If we are generating an integration test APK and the APK under test has a reference to a library for which
     * it generated an R java, then we don't want to generate that R java again for the test APK because otherwise
     * Proguard will fail when building the test APK because of duplication of the R java in the compile classpath.
     *
     * See {@link com.android.builder.core.AndroidBuilder#processResources(com.android.builder.internal.aapt.Aapt,
     * com.android.builder.internal.aapt.AaptPackageConfig.Builder, boolean)}
     *
     * @param libraries AAR libraries for which to generate R java files.
     */
    public void generateLibraryRs( final Set<Artifact> libraries )
    {
        // list of all the symbol tables
        final List<SymbolTable> symbolTables = new ArrayList<>( libraries.size() );

        // For each dependency, load its symbol file.
        for ( final Artifact lib : libraries )
        {
            final File unpackedLibDirectory = mojo.getUnpackedLibFolder( lib );
            final File rFile = new File( unpackedLibDirectory, "R.txt" );

            if ( rFile.isFile() )
            {
                final File libManifestFile = new File( unpackedLibDirectory, "AndroidManifest.xml" );
                final String packageName = new DefaultManifestParser( libManifestFile ).getPackage();
                if ( rJavaAlreadyExists( packageName ) )
                {
                    log.info( "Not creating R for " + packageName + " as it already exists" );
                    continue;
                }
                log.info( "Generating R for " + packageName + " at " + rFile );

                SymbolTable libSymbols = SymbolIo.read( rFile );
                libSymbols = libSymbols.rename( packageName, libSymbols.getTableName() );
                symbolTables.add( libSymbols );
            }
        }

        if ( symbolTables.isEmpty() )
        {
            return;
        }

        // load the full resources values from the R.txt calculated for the project.
        final File projectR = new File( targetDirectory, "R.txt" );
        final SymbolTable mainSymbols = SymbolIo.read( projectR );

        // now loop on all the package name, merge all the symbols to write, and write them
        RGeneration.generateRForLibraries( mainSymbols, symbolTables, genDirectory.getAbsoluteFile(), false );
    }

    private boolean rJavaAlreadyExists( String packageName )
    {
        final String rJavaClass = packageName + ".R";
        try
        {
            compileClassLoader.loadClass( rJavaClass );
            return true;
        }
        catch ( ClassNotFoundException e )
        {
            log.debug( "Could not resolve R java : " + rJavaClass );
            return false;
        }
    }
}


package com.simpligility.maven.plugins.android.standalonemojos;

import com.android.ddmlib.IDevice;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.DeviceCallback;
import com.simpligility.maven.plugins.android.common.DeviceHelper;

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

/**
 * DevicesMojo lists all attached devices and emulators found with the android debug bridge. It uses the same
 * naming convention for the emulator as used in other places in the Android Maven Plugin and adds the status
 * of the device in the list.
 *
 * TODO The goal is very simple and could be enhanced for better display, a verbose option to display and to take the
 * android.device paramter into account.
 *
 * @author Manfred Moser - manfred@simpligility.com
 */
@Mojo( name = "devices", requiresProject = false )
public class DevicesMojo extends AbstractAndroidMojo
{
    /**
     * Display a list of attached devices.
     *
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        doWithDevices( new DeviceCallback()
        {
            public void doWithDevice( final IDevice device ) throws MojoExecutionException
            {
                getLog().info( DeviceHelper.getDescriptiveNameWithStatus( device ) );
            }
        } );
    }
}