/*
 * 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.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.simpligility.maven.plugins.android.asm.AndroidTestFinder;
import com.simpligility.maven.plugins.android.common.DeviceHelper;
import com.simpligility.maven.plugins.android.configuration.Test;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * AbstractInstrumentationMojo implements running the instrumentation
 * tests.
 *
 * @author hugo.josefson@jayway.com
 * @author Manfred Moser - manfred@simpligility.com
 */
public abstract class AbstractInstrumentationMojo extends AbstractAndroidMojo
{

    /**
     * -Dmaven.test.skip is commonly used with Maven to skip tests. We honor it too.
     */
    @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 ignore test failures. We honor it too.
     * Ignore or not tests failures. If true they will be ignored; if
     * false, they will not. Default value is false.
     */
    @Parameter( property = "maven.test.failure.ignore", defaultValue = "false", readonly = true )
    private boolean mavenIgnoreTestFailure;

    /**
     * -Dmaven.test.error.ignore is commonly used with Maven to ignore test errors. We honor it too.
     * Ignore or not tests errors. If true they will be ignored; if
     * false, they will not. Default value is false.
     */
    @Parameter( property = "maven.test.error.ignore", defaultValue = "false", readonly = true )
    private boolean mavenIgnoreTestError;

    /**
     * The configuration to use for running instrumentation tests. Complete configuration
     * is possible in the plugin configuration:
     *

     * <test>
     *   <skip>true|false|auto</skip>
     *   <instrumentationPackage>packageName</instrumentationPackage>
     *   <instrumentationRunner>className</instrumentationRunner>
     *   <debug>true|false</debug>
     *   <coverage>true|false</coverage>
     *   <coverageFile></coverageFile>
     *   <logOnly>true|false</logOnly>  avd
     *   <testSize>small|medium|large</testSize>
     *   <createReport>true|false</createReport>
     *   <classes>
     *     <class>your.package.name.YourTestClass</class>
     *   </classes>
     *   <packages>
     *     <package>your.package.name</package>
     *   </packages>
     *   <instrumentationArgs>
     *     <instrumentationArg>key value</instrumentationArg>
     *   </instrumentationArgs>
     * </test>
     *

     */
    @Parameter
    private Test test;

    /**
     * Enables or disables integration test related goals. If true they will be skipped; if
     * false, they will be run. If auto, they will run if any of the classes inherit from any
     * class in junit.framework.** or android.test.**.
     */
    @Parameter( property = "android.test.skip", defaultValue = "auto" )
    private String testSkip;

    /**
     * Enables or disables integration safe failure.
     * If true build will not stop on test failure or error.
     */
    @Parameter( property = "android.test.failsafe", defaultValue = "true" )
    private Boolean testFailSafe;

    /**
     * Package name of the apk we wish to instrument. If not specified, it is inferred from
     * AndroidManifest.xml.
     */
    @Parameter( property = "android.test.instrumentationPackage" )
    private String testInstrumentationPackage;

    /**
     * Class name of test runner. If not specified, it is inferred from AndroidManifest.xml.
     */
    @Parameter( property = "android.test.instrumentationRunner" )
    private String testInstrumentationRunner;

    /**
     * Enable debug causing the test runner to wait until debugger is
     * connected with the Android debug bridge (adb).
     */
    @Parameter( property = "android.test.debug", defaultValue = "false" )
    private Boolean testDebug;

    /**
     * Enable or disable code coverage for this instrumentation test run.
     */
    @Parameter( property = "android.test.coverage", defaultValue = "false" )
    private Boolean testCoverage;

    /**
     * Location on device into which coverage should be stored (blank for
     * Android default /data/data/your.package.here/files/coverage.ec).
     */
    @Parameter( property = "android.test.coverageFile" )
    private String testCoverageFile;

    /**
     * Enable this flag to run a log only and not execute the tests.
     */
    @Parameter( property = "android.test.logonly", defaultValue = "false" )
    private Boolean testLogOnly;

    /**
     * If specified only execute tests of certain size as defined by
     * the Android instrumentation testing SmallTest, MediumTest and
     * LargeTest annotations. Use "small", "medium" or "large" as values.
     *
     * @see com.android.ddmlib.testrunner.IRemoteAndroidTestRunner
     */
    @Parameter( property = "android.test.testsize" )
    private String testTestSize;

    /**
     * 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.test.createreport", defaultValue = "true" )
    private Boolean testCreateReport;

    /**
     *
Whether to execute tests only in given packages as part of the instrumentation tests.

     *

     * <packages>
     *     <package>your.package.name</package>
     * </packages>
     *

     * or as e.g. -Dandroid.test.packages=package1,package2
     */
    @Parameter( property = "android.test.packages" )
    protected List<String> testPackages;

    /**
     *
Whether to execute test classes which are specified as part of the instrumentation tests.

     *

     * <classes>
     *     <class>your.package.name.YourTestClass</class>
     * </classes>
     *

     * or as e.g. -Dandroid.test.classes=class1,class2
     */
    @Parameter( property = "android.test.classes" )
    protected List<String> testClasses;

    /**
     *
Whether to execute tests which are annotated with the given annotations.

     *

     * <annotations>
     *     <annotation>your.package.name.YourAnnotation</annotation>
     * </annotations>
     *

     * or as e.g. -Dandroid.test.annotations=annotation1,annotation2
     */
    @Parameter( property = "android.test.annotations" )
    protected List<String> testAnnotations;

    /**
     *
Whether to execute tests which are not annotated with the given annotations.

     *

     * <excludeAnnotations>
     *     <excludeAnnotation>your.package.name.YourAnnotation</excludeAnnotation>
     * </excludeAnnotations>
     *

     * or as e.g. -Dandroid.test.excludeAnnotations=annotation1,annotation2
     */
    @Parameter( property = "android.test.excludeAnnotations" )
    protected List<String> testExcludeAnnotations;

    /**
     *
Extra instrumentation arguments.

     *

     * <instrumentationArgs>
     *     <instrumentationArg>key value</instrumentationArg>
     *     <instrumentationArg>key 'value with spaces'</instrumentationArg>
     * </instrumentationArgs>
     *

     * or as e.g. -Dandroid.test.instrumentationArgs="key1 value1","key2 'value with spaces'"
     */
    @Parameter( property = "android.test.instrumentationArgs" )
    protected List<String> testInstrumentationArgs;

    private boolean classesExists;
    private boolean packagesExists;

    // the parsed parameters from the plugin config or properties from command line or pom or settings
    private String parsedSkip;
    private String parsedInstrumentationPackage;
    private String parsedInstrumentationRunner;
    private List<String> parsedClasses;
    private List<String> parsedPackages;
    private List<String> parsedAnnotations;
    private List<String> parsedExcludeAnnotations;
    private Map<String, String> parsedInstrumentationArgs;
    private String parsedTestSize;
    private Boolean parsedCoverage;
    private String parsedCoverageFile;
    private Boolean parsedDebug;
    private Boolean parsedLogOnly;
    private Boolean parsedCreateReport;

    private String packagesList;

    protected void instrument() throws MojoExecutionException, MojoFailureException
    {
        parseConfiguration();

        if ( parsedInstrumentationPackage == null )
        {
            parsedInstrumentationPackage = extractPackageNameFromAndroidManifest( destinationManifestFile );
        }

        if ( parsedInstrumentationRunner == null )
        {
            parsedInstrumentationRunner = extractInstrumentationRunnerFromAndroidManifest( destinationManifestFile );
        }

        // only run Tests in specific package
        packagesList = buildCommaSeparatedString( parsedPackages );
        packagesExists = StringUtils.isNotBlank( packagesList );

        if ( parsedClasses != null )
        {
            classesExists = parsedClasses.size() > 0;
        }
        else
        {
            classesExists = false;
        }

        if ( classesExists && packagesExists )
        {
            // if both packages and classes are specified --> ERROR
            throw new  MojoFailureException( "packages and classes are mutually exclusive. They cannot be specified at"
                    + " the same time. Please specify either packages or classes. For details, see "
                    + "http://developer.android.com/guide/developing/testing/testing_otheride.html" );
        }

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

                RemoteAndroidTestRunner remoteAndroidTestRunner = new RemoteAndroidTestRunner(
                        parsedInstrumentationPackage, parsedInstrumentationRunner, device );

                if ( packagesExists )
                {
                    for ( String str : packagesList.split( "," ) )
                    {
                        remoteAndroidTestRunner.setTestPackageName( str );
                        getLog().info( deviceLogLinePrefix + "Running tests for specified test package: " + str );
                    }
                }

                if ( classesExists )
                {
                    remoteAndroidTestRunner
                            .setClassNames( parsedClasses.toArray( new String[ parsedClasses.size() ] ) );
                    getLog().info( deviceLogLinePrefix + "Running tests for specified test classes/methods: "
                            + parsedClasses );
                }

                if ( parsedAnnotations != null )
                {
                    for ( String annotation : parsedAnnotations )
                    {
                        remoteAndroidTestRunner.addInstrumentationArg( "annotation", annotation );
                    }
                }

                if ( parsedExcludeAnnotations != null )
                {
                    for ( String annotation : parsedExcludeAnnotations )
                    {
                        remoteAndroidTestRunner.addInstrumentationArg( "notAnnotation", annotation );
                    }

                }

                remoteAndroidTestRunner.setDebug( parsedDebug );
                remoteAndroidTestRunner.setCoverage( parsedCoverage );
                if ( StringUtils.isNotBlank( parsedCoverageFile ) )
                {
                    remoteAndroidTestRunner.addInstrumentationArg( "coverageFile", parsedCoverageFile );
                }
                remoteAndroidTestRunner.setLogOnly( parsedLogOnly );

                if ( StringUtils.isNotBlank( parsedTestSize ) )
                {
                    IRemoteAndroidTestRunner.TestSize validSize = IRemoteAndroidTestRunner.TestSize
                            .getTestSize( parsedTestSize );
                    remoteAndroidTestRunner.setTestSize( validSize );
                }

                addAllInstrumentationArgs( remoteAndroidTestRunner, parsedInstrumentationArgs );

                getLog().info( deviceLogLinePrefix +  "Running instrumentation tests in "
                        + parsedInstrumentationPackage );
                try
                {
                    AndroidTestRunListener testRunListener = new AndroidTestRunListener( device, getLog(),
                            parsedCreateReport, false, "", "", targetDirectory );
                    remoteAndroidTestRunner.run( testRunListener );
                    if ( testRunListener.hasFailuresOrErrors() && !testFailSafe )
                    {
                        throw new MojoFailureException( deviceLogLinePrefix + "Tests failed on device." );
                    }
                    if ( testRunListener.testRunFailed() && !testFailSafe  )
                    {
                        throw new MojoFailureException( deviceLogLinePrefix + "Test run failed to complete: "
                                + testRunListener.getTestRunFailureCause() );
                    }
                    if ( testRunListener.threwException() && !testFailSafe  )
                    {
                        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 void addAllInstrumentationArgs(
            final RemoteAndroidTestRunner remoteAndroidTestRunner,
            final Map<String, String> parsedInstrumentationArgs )
    {
        for ( final Map.Entry<String, String> entry : parsedInstrumentationArgs.entrySet() )
        {
            remoteAndroidTestRunner.addInstrumentationArg( entry.getKey(), entry.getValue() );
        }
    }

    private void parseConfiguration()
    {
        // we got config in pom ... lets use it,
        if ( test != null )
        {
            if ( StringUtils.isNotEmpty( test.getSkip() ) )
            {
                parsedSkip = test.getSkip();
            }
            else
            {
                parsedSkip = testSkip;
            }
            if ( StringUtils.isNotEmpty( test.getInstrumentationPackage() ) )
            {
                parsedInstrumentationPackage = test.getInstrumentationPackage();
            }
            else
            {
                parsedInstrumentationPackage = testInstrumentationPackage;
            }
            if ( StringUtils.isNotEmpty( test.getInstrumentationRunner() ) )
            {
                parsedInstrumentationRunner = test.getInstrumentationRunner();
            }
            else
            {
                parsedInstrumentationRunner = testInstrumentationRunner;
            }
            if ( test.getClasses() != null && ! test.getClasses().isEmpty() )
            {
                parsedClasses = test.getClasses();
            }
            else
            {
                parsedClasses = testClasses;
            }
            if ( test.getAnnotations() != null && ! test.getAnnotations().isEmpty() )
            {
                parsedAnnotations = test.getAnnotations();
            }
            else
            {
                parsedAnnotations =  testAnnotations;
            }
            if ( test.getExcludeAnnotations() != null && ! test.getExcludeAnnotations().isEmpty() )
            {
                parsedExcludeAnnotations = test.getExcludeAnnotations();
            }
            else
            {
                parsedExcludeAnnotations = testExcludeAnnotations;
            }
            if ( test.getPackages() != null && ! test.getPackages().isEmpty() )
            {
                parsedPackages = test.getPackages();
            }
            else
            {
                parsedPackages = testPackages;
            }
            if ( StringUtils.isNotEmpty( test.getTestSize() ) )
            {
                parsedTestSize = test.getTestSize();
            }
            else
            {
                parsedTestSize = testTestSize;
            }
            if ( test.isCoverage() != null )
            {
                parsedCoverage = test.isCoverage();
            }
            else
            {
                parsedCoverage = testCoverage;
            }
            if ( test.getCoverageFile() != null )
            {
                parsedCoverageFile = test.getCoverageFile();
            }
            else
            {
                parsedCoverageFile = "";
            }
            if ( test.isDebug() != null )
            {
                parsedDebug = test.isDebug();
            }
            else
            {
                parsedDebug = testDebug;
            }
            if ( test.isLogOnly() != null )
            {
                parsedLogOnly = test.isLogOnly();
            }
            else
            {
                parsedLogOnly = testLogOnly;
            }
            if ( test.isCreateReport() != null )
            {
                parsedCreateReport = test.isCreateReport();
            }
            else
            {
                parsedCreateReport = testCreateReport;
            }

            parsedInstrumentationArgs = InstrumentationArgumentParser.parse( test.getInstrumentationArgs() );
        }
        // no pom, we take properties
        else
        {
            parsedSkip = testSkip;
            parsedInstrumentationPackage = testInstrumentationPackage;
            parsedInstrumentationRunner = testInstrumentationRunner;
            parsedClasses = testClasses;
            parsedAnnotations = testAnnotations;
            parsedExcludeAnnotations = testExcludeAnnotations;
            parsedPackages = testPackages;
            parsedTestSize = testTestSize;
            parsedCoverage = testCoverage;
            parsedCoverageFile = testCoverageFile;
            parsedDebug = testDebug;
            parsedLogOnly = testLogOnly;
            parsedCreateReport = testCreateReport;
            parsedInstrumentationArgs = InstrumentationArgumentParser.parse( testInstrumentationArgs );
        }
    }

    /**
     * Whether or not to execute integration test related goals. Reads from configuration parameter
     * enableIntegrationTest, but can be overridden with -Dmaven.test.skip.
     *
     * @return true if integration test goals should be executed, false otherwise.
     */
    protected boolean isEnableIntegrationTest() throws MojoFailureException, MojoExecutionException
    {
        parseConfiguration();
        if ( mavenTestSkip )
        {
            getLog().info( "maven.test.skip set - skipping tests" );
            return false;
        }

        if ( mavenSkipTests )
        {
            getLog().info( "maven.skip.tests set - skipping tests" );
            return false;
        }

        if ( "true".equalsIgnoreCase( parsedSkip ) )
        {
            getLog().info( "android.test.skip set - skipping tests" );
            return false;
        }

        if ( "false".equalsIgnoreCase( parsedSkip ) )
        {
            return true;
        }

        if ( parsedSkip == null || "auto".equalsIgnoreCase( parsedSkip ) )
        {
            if ( extractInstrumentationRunnerFromAndroidManifest( destinationManifestFile ) == null )
            {
                getLog().info( "No InstrumentationRunner found - skipping tests" );
                return false;
            }
            return AndroidTestFinder.containsAndroidTests( projectOutputDirectory );
        }

        throw new MojoFailureException( "android.test.skip must be configured as 'true', 'false' or 'auto'." );

    }

    /**
     * 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 buildCommaSeparatedString( List<String> lines )
    {
        if ( lines == null || lines.size() == 0 )
        {
            return null;
        }

        List<String> strings = new ArrayList<String>( lines.size() );
        for ( String str : lines )
        { // filter out blank strings
            if ( StringUtils.isNotBlank( str ) )
            {
                strings.add( StringUtils.trimToEmpty( str ) );
            }
        }

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

}


/*
 * Copyright (C) 2011 Lorenzo Villani
 *
 * 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 com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.CollectingOutputReceiver;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
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.Run;

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 org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import java.io.IOException;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;

import java.io.BufferedReader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;

/**
 * Runs the first Activity shown in the top-level launcher as determined by its Intent filters.
 *

 * Android provides a component-based architecture, which means that there is no "main" function which serves as an
 * entry point to the APK. There's an homogeneous collection of Activity(es), Service(s), Receiver(s), etc.
 *

 *

 * The Android top-level launcher (whose purpose is to allow users to launch other applications) uses the Intent
 * resolution mechanism to determine which Activity(es) to show to the end user. Such activities are identified by at
 * least:
 *

 *
Action type: android.intent.action.MAIN

 *
Category: android.intent.category.LAUNCHER

 *

 *

 *
And are declared in AndroidManifest.xml as such:

 *

 * <activity android:name=".ExampleActivity">
 *     <intent-filter>
 *         <action android:name="android.intent.action.MAIN" />
 *         <category android:name="android.intent.category.LAUNCHER" />
 *     </intent-filter>
 * </activity>
 *

 *

 * This {@link Mojo} will try to to launch the first activity of this kind found in AndroidManifest.xml. In
 * case multiple activities satisfy the requirements listed above only the first declared one is run. In case there are
 * no "Launcher activities" declared in the manifest or no activities declared at all, this goal aborts throwing an
 * error.
 *

 *

 * The device parameter is taken into consideration so potentially the Activity found is started on all attached
 * devices. The application will NOT be deployed and running will silently fail if the application is not deployed.
 *

 *
 * @author Lorenzo Villani - lorenzo@villani.me
 * @author Manfred Moser - manfred@simpligility.com
 * @see "http://developer.android.com/guide/topics/fundamentals.html"
 * @see "http://developer.android.com/guide/topics/intents/intents-filters.html"
 */
@Mojo( name = "run" )
public class RunMojo extends AbstractAndroidMojo
{

    /**
     *
The configuration for the run goal can be set up in the plugin configuration in the pom file as:

     *

     * <run>
     *     <debug>true|false|portnumber</debug>
     * </run>
     *

     *
The <debug> parameter is optional and defaults to false. Numeric values like 5432 are
     * parsed as port number.
     *
The debug parameter can also be configured as property in the pom or settings file
     *

     * <properties>
     *     <android.run.debug>true</android.run.debug>
     * </properties>
     *

     * or from command-line with parameter -Dandroid.run.debug=true.

     */
    @Parameter
    @ConfigPojo
    private Run run;

    /**
     * Debug parameter for the the run goal. If true, the device or emulator will pause execution of the process at
     * startup to wait for a debugger to connect. Also see the "run" parameter documentation. Default value is false.
     * If the value is numeric, it is treated as a port number to forward the JDWP protocol
     * of the launched process to.
     */
    @Parameter( property = "android.run.debug" )
    protected String runDebug;

    /* the value for the debug flag after parsing pom and parameter */
    @PullParameter( defaultValue = "false" )
    private String parsedDebug;

    /**
     * Holds information about the "Launcher" activity.
     *
     * @author Lorenzo Villani
     */
    private static class LauncherInfo
    {
        private String packageName;

        private String activity;

        public String getPackageName()
        {
            return packageName;
        }

        public void setPackageName( String packageName )
        {
            this.packageName = packageName;
        }

        public String getActivity()
        {
            return activity;
        }

        public void setActivity( String activity )
        {
            this.activity = activity;
        }
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        if ( project.getPackaging().equals( APK ) )
        {
            try
            {
                LauncherInfo launcherInfo;

                launcherInfo = getLauncherActivity();

                ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
                configHandler.parseConfiguration();

                launch( launcherInfo );
            }
            catch ( Exception ex )
            {
                getLog().info( "Unable to run launcher Activity" );
                getLog().debug( ex );
            }
        }
        else
        {
            getLog().info( "Project packaging is not apk, skipping run action." );
        }
    }

    // ----------------------------------------------------------------------
    // Private methods
    // ----------------------------------------------------------------------

    /**
     * Gets the first "Launcher" Activity by running an XPath query on AndroidManifest.xml.
     *
     * @return A {@link LauncherInfo}
     * @throws MojoFailureException
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     * @throws XPathExpressionException
     */
    private LauncherInfo getLauncherActivity()
            throws ParserConfigurationException, SAXException, IOException, XPathExpressionException,
            MojoFailureException
    {
        Document document;
        DocumentBuilder documentBuilder;
        DocumentBuilderFactory documentBuilderFactory;
        Object result;
        XPath xPath;
        XPathExpression xPathExpression;
        XPathFactory xPathFactory;

        //
        // Setup JAXP stuff
        //
        documentBuilderFactory = DocumentBuilderFactory.newInstance();

        documentBuilder = documentBuilderFactory.newDocumentBuilder();

        document = documentBuilder.parse( destinationManifestFile );

        xPathFactory = XPathFactory.newInstance();

        xPath = xPathFactory.newXPath();

        xPathExpression = xPath.compile(
                "//manifest/application/activity/intent-filter[action[@name=\"android.intent.action.MAIN\"] "
                + "and category[@name=\"android.intent.category.LAUNCHER\"]]/.." );

        //
        // Run XPath query
        //
        result = xPathExpression.evaluate( document, XPathConstants.NODESET );

        if ( result instanceof NodeList )
        {
            NodeList activities;

            activities = ( NodeList ) result;

            if ( activities.getLength() > 0 )
            {
                // Grab the first declared Activity
                LauncherInfo launcherInfo;

                launcherInfo = new LauncherInfo();
                String activityName = activities.item( 0 ).getAttributes().getNamedItem( "android:name" )
                        .getNodeValue();

                if ( ! activityName.contains( "." ) )
                {
                    activityName = "." + activityName;
                }

                if ( activityName.startsWith( "." ) )
                {
                    String packageName = document.getElementsByTagName( "manifest" ).item( 0 ).getAttributes()
                            .getNamedItem( "package" ).getNodeValue();
                    activityName = packageName + activityName;
                }

                launcherInfo.activity = activityName;

                launcherInfo.packageName = renameManifestPackage != null
                    ? renameManifestPackage
                    : document.getDocumentElement().getAttribute( "package" ).toString();

                return launcherInfo;
            }
            else
            {
                // If we get here, we couldn't find a launcher activity.
                throw new MojoFailureException( "Could not find a launcher activity in manifest" );
            }
        }
        else
        {
            // If we get here we couldn't find any Activity
            throw new MojoFailureException( "Could not find any activity in manifest" );
        }
    }

    /**
     * Executes the "Launcher activity".
     *
     * @param info A {@link LauncherInfo}.
     * @throws MojoFailureException
     * @throws MojoExecutionException
     */
    private void launch( final LauncherInfo info ) throws MojoExecutionException, MojoFailureException
    {
        final String command;

        final int debugPort = findDebugPort();

        command = String.format( "am start %s-n %s/%s", debugPort >= 0 ? "-D " : "", info.packageName, info.activity );

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

                try
                {
                    getLog().info( deviceLogLinePrefix + "Attempting to start " + info.packageName + "/"
                            + info.activity );

                    CollectingOutputReceiver shellOutput = new CollectingOutputReceiver();
                    device.executeShellCommand( command, shellOutput );
                    if ( shellOutput.getOutput().contains( "Error" ) )
                    {
                        throw new MojoFailureException( shellOutput.getOutput() );
                    }
                    if ( debugPort > 0 )
                    {
                        int pid = findPid( device, "ps" );
                        if ( pid == -1 )
                        {
                            pid = findPid( device, "ps -Af" );
                        }
                        if ( pid == -1 )
                        {
                            throw new MojoFailureException( "Cannot find stated process " + info.packageName );
                        }
                        getLog().info(
                            deviceLogLinePrefix + "Process " + debugPort + " launched"
                        );
                        try
                        {
                            createForward( device, debugPort, pid );
                            getLog().info(
                                deviceLogLinePrefix + "Debugger listening on " + debugPort
                            );
                        }
                        catch ( Exception ex )
                        {
                            throw new MojoFailureException(
                                "Cannot create forward tcp: " + debugPort
                                    + " jdwp: " + pid, ex
                            );
                        }
                    }
                }
                catch ( IOException ex )
                {
                    throw new MojoFailureException( deviceLogLinePrefix + "Input/Output error", ex );
                }
                catch ( TimeoutException ex )
                {
                    throw new MojoFailureException( deviceLogLinePrefix + "Command timeout", ex );
                }
                catch ( AdbCommandRejectedException ex )
                {
                    throw new MojoFailureException( deviceLogLinePrefix + "ADB rejected the command", ex );
                }
                catch ( ShellCommandUnresponsiveException ex )
                {
                    throw new MojoFailureException( deviceLogLinePrefix + "Unresponsive command", ex );
                }
            }

            private int findPid( IDevice device, final String cmd )
            throws IOException, TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException
            {
                CollectingOutputReceiver processOutput = new CollectingOutputReceiver();
                device.executeShellCommand( cmd, processOutput );
                BufferedReader r = new BufferedReader( new StringReader( processOutput.getOutput() ) );
                int pid = -1;
                for ( ;; )
                {
                    String line = r.readLine();
                    if ( line == null )
                    {
                        break;
                    }
                    if ( line.endsWith( info.packageName ) )
                    {
                        String[] values = line.split( " +" );
                        if ( values.length > 2 )
                        {
                            pid = Integer.valueOf( values[1] );
                            break;
                        }
                    }
                }
                r.close();
                return pid;
            }
        } );
    }

    private static void createForward( IDevice device, int debugPort, int pid )
    throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
    {
        Method m = Class.forName( "com.android.ddmlib.AdbHelper" ).
            getDeclaredMethod(
                "createForward", InetSocketAddress.class,
                device.getClass(), String.class, String.class
            );
        m.setAccessible( true );
        m.invoke(
            null, AndroidDebugBridge.getSocketAddress(), device,
            String.format( "tcp:%d", debugPort ), String.format( "jdwp:%d", pid )
        );
    }

    private int findDebugPort()
    {
        int debugPort;
        if ( "true".equals( parsedDebug ) )
        {
            debugPort = 0;
        }
        else
        {
            try
            {
                debugPort = Integer.parseInt( parsedDebug );
            }
            catch ( NumberFormatException ex )
            {
                debugPort = -1;
            }
        }
        return debugPort;
    }
}


/*
 * 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 org.apache.commons.lang3.SystemUtils;
import org.apache.maven.plugin.MojoExecutionException;

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

/**
 * Represents an Android NDK.
 *
 * @author Johan Lindquist - johanlindquist@gmail.com
 * @author Manfred Moser - manfred@simpligility.com
 */
public class AndroidNdk
{

    public static final String PROPER_NDK_HOME_DIRECTORY_MESSAGE = "Please provide a proper Android NDK directory path"
            + " as configuration parameter <ndk><path>...</path></ndk> in the plugin <configuration/>. As an "
            + "alternative, you may add the parameter to commandline: -Dandroid.ndk.path=... or set environment "
            + "variable ANDROID_ND_HOME.";

    public static final String[] NDK_ARCHITECTURES = { "armeabi", "armeabi-v7a", "arm64-v8a", "mips", "mips64",
            "x86", "x86_64" };

    /**
     * Arm toolchain implementations.
     */
    public static final String[] ARM_TOOLCHAIN = {  "arm-linux-androideabi-4.9", "arm-linux-androideabi-4.8",
            "arm-linux-androideabi-4.7", "arm-linux-androideabi-4.6", "arm-linux-androideabi-4.4.3" };

    /**
     * Arm64 toolchain implementations.
     */
    public static final String[] ARM64_TOOLCHAIN = {  "aarch64-linux-android-4.9" };

    /**
     * x86 toolchain implementations.
     */
    public static final String[] X86_TOOLCHAIN = { "x86-4.9", "x86-4.8", "x86-4.7", "x86-4.6", "x86-4.4.3" };

    /**
     * x86 toolchain implementations.
     */
    public static final String[] X86_64_TOOLCHAIN = { "x86_64-4.9" };

    /**
     * Mips toolchain implementations.
     */
    public static final String[] MIPS_TOOLCHAIN = { "mipsel-linux-android-4.9", "mipsel-linux-android-4.8",
            "mipsel-linux-android-4.7", "mipsel-linux-android-4.6", "mipsel-linux-android-4.4.3" };

    /**
     * Mips toolchain implementations.
     */
    public static final String[] MIPS64_TOOLCHAIN = { "mips64el-linux-android-4.9" };

    /**
     * Possible locations for the gdbserver file.
     */
    private static final String[] GDB_SERVER_LOCATIONS = { "toolchains/%s/prebuilt/gdbserver",
                                                           "prebuilt/%s/gdbserver/gdbserver" };

    private final File ndkPath;

    public AndroidNdk( File ndkPath )
    {
        assertPathIsDirectory( ndkPath );
        this.ndkPath = ndkPath;
    }

    private void assertPathIsDirectory( final File path )
    {
        if ( path == null )
        {
            throw new InvalidNdkException( PROPER_NDK_HOME_DIRECTORY_MESSAGE );
        }
        if ( ! path.isDirectory() )
        {
            throw new InvalidNdkException(
                    "Path \"" + path + "\" is not a directory. " + PROPER_NDK_HOME_DIRECTORY_MESSAGE );
        }
    }

    private File findStripper( String toolchain )
    {
        List<String> osDirectories = new ArrayList<String>();
        String extension = "";

        if ( SystemUtils.IS_OS_LINUX )
        {
            osDirectories.add( "linux-x86" );
            osDirectories.add( "linux-x86_64" );
        }
        else if ( SystemUtils.IS_OS_WINDOWS )
        {
            osDirectories.add( "windows" );
            osDirectories.add( "windows-x86_64" );
            extension = ".exe";
        }
        else if ( SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX )
        {
            osDirectories.add( "darwin-x86" );
            osDirectories.add( "darwin-x86_64" );
        }

        String fileName = "";
        if ( toolchain.startsWith( "arm" ) )
        {
            fileName = "arm-linux-androideabi-strip" + extension;
        }
        else if ( toolchain.startsWith( "aarch64" ) )
        {
            fileName = "aarch64-linux-android-strip" + extension;
        }
        else if ( toolchain.startsWith( "x86_64" ) )
        {
            fileName = "x86_64-linux-android-strip" + extension;
        }
        else if ( toolchain.startsWith( "x86" ) )
        {
            fileName = "i686-linux-android-strip" + extension;
        }
        else if ( toolchain.startsWith( "mips64" ) )
        {
            fileName = "mips64el-linux-android-strip" + extension;
        }
        else if ( toolchain.startsWith( "mips" ) )
        {
            fileName = "mipsel-linux-android-strip" + extension;
        }

        for ( String osDirectory : osDirectories )
        {
            String stripperLocation =
                String.format( "toolchains/%s/prebuilt/%s/bin/%s", toolchain, osDirectory, fileName );
            final File stripper = new File( ndkPath, stripperLocation );
            if ( stripper.exists() )
            {
                return stripper;
            }
        }
        return null;
    }

    public File getStripper( String toolchain ) throws MojoExecutionException
    {
        final File stripper = findStripper( toolchain );
        if ( stripper == null )
        {
            throw new MojoExecutionException( "Could not resolve stripper for current OS: " + SystemUtils.OS_NAME );
        }

        // Some basic validation
        if ( ! stripper.exists() )
        {
            throw new MojoExecutionException( "Strip binary " + stripper.getAbsolutePath()
                    + " does not exist, please double check the toolchain and OS used" );
        }

        // We should be good to go
        return stripper;
    }

    private String resolveNdkToolchain( String[] toolchains )
    {
        for ( String toolchain : toolchains )
        {
            File f = findStripper( toolchain );
            if ( f != null && f.exists() )
            {
                return toolchain;
            }
        }
        return null;
    }

    /**
     * Tries to resolve the toolchain based on the path of the file.
     *
     * @param file Native library
     * @return String
     * @throws MojoExecutionException When no toolchain is found
     */
    public String getToolchain( File file ) throws MojoExecutionException
    {
        String resolvedNdkToolchain = null;

        // try to resolve the toolchain now
        String ndkArchitecture = file.getParentFile().getName();
        if ( ndkArchitecture.startsWith( "armeabi" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( ARM_TOOLCHAIN );
        }
        else if ( ndkArchitecture.startsWith( "arm64" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( ARM64_TOOLCHAIN );
        }
        else if ( ndkArchitecture.startsWith( "x86_64" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( X86_64_TOOLCHAIN );
        }
        else if ( ndkArchitecture.startsWith( "x86" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( X86_TOOLCHAIN );
        }
        else if ( ndkArchitecture.startsWith( "mips64" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( MIPS64_TOOLCHAIN );
        }
        else if ( ndkArchitecture.startsWith( "mips" ) )
        {
            resolvedNdkToolchain = resolveNdkToolchain( MIPS_TOOLCHAIN );
        }

        // if no toolchain can be found
        if ( resolvedNdkToolchain == null )
        {
            throw new MojoExecutionException(
                "Can not resolve automatically a toolchain to use. Please specify one." );
        }
        return resolvedNdkToolchain;
    }

    /**
     * Returns the complete path for the ndk-build tool, based on this NDK.
     *
     * @return the complete path as a String, including the tool's filename.
     */
    public String getNdkBuildPath()
    {
        if ( SystemUtils.IS_OS_WINDOWS )
        {
            return new File( ndkPath, "/ndk-build.cmd" ).getAbsolutePath();
        }
        else
        {
            return new File( ndkPath, "/ndk-build" ).getAbsolutePath();
        }
    }

    public File getGdbServer( String ndkArchitecture ) throws MojoExecutionException
    {
        // create a list of possible gdb server parent folder locations
        List<String> gdbServerLocations = new ArrayList<String>();
        if ( ndkArchitecture.startsWith( "armeabi" ) )
        {
            gdbServerLocations.add( "android-arm" );
            gdbServerLocations.add( "android-armeabi" );
            gdbServerLocations.addAll( Arrays.asList( ARM_TOOLCHAIN ) );
        }
        else if ( ndkArchitecture.startsWith( "arm64" ) )
        {
            gdbServerLocations.add( "android-arm64" );
            gdbServerLocations.addAll( Arrays.asList( ARM64_TOOLCHAIN ) );
        }
        else if ( ndkArchitecture.startsWith( "x86_64" ) )
        {
            gdbServerLocations.add( "android-x86_64" );
            gdbServerLocations.addAll( Arrays.asList( X86_64_TOOLCHAIN ) );
        }
        else if ( ndkArchitecture.startsWith( "x86" ) )
        {
            gdbServerLocations.add( "android-x86" );
            gdbServerLocations.addAll( Arrays.asList( X86_TOOLCHAIN ) );
        }
        else if ( ndkArchitecture.startsWith( "mips64" ) )
        {
            gdbServerLocations.add( "android-mips64" );
            gdbServerLocations.addAll( Arrays.asList( MIPS64_TOOLCHAIN ) );
        }
        else if ( ndkArchitecture.startsWith( "mips" ) )
        {
            gdbServerLocations.add( "android-mips" );
            gdbServerLocations.addAll( Arrays.asList( MIPS_TOOLCHAIN ) );
        }

        // check for the gdb server
        for ( String location : GDB_SERVER_LOCATIONS )
        {
            for ( String gdbServerLocation : gdbServerLocations )
            {
                File gdbServerFile = new File( ndkPath, String.format( location, gdbServerLocation ) );
                if ( gdbServerFile.exists() )
                {
                    return gdbServerFile;
                }
            }
        }

        //  if we got here, throw an error
        throw new MojoExecutionException( "gdbserver binary for architecture " + ndkArchitecture
            + " does not exist, please double check the toolchain and OS used" );
    }

}


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

import com.android.ddmlib.IDevice;
import org.apache.commons.lang3.StringUtils;

/**
 * A bunch of helper methods for dealing with IDevice instances.
 *
 * @author Manfred Moser - manfred@simpligility.com
 */
public class DeviceHelper
{

    private static final String MANUFACTURER_PROPERTY = "ro.product.manufacturer";
    private static final String MODEL_PROPERTY = "ro.product.model";
    private static final String SEPARATOR = "_";
    /**
     * Get a device identifier string that is suitable for filenames as well as log messages.
     * This means it is human readable and contains no spaces.
     * Used for instrumentation test report file names so see more at
     * AbstractInstrumentationMojo#testCreateReport javadoc since
     * that is the public documentation.
     */
    public static String getDescriptiveName( IDevice device )
    {
        // if any of this logic changes update javadoc for
        // AbstractInstrumentationMojo#testCreateReport
        StringBuilder identfier = new StringBuilder().append( device.getSerialNumber() );
        if ( device.getAvdName() != null )
        {
            identfier.append( SEPARATOR ).append( device.getAvdName() );
        }
        String manufacturer = getManufacturer( device );
        if ( StringUtils.isNotBlank( manufacturer ) )
        {
            identfier.append( SEPARATOR ).append( manufacturer );
        }
        String model = getModel( device );
        if ( StringUtils.isNotBlank( model ) )
        {
            identfier.append( SEPARATOR ).append( model );
        }

        return FileNameHelper.fixFileName( identfier.toString() );
    }

    public static String getDeviceLogLinePrefix( IDevice device )
    {
        return getDescriptiveName( device ) + " :   ";
    }

    /**
     * @return the manufacturer of the device as set in #MANUFACTURER_PROPERTY, typically "unknown" for emulators
     */
    public static String getManufacturer( IDevice device )
    {
        return StringUtils.deleteWhitespace( device.getProperty( MANUFACTURER_PROPERTY ) );
    }

    /**
     * @return the model of the device as set in #MODEL_PROPERTY, typically "sdk" for emulators
     */
    public static String getModel( IDevice device )
    {
        return StringUtils.deleteWhitespace( device.getProperty( MODEL_PROPERTY ) );
    }

    /**
     * @return the descriptive name with online/offline/unknown status string appended.
     */
    public static String getDescriptiveNameWithStatus( IDevice device )
    {
        String status;
        if ( device.isOnline() )
        {
            status = "Online";
        }
        else
        {
            if ( device.isOffline() )
            {
                status = "Offline";
            }
            else
            {
                status = "Unknown";
            }
        }
        return getDescriptiveName( device ) + " " + status;
    }
}


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

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.XmlHelper;
import com.simpligility.maven.plugins.android.configuration.Manifest;
import com.simpligility.maven.plugins.android.configuration.UsesSdk;
import com.simpligility.maven.plugins.android.configuration.VersionGenerator;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
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.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;

/**
 * Updates various version attributes present in the AndroidManifest.xml file.
 *
 * @author joakim@erdfelt.com
 * @author nic.strong@gmail.com
 * @author Manfred Moser - manfred@simpligility.com
 * @deprecated Use manifest-merger v2 mojo instead
 * {@link com.simpligility.maven.plugins.android.standalonemojos.ManifestMergerMojo}
 */
@Deprecated
@Mojo( name = "manifest-update", defaultPhase = LifecyclePhase.PROCESS_RESOURCES )
public class ManifestUpdateMojo extends AbstractAndroidMojo
{
    // basic attributes
    private static final String ATTR_VERSION_NAME = "android:versionName";
    private static final String ATTR_VERSION_CODE = "android:versionCode";
    private static final String ATTR_SHARED_USER_ID = "android:sharedUserId";
    private static final String ATTR_DEBUGGABLE = "android:debuggable";

    // supports-screens attributes
    private static final String ATTR_SCREEN_DENSITY = "android:screenDensity";
    private static final String ATTR_SCREEN_SIZE = "android:screenSize";

    // compatible-screens attributes
    private static final String ATTR_ANY_DENSITY = "android:anyDensity";
    private static final String ATTR_SMALL_SCREENS = "android:smallScreens";
    private static final String ATTR_NORMAL_SCREENS = "android:normalScreens";
    private static final String ATTR_LARGE_SCREENS = "android:largeScreens";
    private static final String ATTR_XLARGE_SCREENS = "android:xlargeScreens";
    private static final String ATTR_RESIZEABLE = "android:resizeable";
    private static final String ATTR_REQUIRES_SMALLEST_WIDTH_DP = "android:requiresSmallestWidthDp";
    private static final String ATTR_LARGEST_WIDTH_LIMIT_DP = "android:largestWidthLimitDp";
    private static final String ATTR_COMPATIBLE_WIDTH_LIMIT_DP = "android:compatibleWidthLimitDp";

    // uses-sdk attributes
    private static final String ATTR_MIN_SDK_VERSION = "android:minSdkVersion";
    private static final String ATTR_MAX_SDK_VERSION = "android:maxSdkVersion";
    private static final String ATTR_TARGET_SDK_VERSION = "android:targetSdkVersion";

    // provider attributes
    private static final String ATTR_NAME = "android:name";
    private static final String ATTR_AUTHORITIES = "android:authorities";
    // application attributes
    private static final String ATTR_APPLICATION_ICON = "android:icon";
    private static final String ATTR_APPLICATION_LABEL = "android:label";
    private static final String ATTR_APPLICATION_THEME = "android:theme";

    private static final String ELEM_APPLICATION = "application";
    private static final String ELEM_PROVIDER = "provider";
    private static final String ELEM_SUPPORTS_SCREENS = "supports-screens";
    private static final String ELEM_COMPATIBLE_SCREENS = "compatible-screens";
    private static final String ELEM_SCREEN = "screen";
    private static final String ELEM_USES_SDK = "uses-sdk";

    /**
     * Configuration for the manifest-update goal.
     *

     * You can configure this mojo to update the following basic manifest attributes:
     *

     *

     * android:versionName on the manifest element.
     * android:versionCode on the manifest element.
     * android:sharedUserId on the manifest element.
     * android:debuggable on the application element.
     *

     *

     * Moreover, you may specify custom values for the supports-screens and
     * compatible-screens elements. This is useful if you're using custom build
     * profiles to build APKs tailored to specific screen configurations. Values passed via POM
     * configuration for these elements will be merged with whatever is found in the Manifest file.
     * Values defined in the POM will take precedence.
     *

     *
     * Note: This process will reformat the AndroidManifest.xml per JAXP
     * {@link Transformer} defaults if updates are made to the manifest.
     *
     * You can configure attributes in the plugin configuration like so
     *
     *

     *   <plugin>
     *     <groupId>com.jayway.maven.plugins.android.generation2</groupId>
     *     <artifactId>android-maven-plugin</artifactId>
     *     <executions>
     *       <execution>
     *         <id>update-manifest</id>
     *         <goals>
     *           <goal>manifest-update</goal>
     *         </goals>
     *         <configuration>
     *           <manifest>
     *             <versionName></versionName>
     *             <versionCode>123</versionCode>
     *             <versionCodeAutoIncrement>true|false</versionCodeAutoIncrement>
     *             <versionCodeUpdateFromVersion>true|false</versionCodeUpdateFromVersion>
     *             <sharedUserId>anId</sharedUserId>
     *             <debuggable>true|false</debuggable>
     *
     *             <supports-screens>
     *               <anyDensity>true</anyDensity>
     *               <xlargeScreens>false</xlargeScreens>
     *             </supports-screens>
     *
     *             <compatible-screens>
     *               <compatible-screen>
     *                 <screenSize>small</screenSize>
     *                 <screenDensity>ldpi</screenDensity>
     *               </compatible-screen>
     *             </compatible-screens>
     *           </manifest>
     *         </configuration>
     *       </execution>
     *     </executions>
     *   </plugin>
     *

     *
     * or use properties set in the pom or settings file or supplied as command line parameter. Add
     * "android." in front of the property name for command line usage. All parameters follow a
     * manifest.* naming convention.
     *
     */
    @Parameter
    private Manifest manifest;

    /**
     * Update the android:versionName with the specified parameter. If left empty it
     * will use the version number of the project. Exposed via the project property
     * android.manifest.versionName.
     */
    @Parameter( property = "android.manifest.versionName", defaultValue = "${project.version}" )
    protected String manifestVersionName;

    /**
     * Update the android:versionCode attribute with the specified parameter. Exposed via
     * the project property android.manifest.versionCode.
     */
    @Parameter( property = "android.manifest.versionCode" )
    protected Integer manifestVersionCode;

    /**
     * Auto increment the android:versionCode attribute with each build. The value is
     * exposed via the project property android.manifest.versionCodeAutoIncrement and
     * the resulting value as android.manifest.versionCode.
     */
    @Parameter( property = "android.manifest.versionCodeAutoIncrement", defaultValue = "false" )
    private Boolean manifestVersionCodeAutoIncrement = false;

    /**
     * Update the android:icon attribute with the specified parameter. Exposed via
     * the project property android.manifest.appIcon.
     */
    @Parameter( property = "android.manifest.applicationIcon" )
    private String manifestApplicationIcon;

    /**
     * Update the android:label attribute with the specified parameter. Exposed via
     * the project property android.manifest.appLabel.
     */
    @Parameter( property = "android.manifest.applicationLabel" )
    private String manifestApplicationLabel;

    /**
     * Update the android:theme attribute with the specified parameter. Exposed via
     * the project property android.manifest.applicationTheme.
     */
    @Parameter( property = "android.manifest.applicationTheme" )
    private String manifestApplicationTheme;

    /**
     * Update the android:versionCode attribute automatically from the project version
     * e.g 3.2.1 will become version code 3002001. As described in this blog post
     * http://www.simpligility.com/2010/11/release-version-management-for-your-android-application/
     * but done without using resource filtering. The value is exposed via the project property
     * property android.manifest.versionCodeUpdateFromVersion and the resulting value
     * as android.manifest.versionCode.
     * For the purpose of generating the versionCode, if a version element is missing it is presumed to be 0.
     * The maximum values for the version increment and version minor values are 999,
     * the version major should be no larger than 2000.  Any other suffixes do not
     * participate in the version code generation.
     */
    @Parameter( property = "android.manifest.versionCodeUpdateFromVersion", defaultValue = "false" )
    protected Boolean manifestVersionCodeUpdateFromVersion = false;

    /**
     * Update the android:sharedUserId attribute with the specified parameter. If
     * specified, exposes the project property android.manifest.sharedUserId.
     */
    @Parameter( property = "android.manifest.sharedUserId" )
    protected String manifestSharedUserId;

    /**
     * Update the android:debuggable attribute with the specified parameter. Exposed via
     * the project property android.manifest.debuggable.
     */
    @Parameter( property = "android.manifest.debuggable" )
    protected Boolean manifestDebuggable;

    /**
     * For a given provider (named by android:name update the android:authorities
     * attribute for the provider. Exposed via the project property android.manifest.providerAuthorities.
     */
    @Parameter( property = "android.manifest.providerAuthorities" )
    protected Properties manifestProviderAuthorities;

    protected SupportsScreens manifestSupportsScreens;

    protected List<CompatibleScreen> manifestCompatibleScreens;

    /**
     *  Update the uses-sdk tag. It can be configured to change: android:minSdkVersion,
     *  android:maxSdkVersion and android:targetSdkVersion
     */
    protected UsesSdk manifestUsesSdk;

    private String parsedVersionName;
    private Integer parsedVersionCode;
    private boolean parsedVersionCodeAutoIncrement;
    private String parsedApplicationIcon;
    private String parsedApplicationLabel;
    private String parsedApplicationTheme;
    private Boolean parsedVersionCodeUpdateFromVersion;
    private String parsedSharedUserId;
    private Boolean parsedDebuggable;
    private SupportsScreens parsedSupportsScreens;
    private List<CompatibleScreen> parsedCompatibleScreens;
    private Properties parsedProviderAuthorities;
    private UsesSdk parsedUsesSdk;

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        if ( ! AndroidExtension.isAndroidPackaging( project.getPackaging() ) )
        {
            return; // skip, not an android project.
        }

        if ( destinationManifestFile == null )
        {
            return; // skip, no androidmanifest.xml defined (rare case)
        }

        parseConfiguration();

        getLog().info( "Attempting to update manifest " + destinationManifestFile );
        getLog().debug( "    usesSdk=" + parsedUsesSdk );
        getLog().debug( "    versionName=" + parsedVersionName );
        getLog().debug( "    versionCode=" + parsedVersionCode );
        getLog().debug( "    versionCodeAutoIncrement=" + parsedVersionCodeAutoIncrement );
        getLog().debug( "    versionCodeUpdateFromVersion=" + parsedVersionCodeUpdateFromVersion );

        getLog().debug( "    applicationIcon=" + parsedApplicationIcon );
        getLog().debug( "    applicationLabel=" + parsedApplicationLabel );
        getLog().debug( "    applicationTheme=" + parsedApplicationTheme );

        getLog().debug( "    sharedUserId=" + parsedSharedUserId );
        getLog().debug( "    debuggable=" + parsedDebuggable );
        getLog().debug( "    providerAuthorities: " + parsedProviderAuthorities );
        getLog().debug( "    supports-screens: " + ( parsedSupportsScreens == null ? "not set" : "set" ) );
        getLog().debug( "    compatible-screens: " + ( parsedCompatibleScreens == null ? "not set" : "set" ) );

        if ( ! destinationManifestFile.exists() )
        {
            return; // skip, no AndroidManifest.xml file found.
        }

        try
        {
            updateManifest( destinationManifestFile );
        }
        catch ( IOException e )
        {
            throw new MojoFailureException( "XML I/O error: " + destinationManifestFile, e );
        }
        catch ( ParserConfigurationException e )
        {
            throw new MojoFailureException( "Unable to prepare XML parser", e );
        }
        catch ( SAXException e )
        {
            throw new MojoFailureException( "Unable to parse XML: " + destinationManifestFile, e );
        }
        catch ( TransformerException e )
        {
            throw new MojoFailureException( "Unable write XML: " + destinationManifestFile, e );
        }
    }

    private void parseConfiguration()
    {
        // manifest element found in plugin config in pom
        if ( manifest != null )
        {
            if ( StringUtils.isNotEmpty( manifest.getVersionName() ) )
            {
                parsedVersionName = manifest.getVersionName();
            }
            else
            {
                parsedVersionName = manifestVersionName;
            }
            if ( manifest.getVersionCode() != null )
            {
                parsedVersionCode = manifest.getVersionCode();
            }
            else
            {
                parsedVersionCode = manifestVersionCode;
            }
            if ( manifest.getVersionCodeAutoIncrement() != null )
            {
                parsedVersionCodeAutoIncrement = manifest.getVersionCodeAutoIncrement();
            }
            else
            {
                parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement;
            }
            if ( manifest.getVersionCodeUpdateFromVersion() != null )
            {
                parsedVersionCodeUpdateFromVersion = manifest.getVersionCodeUpdateFromVersion();
            }
            else
            {
                parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion;
            }

            if ( StringUtils.isNotEmpty( manifest.getApplicationIcon() ) )
            {
                parsedApplicationIcon = manifest.getApplicationIcon();
            }
            else
            {
                parsedApplicationIcon = manifestApplicationIcon;
            }

            if ( StringUtils.isNotEmpty( manifest.getApplicationLabel() ) )
            {
                parsedApplicationLabel = manifest.getApplicationLabel();
            }
            else
            {
                parsedApplicationLabel = manifestApplicationLabel;
            }

            if ( StringUtils.isNotEmpty( manifest.getApplicationTheme() ) )
            {
                parsedApplicationTheme = manifest.getApplicationTheme();
            }
            else
            {
                parsedApplicationTheme = manifestApplicationTheme;
            }

            if ( StringUtils.isNotEmpty( manifest.getSharedUserId() ) )
            {
                parsedSharedUserId = manifest.getSharedUserId();
            }
            else
            {
                parsedSharedUserId = manifestSharedUserId;
            }
            if ( manifest.getDebuggable() != null )
            {
                parsedDebuggable = manifest.getDebuggable();
            }
            else
            {
                parsedDebuggable = manifestDebuggable;
            }
            if ( manifest.getSupportsScreens() != null )
            {
                parsedSupportsScreens = manifest.getSupportsScreens();
            }
            else
            {
                parsedSupportsScreens = manifestSupportsScreens;
            }
            if ( manifest.getCompatibleScreens() != null )
            {
                parsedCompatibleScreens = manifest.getCompatibleScreens();
            }
            else
            {
                parsedCompatibleScreens = manifestCompatibleScreens;
            }
            if ( manifest.getProviderAuthorities() != null )
            {
                parsedProviderAuthorities = manifest.getProviderAuthorities();
            }
            else
            {
                parsedProviderAuthorities = manifestProviderAuthorities;
            }
            if ( manifest.getUsesSdk() != null )
            {
                parsedUsesSdk = manifest.getUsesSdk();
            }
            else
            {
                parsedUsesSdk = manifestUsesSdk;
            }
        }
        else
        {
            parsedVersionName = manifestVersionName;
            parsedVersionCode = manifestVersionCode;
            parsedVersionCodeAutoIncrement = manifestVersionCodeAutoIncrement;
            parsedVersionCodeUpdateFromVersion = manifestVersionCodeUpdateFromVersion;
            parsedApplicationIcon = manifestApplicationIcon;
            parsedApplicationLabel = manifestApplicationLabel;
            parsedApplicationTheme = manifestApplicationTheme;
            parsedSharedUserId = manifestSharedUserId;
            parsedDebuggable = manifestDebuggable;
            parsedSupportsScreens = manifestSupportsScreens;
            parsedCompatibleScreens = manifestCompatibleScreens;
            parsedProviderAuthorities = manifestProviderAuthorities;
            parsedUsesSdk = manifestUsesSdk;
        }
    }

    /**
     * Read manifest using JAXP
     */
    private Document readManifest( File manifestFile ) throws IOException, ParserConfigurationException, SAXException
    {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse( manifestFile );
        return doc;
    }

    /**
     * Write manifest using JAXP transformer
     */
    private void writeManifest( File manifestFile, Document doc ) throws IOException, TransformerException
    {
        TransformerFactory xfactory = TransformerFactory.newInstance();
        Transformer xformer = xfactory.newTransformer();
        xformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
        Source source = new DOMSource( doc );

        OutputStreamWriter writer = null;
        try
        {
            manifestFile.getParentFile().mkdirs();

            String encoding = doc.getXmlEncoding() != null ? doc.getXmlEncoding() : "UTF-8";
            writer = new OutputStreamWriter( new FileOutputStream( manifestFile, false ), encoding );
            if ( doc.getXmlEncoding() != null && doc.getXmlVersion() != null )
            {
                String xmldecl = String
                    .format( "<?xml version=\"%s\" encoding=\"%s\"?>%n", doc.getXmlVersion(), doc.getXmlEncoding() );
                writer.write( xmldecl );
            }
            Result result = new StreamResult( writer );

            xformer.transform( source, result );
        }
        finally
        {
            IOUtils.closeQuietly( writer );
        }
    }

    /**
     *
     * @param manifestFile
     * @throws IOException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws TransformerException
     * @throws MojoFailureException
     * @throws MojoExecutionException
     */
    public void updateManifest( File manifestFile )
    throws IOException, ParserConfigurationException, SAXException, TransformerException,
    MojoFailureException, MojoExecutionException
    {
        Document doc = readManifest( manifestFile );
        Element manifestElement = doc.getDocumentElement();
        boolean dirty = false;

        if ( StringUtils.isEmpty( parsedVersionName ) )
        {  // default to ${project.version}
            parsedVersionName = project.getVersion();
        }
        Attr versionNameAttrib = manifestElement.getAttributeNode( ATTR_VERSION_NAME );
        if ( versionNameAttrib == null || ! StringUtils.equals( parsedVersionName, versionNameAttrib.getValue() ) )
        {
            getLog().info( "Setting " + ATTR_VERSION_NAME + " to " + parsedVersionName );
            manifestElement.setAttribute( ATTR_VERSION_NAME, parsedVersionName );
            dirty = true;
        }
        if ( ( parsedVersionCodeAutoIncrement && parsedVersionCode != null )
                || ( parsedVersionCodeUpdateFromVersion && parsedVersionCode != null )
                || ( parsedVersionCodeAutoIncrement && parsedVersionCodeUpdateFromVersion ) )
        {
            throw new MojoFailureException( "versionCodeAutoIncrement, versionCodeUpdateFromVersion and versionCode "
                    + "are mutual exclusive. They cannot be specified at the same time. Please specify either "
                    + "versionCodeAutoIncrement, versionCodeUpdateFromVersion or versionCode!" );
        }
        exportProperties();
        if ( parsedVersionCodeAutoIncrement )
        {
            performVersioCodeAutoIncrement( manifestElement );
            dirty = true;
        }
        if ( parsedVersionCodeUpdateFromVersion )
        {
            performVersionCodeUpdateFromVersion( manifestElement );
            dirty = true;
        }
        if ( parsedVersionCode != null )
        {
            Attr versionCodeAttr = manifestElement.getAttributeNode( ATTR_VERSION_CODE );
            int currentVersionCode = 0;
            if ( versionCodeAttr != null )
            {
                currentVersionCode = NumberUtils.toInt( versionCodeAttr.getValue(), 0 );
            }
            if ( currentVersionCode != parsedVersionCode )
            {
                getLog().info( "Setting " + ATTR_VERSION_CODE + " to " + parsedVersionCode );
                manifestElement.setAttribute( ATTR_VERSION_CODE, String.valueOf( parsedVersionCode ) );
                dirty = true;
            }
            project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( parsedVersionCode ) );
        }
        if ( !StringUtils.isEmpty( parsedApplicationIcon ) )
        {
            dirty = updateApplicationAttribute( manifestElement, ATTR_APPLICATION_ICON, parsedApplicationIcon, dirty );
            project.getProperties()
                .setProperty( "android.manifest.applicationIcon", String.valueOf( parsedApplicationIcon ) );
        }

        if ( ! StringUtils.isEmpty( parsedApplicationLabel ) )
        {
            dirty =
                updateApplicationAttribute( manifestElement, ATTR_APPLICATION_LABEL, parsedApplicationLabel, dirty );
            project.getProperties()
                .setProperty( "android.manifest.applicationLabel", String.valueOf( parsedApplicationLabel ) );
        }

        if ( ! StringUtils.isEmpty( parsedApplicationTheme ) )
        {
            dirty =
                updateApplicationAttribute( manifestElement, ATTR_APPLICATION_THEME, parsedApplicationTheme, dirty );
            project.getProperties()
                .setProperty( "android.manifest.applicationTheme", String.valueOf( parsedApplicationTheme ) );
        }

        if ( ! StringUtils.isEmpty( parsedSharedUserId ) )
        {
            Attr sharedUserIdAttrib = manifestElement.getAttributeNode( ATTR_SHARED_USER_ID );

            if ( sharedUserIdAttrib == null || ! StringUtils
                    .equals( parsedSharedUserId, sharedUserIdAttrib.getValue() ) )
            {
                getLog().info( "Setting " + ATTR_SHARED_USER_ID + " to " + parsedSharedUserId );
                manifestElement.setAttribute( ATTR_SHARED_USER_ID, parsedSharedUserId );
                dirty = true;
            }
        }

        if ( parsedDebuggable != null )
        {
            NodeList appElems = manifestElement.getElementsByTagName( ELEM_APPLICATION );

            // Update all application nodes. Not sure whether there will ever be more than one.
            for ( int i = 0; i < appElems.getLength(); ++ i )
            {
                Node node = appElems.item( i );
                getLog().info( "Testing if node " + node.getNodeName() + " is application" );
                if ( node.getNodeType() == Node.ELEMENT_NODE )
                {
                    Element element = ( Element ) node;
                    Attr debuggableAttrib = element.getAttributeNode( ATTR_DEBUGGABLE );
                    if ( debuggableAttrib == null || parsedDebuggable != BooleanUtils
                            .toBoolean( debuggableAttrib.getValue() ) )
                    {
                        getLog().info( "Setting " + ATTR_DEBUGGABLE + " to " + parsedDebuggable );
                        element.setAttribute( ATTR_DEBUGGABLE, String.valueOf( parsedDebuggable ) );
                        dirty = true;
                    }
                }
            }
        }

        if ( parsedSupportsScreens != null )
        {
            boolean madeDirty = performSupportScreenModification( doc, manifestElement );
            if ( madeDirty )
            {
                dirty = true;
            }
        }

        if ( parsedCompatibleScreens != null )
        {
            getLog().info( "Setting " + ELEM_COMPATIBLE_SCREENS );
            updateCompatibleScreens( doc, manifestElement );
            dirty = true;
        }

        dirty = processProviderAuthorities( manifestElement, dirty );
        dirty = processUsesSdk( doc, manifestElement, dirty );

        if ( dirty )
        {
            if ( manifestFile.exists() && ! manifestFile.delete() )
            {
                getLog().warn( "Could not remove old " + manifestFile );
            }
            getLog().info( "Made changes to manifest file, updating " + manifestFile );
            writeManifest( manifestFile, doc );
        }
        else
        {
            getLog().info( "No changes found to write to manifest file" );
        }
    }

    private boolean processProviderAuthorities( Element manifestElement, boolean dirty )
    {
        if ( parsedProviderAuthorities != null )
        {
            boolean madeDirty = updateProviderAuthorities( manifestElement );
            if ( madeDirty )
            {
                dirty = true;
            }
        }
        return dirty;
    }

    private boolean processUsesSdk( Document doc, Element manifestElement, boolean dirty )
    {
        if ( parsedUsesSdk != null )
        {
            boolean madeDirty = performUsesSdkModification( doc, manifestElement );
            if ( madeDirty )
            {
                dirty = true;
            }
        }
        return dirty;
    }

    private boolean updateApplicationAttribute( Element manifestElement,
            String attribute, String value, boolean dirty )
    {
        NodeList appElements =
                manifestElement.getElementsByTagName( ELEM_APPLICATION );
        // Update all application nodes. Not sure whether there will ever be
        // more than one.
        for ( int i = 0; i < appElements.getLength(); ++i )
        {
            Node node = appElements.item( i );
            getLog().info( "Testing if node " + node.getNodeName()
                    + " is application" );
            if ( node.getNodeType() == Node.ELEMENT_NODE )
            {
                Element element = (Element) node;
                Attr labelAttrib = element.getAttributeNode( attribute );
                if ( labelAttrib == null
                        || !value.equals( labelAttrib.getValue() ) )
                {
                    getLog().info( "Setting " + attribute + " to " + value );
                    element.setAttribute( attribute, String.valueOf( value ) );
                    dirty = true;
                }
            }
        }
        return dirty;
    }

    /**
     * Expose the version properties and other simple parsed manifest entries.
     */
    private void exportProperties()
    {
        project.getProperties().setProperty( "android.manifest.versionName", parsedVersionName );
        project.getProperties().setProperty( "android.manifest.versionCodeAutoIncrement",
                String.valueOf( parsedVersionCodeAutoIncrement ) );
        project.getProperties().setProperty( "android.manifest.versionCodeUpdateFromVersion",
                String.valueOf( parsedVersionCodeUpdateFromVersion ) );
        project.getProperties().setProperty( "android.manifest.debuggable", String.valueOf( parsedDebuggable ) );
        if ( parsedSharedUserId != null )
        {
            project.getProperties().setProperty( "android.manifest.sharedUserId", parsedSharedUserId );
        }
    }

    private void performVersioCodeAutoIncrement( Element manifestElement )
    {
        Attr versionCode = manifestElement.getAttributeNode( ATTR_VERSION_CODE );
        int currentVersionCode = 0;
        if ( versionCode != null )
        {
            currentVersionCode = NumberUtils.toInt( versionCode.getValue(), 0 );
        }
        currentVersionCode++;
        manifestElement.setAttribute( ATTR_VERSION_CODE, String.valueOf( currentVersionCode ) );
        project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( currentVersionCode ) );
    }

    /**
     * If the specified version name cannot be properly parsed then fall back to
     * an automatic method.
     * If the version can be parsed then generate a version code from the
     * version components.  In an effort to preseve uniqueness two digits
     * are allowed for both the minor and incremental versions.
     * @throws MojoExecutionException
     */
    private void performVersionCodeUpdateFromVersion( Element manifestElement ) throws MojoExecutionException
    {
        String verString = project.getVersion();
        getLog().debug( "Generating versionCode for " + verString );
        String verCode = generateVersionCodeFromVersionName( verString );
        getLog().info( "Setting " + ATTR_VERSION_CODE + " to " + verCode );
        manifestElement.setAttribute( ATTR_VERSION_CODE, verCode );
        project.getProperties().setProperty( "android.manifest.versionCode", String.valueOf( verCode ) );
    }

    private String generateVersionCodeFromVersionName( String versionName ) throws MojoExecutionException
    {
        // use the new version generator with default settings
        VersionGenerator gen = new VersionGenerator();

        return Integer.toString( gen.generate( versionName ) );
    }

    private boolean performSupportScreenModification( Document doc, Element manifestElement )
    {
        boolean dirty = false;
        Element supportsScreensElem = XmlHelper.getOrCreateElement( doc, manifestElement,
                ELEM_SUPPORTS_SCREENS );

        getLog().info( "Setting " + ELEM_SUPPORTS_SCREENS );

        if ( parsedSupportsScreens.getAnyDensity() != null )
        {
            supportsScreensElem.setAttribute( ATTR_ANY_DENSITY, parsedSupportsScreens.getAnyDensity() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getSmallScreens() != null )
        {
            supportsScreensElem.setAttribute( ATTR_SMALL_SCREENS, parsedSupportsScreens.getSmallScreens() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getNormalScreens() != null )
        {
            supportsScreensElem.setAttribute( ATTR_NORMAL_SCREENS, parsedSupportsScreens.getNormalScreens() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getLargeScreens() != null )
        {
            supportsScreensElem.setAttribute( ATTR_LARGE_SCREENS, parsedSupportsScreens.getLargeScreens() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getXlargeScreens() != null )
        {
            supportsScreensElem.setAttribute( ATTR_XLARGE_SCREENS, parsedSupportsScreens.getXlargeScreens() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getCompatibleWidthLimitDp() != null )
        {
            supportsScreensElem.setAttribute( ATTR_COMPATIBLE_WIDTH_LIMIT_DP,
                    parsedSupportsScreens.getCompatibleWidthLimitDp() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getLargestWidthLimitDp() != null )
        {
            supportsScreensElem
                    .setAttribute( ATTR_LARGEST_WIDTH_LIMIT_DP, parsedSupportsScreens.getLargestWidthLimitDp() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getRequiresSmallestWidthDp() != null )
        {
            supportsScreensElem.setAttribute( ATTR_REQUIRES_SMALLEST_WIDTH_DP,
                    parsedSupportsScreens.getRequiresSmallestWidthDp() );
            dirty = true;
        }
        if ( parsedSupportsScreens.getResizeable() != null )
        {
            supportsScreensElem.setAttribute( ATTR_RESIZEABLE, parsedSupportsScreens.getResizeable() );
            dirty = true;
        }
        return dirty;
    }

    private boolean performUsesSdkModification ( Document doc, Element manifestElement )
    {
        boolean dirty = false;
        Element usesSdkElem = XmlHelper.getOrCreateElement( doc, manifestElement,
                ELEM_USES_SDK );

        if ( parsedUsesSdk.getMinSdkVersion() != null )
        {
            usesSdkElem.setAttribute( ATTR_MIN_SDK_VERSION, parsedUsesSdk.getMinSdkVersion() );
            dirty = true;
        }
        if ( parsedUsesSdk.getMaxSdkVersion() != null )
        {
            usesSdkElem.setAttribute( ATTR_MAX_SDK_VERSION, parsedUsesSdk.getMaxSdkVersion() );
            dirty = true;
        }
        if ( parsedUsesSdk.getTargetSdkVersion() != null )
        {
            usesSdkElem.setAttribute( ATTR_TARGET_SDK_VERSION, parsedUsesSdk.getTargetSdkVersion() );
            dirty = true;
        }

        return dirty;
    }

    private void updateCompatibleScreens( Document doc, Element manifestElement )
    {
        Element compatibleScreensElem = XmlHelper.getOrCreateElement( doc, manifestElement, ELEM_COMPATIBLE_SCREENS );

        // read those screen elements that were already defined in the Manifest
        NodeList manifestScreenElems = compatibleScreensElem.getElementsByTagName( ELEM_SCREEN );
        int numManifestScreens = manifestScreenElems.getLength();
        ArrayList<CompatibleScreen> manifestScreens = new ArrayList<CompatibleScreen>( numManifestScreens );
        for ( int i = 0; i < numManifestScreens; i++ )
        {
            Element screenElem = ( Element ) manifestScreenElems.item( i );

            CompatibleScreen screen = new CompatibleScreen();
            screen.setScreenDensity( screenElem.getAttribute( ATTR_SCREEN_DENSITY ) );
            screen.setScreenSize( screenElem.getAttribute( ATTR_SCREEN_SIZE ) );

            manifestScreens.add( screen );
            getLog().debug( "Found Manifest compatible-screen: " + screen );
        }

        // remove all child nodes, since we'll rebuild the element
        XmlHelper.removeDirectChildren( compatibleScreensElem );

        for ( CompatibleScreen screen : parsedCompatibleScreens )
        {
            getLog().debug( "Found POM compatible-screen: " + screen );
        }

        // merge those screens defined in the POM, overriding any matching screens
        // already defined in the Manifest
        HashSet<CompatibleScreen> mergedScreens = new HashSet<CompatibleScreen>();
        mergedScreens.addAll( manifestScreens );
        mergedScreens.addAll( parsedCompatibleScreens );

        for ( CompatibleScreen screen : mergedScreens )
        {
            getLog().debug( "Using compatible-screen: " + screen );
            Element screenElem = doc.createElement( ELEM_SCREEN );
            screenElem.setAttribute( ATTR_SCREEN_SIZE, screen.getScreenSize() );
            screenElem.setAttribute( ATTR_SCREEN_DENSITY, screen.getScreenDensity() );

            compatibleScreensElem.appendChild( screenElem );
        }
    }

    private boolean updateProviderAuthorities( Element manifestElement )
    {
        boolean dirty = false;
        NodeList appElems = manifestElement.getElementsByTagName( ELEM_APPLICATION );

        // Update all application nodes. Not sure whether there will ever be more than one.
        for ( int i = 0; i < appElems.getLength(); ++ i )
        {
            Node node = appElems.item( i );
            if ( node.getNodeType() == Node.ELEMENT_NODE )
            {
                NodeList providerElems = manifestElement.getElementsByTagName( ELEM_PROVIDER );
                for ( int j = 0; j < providerElems.getLength(); ++ j )
                {
                    Node providerNode = providerElems.item( j );
                    if ( providerNode.getNodeType() == Node.ELEMENT_NODE )
                    {
                        Element providerElem = (Element) providerNode;
                        Attr providerName = providerElem.getAttributeNode( ATTR_NAME );
                        getLog().debug( "Checking provider " + providerName.getValue() );
                        if ( shouldPerformProviderUpdate( providerName ) )
                        {
                            dirty = true;
                            String name = providerName.getValue();
                            String newAuthorities = parsedProviderAuthorities.getProperty( name );
                            getLog().info( "Updating provider " + name + " authorities attr to " + newAuthorities );
                            performProviderUpdate( providerElem, newAuthorities );
                        }
                    }
                }
            }
        }

        return dirty;
    }

    private boolean shouldPerformProviderUpdate( Attr providerName )
    {
        if ( providerName == null )
        {
            return false;
        }

        for ( String propName: parsedProviderAuthorities.stringPropertyNames() )
        {
            if ( propName.equals( providerName.getValue() ) )
            {
                return true;
            }
        }
        return false;
    }

    private void performProviderUpdate( Element providerElem, String newAuthorities )
    {
        Attr providerAuthorities = providerElem.getAttributeNode( ATTR_AUTHORITIES );
        providerAuthorities.setValue( newAuthorities );
    }

}