/*
 * Copyright (C) 2009, 2010 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.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.simpligility.maven.plugins.android.common.DeviceHelper;
import com.simpligility.maven.plugins.android.configuration.Emulator;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * AbstractEmulatorMojo contains all code related to the interaction with the Android emulator. At this stage that is
 * starting and stopping the emulator.
 *
 * @author Manfred Moser - manfred@simpligility.com
 * @author Bryan O'Neil - bryan.oneil@hotmail.com
 * @see com.simpligility.maven.plugins.android.configuration.Emulator
 * @see com.simpligility.maven.plugins.android.standalonemojos.EmulatorStartMojo
 * @see com.simpligility.maven.plugins.android.standalonemojos.EmulatorStopMojo
 * @see com.simpligility.maven.plugins.android.standalonemojos.EmulatorStopAllMojo
 */
public abstract class AbstractEmulatorMojo extends AbstractAndroidMojo
{
    /**
     * operating system name.
     */
    public static final String OS_NAME = System.getProperty( "os.name" ).toLowerCase( Locale.US );
    private static final int MILLIS_TO_SLEEP_BETWEEN_DEVICE_ONLINE_CHECKS = 200;

    /**
     * Even if the device finished booting, there are usually still some things going on in the background,
     * polling at a higher frequency (un-cached!) is most probably useless
     */
    private static final int MILLIS_TO_SLEEP_BETWEEN_SYS_BOOTED_CHECKS = 5000;

    /**
     * Names of device properties related to the boot state
     */
    private static final String[] BOOT_INDICATOR_PROP_NAMES
            = { "dev.bootcomplete", "sys.boot_completed", "init.svc.bootanim" };

    /**
     * Target values for properties listed in {@link #BOOT_INDICATOR_PROP_NAMES}, which indicate 'boot completed'
     */
    private static final String[] BOOT_INDICATOR_PROP_TARGET_VALUES = { "1", "1", "stopped" };

    /**
     * Determines, which of the properties listed in {@link #BOOT_INDICATOR_PROP_NAMES} are required
     * to reach the target value in {@link #BOOT_INDICATOR_PROP_TARGET_VALUES} in order to stop polling.
     * Since one cannot be picky about what is used as a 'booted' indicator, any 'signalled' property will
     * be used as an indicator in case of a timeout.
     */
    private static final boolean[] BOOT_INDICATOR_PROP_WAIT_FOR = { false, false, true };

    /**
     * Warning threshold for narrow timeout values
     * TODO Improve; e.g. with an additional percentage threshold
     */
    private static final long START_TIMEOUT_REMAINING_TIME_WARNING_THRESHOLD = 5000; //[ms]

    /**
     * Configuration for the emulator goals. Either use the plugin configuration like this
     *

     * <emulator>
     *   <avd>Default</avd>
     *   <wait>20000</wait>
     *   <options>-no-skin</options>
     *   <executable>emulator-arm</executable>
     *   <location>C:/SDK/emulator</location>
     * </emulator>
     *

     * or configure as properties  on the command line as android.emulator.avd, android.emulator.wait,
     * android.emulator.options and android.emulator.executable or in pom or settings file as emulator.avd,
     * emulator.wait and emulator.options.
     */
    @Parameter
    private Emulator emulator;

    /**
     * Name of the Android Virtual Device (emulatorAvd) that will be started by the emulator. Default value is "Default"
     *
     * @see com.simpligility.maven.plugins.android.configuration.Emulator#avd
     */
    @Parameter( property = "android.emulator.avd" )
    private String emulatorAvd;

    /**
     * Unlock the emulator after it is started.
     */
    @Parameter( property = "android.emulatorUnlock", defaultValue = "false" )
    private boolean emulatorUnlock;

    /**
     * Wait time for the emulator start up.
     *
     * @see com.simpligility.maven.plugins.android.configuration.Emulator#wait
     */
    @Parameter( property = "android.emulator.wait" )
    private String emulatorWait;

    /**
     * Additional command line options for the emulator start up. This option can be used to pass any additional
     * options desired to the invocation of the emulator. Use emulator -help for more details. An example would be
     * "-no-skin".
     *
     * @see com.simpligility.maven.plugins.android.configuration.Emulator#options
     */
    @Parameter( property = "android.emulator.options" )
    private String emulatorOptions;

    /**
     * Override default emulator executable. Default uses just "emulator".
     *
     * @see com.simpligility.maven.plugins.android.configuration.Emulator#executable
     */
    @Parameter( property = "android.emulator.executable" )
    private String emulatorExecutable;

    /**
     * Override default path to emulator folder.
     */
    @Parameter( property = "android.emulator.location" )
    private String emulatorLocation;

    /**
     * parsed value for avd that will be used for the invocation.
     */
    private String parsedAvd;

    /**
     * parsed value for options that will be used for the invocation.
     */
    private String parsedOptions;

    /**
     * parsed value for wait that will be used for the invocation.
     */
    private String parsedWait;

    private String parsedExecutable;

    /**
     * parsed value for location that will be used for the invocation.
     */
    private String parsedEmulatorLocation;

    private static final String START_EMULATOR_MSG = "Starting android emulator with script: ";
    private static final String START_EMULATOR_WAIT_MSG = "Waiting for emulator start:";

    /**
     * Folder that contains the startup script and the pid file.
     */
    private static final String SCRIPT_FOLDER = System.getProperty( "java.io.tmpdir" );

    /**
     * Are we running on a flavour of Windows.
     *
     * @return
     */
    private boolean isWindows()
    {
        boolean result;
        if ( OS_NAME.toLowerCase().contains( "windows" ) )
        {
            result = true;
        }
        else
        {
            result = false;
        }
        getLog().debug( "isWindows: " + result );
        return result;
    }

    /**
     * Start the Android Emulator with the specified options.
     *
     * @throws org.apache.maven.plugin.MojoExecutionException
     *
     * @see #emulatorAvd
     * @see #emulatorWait
     * @see #emulatorOptions
     */
    protected void startAndroidEmulator() throws MojoExecutionException
    {
        parseParameters();

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

        try
        {
            String filename;
            if ( isWindows() )
            {
                filename = writeEmulatorStartScriptWindows();
            }
            else
            {
                filename = writeEmulatorStartScriptUnix();
            }

            final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
            if ( androidDebugBridge.isConnected() )
            {
                waitForInitialDeviceList( androidDebugBridge );
                List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
                int numberOfDevices = devices.size();
                getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );

                IDevice existingEmulator = findExistingEmulator( devices );
                if ( existingEmulator == null )
                {
                    getLog().info( START_EMULATOR_MSG + filename );
                    executor.executeCommand( filename, null );

                    getLog().info( START_EMULATOR_WAIT_MSG + parsedWait );
                    // wait for the emulator to start up
                    boolean booted = waitUntilDeviceIsBootedOrTimeout( androidDebugBridge );
                    if ( booted )
                    {
                        getLog().info( "Emulator is up and running." );
                        unlockEmulator( androidDebugBridge );
                    }
                    else
                    {
                        throw new MojoExecutionException( "Timeout while waiting for emulator to startup." );
                    }

                }
                else
                {
                    getLog().info( String.format(
                            "Emulator already running [Serial No: '%s', AVD Name '%s']. " + "Skipping start and wait.",
                            existingEmulator.getSerialNumber(), existingEmulator.getAvdName() ) );
                }
            }
        }
        catch ( Exception e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

    /**
     * Unlocks the emulator.
     * @param androidDebugBridge
     */
    void unlockEmulator( AndroidDebugBridge androidDebugBridge )
    {
        if ( emulatorUnlock )
        {
            IDevice myEmulator = findExistingEmulator( Arrays.asList( androidDebugBridge.getDevices() ) );
            int devicePort = extractPortFromDevice( myEmulator );
            if ( devicePort == -1 )
            {
                getLog().info( "Unable to retrieve port to unlock emulator "
                        + DeviceHelper.getDescriptiveName( myEmulator ) );
            }
            else
            {
                getLog().info( "Unlocking emulator "
                        + DeviceHelper.getDescriptiveName( myEmulator ) );

                sendEmulatorCommand( devicePort,
                        "event send EV_KEY:KEY_SOFT1:1" );
                sendEmulatorCommand( devicePort,
                        "event send EV_KEY:KEY_SOFT1:0" );
                sendEmulatorCommand( devicePort,
                        "event send EV_KEY:KEY_SOFT1:1" );
                sendEmulatorCommand( devicePort,
                        "event send EV_KEY:KEY_SOFT1:0" );
            }
        }
    }

    // TODO Separate timeout params?: New param 'android.emulator.bootTimeout', rename param 'android.emulator.wait' to 'android.emulator.connectTimeout'
    // TODO Higher default timeout(s)?: Perhaps at least for emulators, since they are probably booted or even created on demand
    boolean waitUntilDeviceIsBootedOrTimeout( AndroidDebugBridge androidDebugBridge )
            throws MojoExecutionException
    {
        final long timeout = System.currentTimeMillis() + Long.parseLong( parsedWait );
        IDevice myEmulator;
        boolean devOnline;
        boolean sysBootCompleted = false;
        long remainingTime = 0;

        //If necessary, wait until the device is online or the specified timeout is reached
        boolean waitingForConnection = false;
        do
        {
            myEmulator = findExistingEmulator( Arrays.asList( androidDebugBridge.getDevices() ) );
            devOnline = ( myEmulator != null ) && ( myEmulator.isOnline() );
            if ( devOnline )
            {
                break;
            }
            else
            {
                myEmulator = null;
            }

            if ( !waitingForConnection )
            {
                waitingForConnection = true;
                getLog().info( "Waiting for the device to go online..." );
            }
            try
            {
                Thread.sleep( MILLIS_TO_SLEEP_BETWEEN_DEVICE_ONLINE_CHECKS );
            }
            catch ( InterruptedException e )
            {
                throw new MojoExecutionException( "Interrupted waiting for device to become ready" );
            }

            remainingTime = timeout - System.currentTimeMillis();
        } while ( remainingTime > 0 );

        if ( devOnline )
        {
            boolean waitingForBootCompleted = false;
            final String[] bootIndicatorPropValues = new String[ BOOT_INDICATOR_PROP_NAMES.length ];
            boolean anyTargetStateReached = false;
            boolean requiredTargetStatesReached = false;

            // If necessary, wait until the device's system is booted or the specified timeout is reached
            do
            {
                try
                {
                    // update state flags...
                    anyTargetStateReached = false;
                    requiredTargetStatesReached = true;

                    for ( int indicatorProp = 0; indicatorProp < BOOT_INDICATOR_PROP_NAMES.length; ++indicatorProp )
                    {
                        // issue an un-cached property request
                        boolean targetStateReached =
                                (
                                        bootIndicatorPropValues[indicatorProp] != null
                                                &&  bootIndicatorPropValues[indicatorProp]
                                                .equals( BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp] )
                                );
                        if ( !targetStateReached )
                        {
                            // (re)query
                            bootIndicatorPropValues[indicatorProp] =
                                    myEmulator.getPropertySync( BOOT_INDICATOR_PROP_NAMES[indicatorProp] );
                            targetStateReached =
                                    (
                                            bootIndicatorPropValues[indicatorProp] != null
                                                    &&  bootIndicatorPropValues[indicatorProp]
                                                    .equals( BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp] )
                                    );
                        }
                        anyTargetStateReached |= targetStateReached;
                        requiredTargetStatesReached &=
                                BOOT_INDICATOR_PROP_WAIT_FOR[indicatorProp] ? targetStateReached : true;

                        getLog().debug( BOOT_INDICATOR_PROP_NAMES[indicatorProp]
                                        + " : " +  bootIndicatorPropValues[indicatorProp]
                                        + ( targetStateReached ? " == " : " != " )
                                        + BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp]
                                        + " [" + ( targetStateReached ? "OK" : "PENDING" ) + ']'
                        );
                    }
                }
                catch ( TimeoutException e )
                {
                    // TODO Abort here? Not too problematic since timeouts are used
                    // optimistically ignore this exception and continue...
                }
                catch ( AdbCommandRejectedException e )
                {
                    // TODO Abort here? Not too problematic since timeouts are used
                    // optimistically ignore this exception and continue...
                }
                catch ( ShellCommandUnresponsiveException e )
                {
                    // TODO Abort here? Not too problematic since timeouts are used
                    // optimistically ignore this exception and continue...
                }
                catch ( IOException e )
                {
                    throw new MojoExecutionException( "IO error during status request" , e );
                }

                remainingTime = timeout - System.currentTimeMillis();

                if ( remainingTime > 0 )
                {
                    // consider the boot process to be finished, if all required states have been reached
                    sysBootCompleted = requiredTargetStatesReached;
                }
                else
                {
                    // on timeout, use any indicator
                    sysBootCompleted = anyTargetStateReached;
                }

                if ( remainingTime > 0 && !sysBootCompleted )
                {
                    if ( !waitingForBootCompleted )
                    {
                        waitingForBootCompleted = true;
                        getLog().info( "Waiting for the device to finish booting..." );
                    }

                    try
                    {
                        Thread.sleep( MILLIS_TO_SLEEP_BETWEEN_SYS_BOOTED_CHECKS );
                    }
                    catch ( InterruptedException e )
                    {
                        throw new MojoExecutionException(
                                "Interrupted while waiting for the device to finish booting" );
                    }
                }
            } while ( !sysBootCompleted && remainingTime > 0 );
            if ( sysBootCompleted && remainingTime < START_TIMEOUT_REMAINING_TIME_WARNING_THRESHOLD )
            {
                getLog().warn(
                        "Boot indicators have been signalled, but remaining time was " + remainingTime + " ms" );
            }
        }
        return sysBootCompleted;
    }

    private IDevice findExistingEmulator( List<IDevice> devices )
    {
        IDevice existingEmulator = null;

        for ( IDevice device : devices )
        {
            if ( device.isEmulator() )
            {
                if ( isExistingEmulator( device ) )
                {
                    existingEmulator = device;
                    break;
                }
            }
        }
        return existingEmulator;
    }

    /**
     * Checks whether the given device has the same AVD name as the device which the current command
     * is related to. true returned if the device AVD names are identical (independent of case)
     * and false if the device AVD names are different.
     *
     * @param device The device to check
     * @return Boolean results of the check
     */
    private boolean isExistingEmulator( IDevice device )
    {
        return ( ( device.getAvdName() != null ) && ( device.getAvdName().equalsIgnoreCase( parsedAvd ) ) );
    }

    /**
     * Writes the script to start the emulator in the background for windows based environments.
     *
     * @return absolute path name of start script
     * @throws IOException
     * @throws MojoExecutionException
     */
    private String writeEmulatorStartScriptWindows() throws MojoExecutionException
    {

        String filename = SCRIPT_FOLDER + "\\android-maven-plugin-emulator-start.vbs";

        File file = new File( filename );
        PrintWriter writer = null;
        try
        {
            writer = new PrintWriter( new FileWriter( file ) );

            // command needs to be assembled before unique window title since it parses settings and sets up parsedAvd
            // and others.
            String command = assembleStartCommandLine();
            String uniqueWindowTitle = "AndroidMavenPlugin-AVD" + parsedAvd;
            writer.println( "Dim oShell" );
            writer.println( "Set oShell = WScript.CreateObject(\"WScript.shell\")" );
            String cmdPath = System.getenv( "COMSPEC" );
            if ( cmdPath == null )
            {
                cmdPath = "cmd.exe";
            }
            String cmd = cmdPath + " /X /C START /SEPARATE \"\"" + uniqueWindowTitle + "\"\"  " + command.trim();
            writer.println( "oShell.run \"" + cmd + "\"" );
        }
        catch ( IOException e )
        {
            getLog().error( "Failure writing file " + filename );
        }
        finally
        {
            if ( writer != null )
            {
                writer.flush();
                writer.close();
            }
        }
        file.setExecutable( true );
        return filename;
    }

    /**
     * Writes the script to start the emulator in the background for unix based environments.
     *
     * @return absolute path name of start script
     * @throws IOException
     * @throws MojoExecutionException
     */
    private String writeEmulatorStartScriptUnix() throws MojoExecutionException
    {
        String filename = SCRIPT_FOLDER + "/android-maven-plugin-emulator-start.sh";

        File sh;
        sh = new File( "/bin/bash" );
        if ( !sh.exists() )
        {
            sh = new File( "/usr/bin/bash" );
        }
        if ( !sh.exists() )
        {
            sh = new File( "/bin/sh" );
        }

        File file = new File( filename );
        PrintWriter writer = null;
        try
        {
            writer = new PrintWriter( new FileWriter( file ) );
            writer.println( "#!" + sh.getAbsolutePath() );
            writer.print( assembleStartCommandLine() );
            writer.print( " 1>/dev/null 2>&1 &" ); // redirect outputs and run as background task
        }
        catch ( IOException e )
        {
            getLog().error( "Failure writing file " + filename );
        }
        finally
        {
            if ( writer != null )
            {
                writer.flush();
                writer.close();
            }
        }
        file.setExecutable( true );
        return filename;
    }

    /**
     * Stop the running Android Emulator.
     *
     * @throws org.apache.maven.plugin.MojoExecutionException
     *
     */
    protected void stopAndroidEmulator() throws MojoExecutionException
    {
        parseParameters();

        final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
        if ( androidDebugBridge.isConnected() )
        {
            List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
            int numberOfDevices = devices.size();
            getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );

            for ( IDevice device : devices )
            {
                if ( device.isEmulator() )
                {
                    if ( isExistingEmulator( device ) )
                    {
                        stopEmulator( device );
                    }
                }
                else
                {
                    getLog().info( "Skipping stop. Not an emulator. " + DeviceHelper.getDescriptiveName( device ) );
                }
            }
        }
    }

    /**
     * Stop the running Android Emulators.
     *
     * @throws org.apache.maven.plugin.MojoExecutionException
     *
     */
    protected void stopAndroidEmulators() throws MojoExecutionException
    {
        final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
        if ( androidDebugBridge.isConnected() )
        {
            List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
            int numberOfDevices = devices.size();
            getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );

            for ( IDevice device : devices )
            {
                if ( device.isEmulator() )
                {
                    stopEmulator( device );
                }
                else
                {
                    getLog().info( "Skipping stop. Not an emulator. " + DeviceHelper.getDescriptiveName( device ) );
                }
            }
        }
    }

    /**
     * This method contains the code required to stop an emulator
     *
     * @param device The device to stop
     */
    private void stopEmulator( IDevice device )
    {
        int devicePort = extractPortFromDevice( device );
        if ( devicePort == -1 )
        {
            getLog().info( "Unable to retrieve port to stop emulator " + DeviceHelper.getDescriptiveName( device ) );
        }
        else
        {
            getLog().info( "Stopping emulator " + DeviceHelper.getDescriptiveName( device ) );

            sendEmulatorCommand( devicePort, "avd stop" );
            boolean killed = sendEmulatorCommand( devicePort, "kill" );
            if ( !killed )
            {
                getLog().info( "Emulator failed to stop " + DeviceHelper.getDescriptiveName( device ) );
            }
            else
            {
                getLog().info( "Emulator stopped successfully " + DeviceHelper.getDescriptiveName( device ) );
            }
        }
    }

    /**
     * This method extracts a port number from the serial number of a device.
     * It assumes that the device name is of format [xxxx-nnnn] where nnnn is the
     * port number.
     *
     * @param device The device to extract the port number from.
     * @return Returns the port number of the device
     */
    private int extractPortFromDevice( IDevice device )
    {
        String portStr = StringUtils.substringAfterLast( device.getSerialNumber(), "-" );
        if ( StringUtils.isNotBlank( portStr ) && StringUtils.isNumeric( portStr ) )
        {
            return Integer.parseInt( portStr );
        }

        //If the port is not available then return -1
        return -1;
    }

    /**
     * Sends a user command to the running emulator via its telnet interface.
     *
     * @param port    The emulator's telnet port.
     * @param command The command to execute on the emulator's telnet interface.
     * @return Whether sending the command succeeded.
     */
    private boolean sendEmulatorCommand(
            //final Launcher launcher,
            //final PrintStream logger,
            final int port, final String command )
    {
        Callable<Boolean> task = new Callable<Boolean>()
        {
            public Boolean call() throws IOException
            {
                Socket socket = null;
                BufferedReader in = null;
                PrintWriter out = null;
                try
                {
                    socket = new Socket( "127.0.0.1", port );
                    out = new PrintWriter( socket.getOutputStream(), true );
                    in = new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
                    if ( in.readLine() == null )
                    {
                        return false;
                    }

                    out.write( command );
                    out.write( "\r\n" );
                }
                finally
                {
                    try
                    {
                        out.close();
                        in.close();
                        socket.close();
                    }
                    catch ( Exception e )
                    {
                        // Do nothing
                    }
                }

                return true;
            }

            private static final long serialVersionUID = 1L;
        };

        boolean result = false;
        try
        {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<Boolean> future = executor.submit( task );
            result = future.get();
        }
        catch ( Exception e )
        {
            getLog().error( String.format( "Failed to execute emulator command '%s': %s", command, e ) );
        }

        return result;
    }

    /**
     * Assemble the command line for starting the emulator based on the parameters supplied in the pom file and on the
     * command line. It should not be that painful to do work with command line and pom supplied values but evidently
     * it is.
     *
     * @return
     * @throws MojoExecutionException
     * @see com.simpligility.maven.plugins.android.configuration.Emulator
     */
    private String assembleStartCommandLine() throws MojoExecutionException
    {
        String emulatorPath;
        if ( !"SdkTools".equals( parsedEmulatorLocation ) )
        {
            emulatorPath = new File( parsedEmulatorLocation, parsedExecutable ).getAbsolutePath();
        }
        else
        {
            emulatorPath = new File( getAndroidSdk().getToolsPath(), parsedExecutable ).getAbsolutePath();
        }
        StringBuilder startCommandline = new StringBuilder( "\"\"" ).append( emulatorPath ).append( "\"\"" )
                .append( " -avd " ).append( parsedAvd ).append( " " );
        if ( !StringUtils.isEmpty( parsedOptions ) )
        {
            startCommandline.append( parsedOptions );
        }
        getLog().info( "Android emulator command: " + startCommandline );
        return startCommandline.toString();
    }

    private void parseParameters()
    {
        // <emulator> exist in pom file
        if ( emulator != null )
        {
            // <emulator><avd> exists in pom file
            if ( emulator.getAvd() != null )
            {
                parsedAvd = emulator.getAvd();
            }
            else
                {
                parsedAvd = determineAvd();
            }
            // <emulator><options> exists in pom file
            if ( emulator.getOptions() != null )
            {
                parsedOptions = emulator.getOptions();
            }
            else
                {
                parsedOptions = determineOptions();
            }
            // <emulator><wait> exists in pom file
            if ( emulator.getWait() != null )
            {
                parsedWait = emulator.getWait();
            }
            else
                {
                parsedWait = determineWait();
            }
            // <emulator><emulatorExecutable> exists in pom file
            if ( emulator.getExecutable() != null )
            {
                parsedExecutable = emulator.getExecutable();
            }
            else
                {
                parsedExecutable = determineExecutable();
            }
            // <emulator><location> exists in pom file
            if ( emulator.getLocation() != null )
            {
                parsedEmulatorLocation = emulator.getLocation();
            }
            else
            {
            parsedEmulatorLocation = determineEmulatorLocation();
            }
        }
        // commandline options
        else
        {
            parsedAvd = determineAvd();
            parsedOptions = determineOptions();
            parsedWait = determineWait();
            parsedExecutable = determineExecutable();
            parsedEmulatorLocation = determineEmulatorLocation();
        }
    }

    /**
     * Get executable value for emulator from command line options or default to "emulator".
     *
     * @return
     */
    private String determineExecutable()
    {
        String emulator;
        if ( emulatorExecutable != null )
        {
            emulator = emulatorExecutable;
        }
        else
        {
            emulator = "emulator";
        }
        return emulator;
    }

    /**
     * Get wait value for emulator from command line option.
     *
     * @return if available return command line value otherwise return default value (5000).
     */
    String determineWait()
    {
        String wait;
        if ( emulatorWait != null )
        {
            wait = emulatorWait;
        }
        else
        {
            wait = "5000";
        }
        return wait;
    }

    /**
     * Get options value for emulator from command line option.
     *
     * @return if available return command line value otherwise return default value ("").
     */
    private String determineOptions()
    {
        String options;
        if ( emulatorOptions != null )
        {
            options = emulatorOptions;
        }
        else
        {
            options = "";
        }
        return options;
    }

    /**
     * Get avd value for emulator from command line option.
     *
     * @return if available return command line value otherwise return default value ("Default").
     */
    String determineAvd()
    {
        String avd;
        if ( emulatorAvd != null )
        {
            avd = emulatorAvd;
        }
        else
        {
            avd = "Default";
        }
        return avd;
    }

    /**
     * Get location value for emulator from command line option.
     *
     * @return if available return command line value otherwise return default value ("SdkTools").
     */
    String determineEmulatorLocation()
    {
        String location;
        if ( emulatorLocation != null )
        {
            location = emulatorLocation;
        }
        else
        {
            location = "SdkTools";
        }
        return location;
    }

}


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

import org.apache.commons.io.IOUtils;
import org.codehaus.plexus.util.IOUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Helper class to deal with jar files.
 *
 * @author Johan Lindquist
 */
public class JarHelper
{

    /**
     * Listener for jar extraction.
     */
    public interface UnjarListener
    {
       boolean include( JarEntry jarEntry );
    }

    /**
     * Unjars the specified jar file into the the specified directory
     *
     * @param jarFile
     * @param outputDirectory
     * @param unjarListener
     * @throws IOException
     */
    public static void unjar( JarFile jarFile, File outputDirectory, UnjarListener unjarListener ) throws IOException
    {
        for ( Enumeration en = jarFile.entries(); en.hasMoreElements(); )
        {
            JarEntry entry = ( JarEntry ) en.nextElement();
            File entryFile = new File( outputDirectory, entry.getName() );
            if ( unjarListener.include( entry ) )
            {
                // Create the output directory if need be
                if ( ! entryFile.getParentFile().exists() )
                {
                    if ( ! entryFile.getParentFile().mkdirs() )
                    {
                        throw new IOException( "Error creating output directory: " + entryFile.getParentFile() );
                    }
                }

                // If the entry is an actual file, unzip that too
                if ( ! entry.isDirectory() )
                {
                    final InputStream in = jarFile.getInputStream( entry );
                    try
                    {
                        final OutputStream out = new FileOutputStream( entryFile );
                        try
                        {
                            IOUtil.copy( in, out );
                        }
                        finally
                        {
                            IOUtils.closeQuietly( out );
                        }
                    }
                    finally
                    {
                        IOUtils.closeQuietly( in );
                    }
                }
            }
        }
    }

}


/*
 * Copyright (C) 2007-2008 JVending Masa
 * Copyright (C) 2011 Jayway AB
 *
 * 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.IDevice;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.SyncService;
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.common.LogSyncProgressMonitor;
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.Push;

import org.apache.commons.io.FileUtils;
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 java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Copy file to all the attached (or specified) devices/emulators.
 *
 * @author Manfred Moser - manfred@simpligility.com
 */
@SuppressWarnings( "unused" )
@Mojo( name = "push", requiresProject = false )
public class PushMojo extends AbstractAndroidMojo
{

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

     *

     * <push>
     *     <source>path</source>
     *     <destination>path</destination>
     * </push>
     *

     *
The parameters can also be configured as property in the pom or settings file
     *

     * <properties>
     *     <android.push.source>pathondevice</android.push.source>
     *     <android.push.destination>path</android.push.destination>
     * </properties>
     *

     * or from command-line with parameter
     * -Dandroid.push.source=path
     * and
     * -Dandroid.push.destination=path.

     */
    @Parameter
    @ConfigPojo
    private Push push;

    /**
     * The file name of the local filesystem file to push to the emulator or
     * device either as absolute path or relative to the execution folder.
     *
     * If you specify a directory, all containing files will be pushed recursively.
     */
    @Parameter( property = "android.push.source" )
    private String pushSource;

    @PullParameter( required = true )
    private String parsedSource;

    /**
     * The destination file name as absolute path on the emulator or device.
     * If the last character is a "/" it will be assumed that the original
     * base filename should be preserved and a target directory is specified.
     * This works analogous if the source is a directory.
     */
    @Parameter( property = "android.push.destination" )
    private String pushDestination;

    @PullParameter( required = true )
    private String parsedDestination;

    public void execute() throws MojoExecutionException, MojoFailureException
    {

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

        final Map<String, String> sourceDestinationMap = calculateSourceDestinationMapping();

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

                // message will be set in for each loop according to the processed files
                String message = "";

                try
                {
                    SyncService syncService = device.getSyncService();

                    for ( Map.Entry<String, String> pushFileEntry : sourceDestinationMap.entrySet() )
                    {
                        String sourcePath = pushFileEntry.getKey();
                        String destinationPath = pushFileEntry.getValue();

                        message = deviceLogLinePrefix + "Push of " + sourcePath + " to " + destinationPath + " on "
                                + DeviceHelper.getDescriptiveName( device );

                        syncService.pushFile( sourcePath, destinationPath, new LogSyncProgressMonitor( getLog() ) );

                        getLog().info( message + " successful." );
                    }
                }
                catch ( SyncException e )
                {
                    throw new MojoExecutionException( message + " failed.", e );
                }
                catch ( IOException e )
                {
                    throw new MojoExecutionException( message + " failed.", e );
                }
                catch ( TimeoutException e )
                {
                    throw new MojoExecutionException( message + " failed.", e );
                }
                catch ( AdbCommandRejectedException e )
                {
                    throw new MojoExecutionException( message + " failed.", e );
                }
            }
        } );
    }

    /**
     * Calculates a map which contains all files to be pushed to the device or
     * emulator. The source filename works as the key while the value is the
     * destination.
     *
     * @return a map with file source -> destination pairs
     * @throws MojoExecutionException
     */
    private Map<String, String> calculateSourceDestinationMapping() throws MojoExecutionException
    {
        Map<String, String> result = new HashMap<String, String>();

        File sourceFile = new File( parsedSource );
        final String destinationPath;
        if ( parsedDestination.endsWith( "/" ) )
        {
            destinationPath = parsedDestination + sourceFile.getName();
        }
        else
        {
            destinationPath = parsedDestination;
        }

        if ( sourceFile.isFile() )
        {
            // only put the source in
            final String sourcePath = sourceFile.getAbsolutePath();
            result.put( sourcePath, destinationPath );
        }
        else
        {
            if ( sourceFile.isDirectory() )
            {
                // find recursively all files to be pushed
                @SuppressWarnings( "unchecked" ) Collection<File> filesList = FileUtils
                        .listFiles( sourceFile, null, true );
                for ( File file : filesList )
                {
                    // make the file's path relative - this is kind of a hack but it
                    // works just fine in this controlled environment
                    String filePath = file.getAbsolutePath().substring( sourceFile.getAbsolutePath().length() );
                    filePath = filePath.replace( System.getProperty( "file.separator" ), "/" );
                    result.put( file.getAbsolutePath(), destinationPath + filePath );
                }
            }
            else
            {
                throw new MojoExecutionException( "Cannot execute push goal: File or directory "
                        + sourceFile.getAbsolutePath() + " does not exist." );
            }
        }
        return result;
    }
}


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

import org.apache.maven.artifact.Artifact;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.codehaus.plexus.logging.Logger;

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

/**
 * Filters a graph of Artifacts and transforms it into a Set.
 */
final class DependencyCollector
{
    private final Logger log;
    private final Set<Artifact> dependencies = new HashSet<Artifact>();
    private final Artifact target;

    /**
     * @param logger    Logger on which to output and messages.
     * @param target    Artifact from which we will start collecting.
     */
    DependencyCollector( Logger logger, Artifact target )
    {
        this.log = logger;
        this.target = target;
    }

    /**
     * Visits all nodes from the given node and collects dependencies.
     *
     * @param node          DependencyNode from which to search.
     * @param collecting    Whether we are currently collecting artifacts.
     */
    public void visit( DependencyNode node, boolean collecting )
    {
        if ( collecting )
        {
            dependencies.add( node.getArtifact() );
        }

        if ( matchesTarget( node.getArtifact() ) )
        {
            collecting = true;
            log.debug( "Found target. Collecting dependencies after " + node.getArtifact() );
        }

        for ( final DependencyNode child : node.getChildren() )
        {
            visit( child, collecting );
        }
    }

    public Set<Artifact> getDependencies()
    {
        return Collections.unmodifiableSet( dependencies );
    }

    private boolean matchesTarget( Artifact found )
    {
        return found.getGroupId().equals( target.getGroupId() )
                && found.getArtifactId().equals( target.getArtifactId() )
                && found.getVersion().equals( target.getVersion() )
                && found.getType().equals( target.getType() )
                && classifierMatch( found.getClassifier(), target.getClassifier() )
                ;
    }

    private boolean classifierMatch( String classifierA, String classifierB )
    {
        final boolean hasClassifierA = !isNullOrEmpty( classifierA );
        final boolean hasClassifierB = !isNullOrEmpty( classifierB );
        if ( !hasClassifierA && !hasClassifierB )
        {
            return true;
        }
        else if ( hasClassifierA && hasClassifierB )
        {
            return classifierA.equals( classifierB );
        }
        return false;
    }

    private boolean isNullOrEmpty( String string )
    {
        return ( string == null ) || string.isEmpty();
    }
}


/*******************************************************************************
 * Copyright (c) 2008, 2011 Sonatype Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Sonatype Inc. - initial API and implementation
 *******************************************************************************/
package com.simpligility.maven.plugins.android.common;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.plexus.logging.Logger;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * Provides convenient functions for resolving artifacts.
 */
public final class ArtifactResolverHelper
{
    /**
     * Which dependency scopes should be excluded when packing dependencies into the apk.
     */
    public static final List<String> EXCLUDE_NON_PACKAGED_SCOPES = Arrays.asList(
            Artifact.SCOPE_PROVIDED, Artifact.SCOPE_IMPORT
    );

    private final ArtifactResolver artifactResolver;
    private final Logger log;
    private final List<ArtifactRepository> remoteArtifactRepositories;

    /**
     * Creates an ArtifactResolver that has no remote repositories to resolve against.
     */
    public ArtifactResolverHelper( ArtifactResolver artifactResolver, Logger log )
    {
        this( artifactResolver, log, Collections.<ArtifactRepository>emptyList() );
    }

    public ArtifactResolverHelper( ArtifactResolver artifactResolver, Logger log,
                                  final List<ArtifactRepository> remoteArtifactRepositories )
    {
        this.artifactResolver = artifactResolver;
        this.log = log;
        this.remoteArtifactRepositories = remoteArtifactRepositories;
    }

    /**
     * Filters provided artifacts and selects only defined types based on {@code types} argument
     * or all types if {@code types} argument is empty.
     *
     * @param allArtifacts artifacts to be filtered
     * @param types artifact types to be selected
     * @return a {@code List} of all project dependencies. Never {@code null}.
     *      This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} scopes.
     *      And this should maintain dependency order to comply with library project resource precedence.
     */
    public Set<Artifact> getFilteredArtifacts( Iterable<Artifact> allArtifacts, String... types )
    {
        return getFilteredArtifacts( EXCLUDE_NON_PACKAGED_SCOPES, allArtifacts, types );
    }

    /**
     * Filters provided artifacts and selects only defined types based on {@code types} argument
     * or all types if {@code types} argument is empty.
     *
     * @param filteredScopes scopes to be filtered
     * @param allArtifacts artifacts to be filtered
     * @param types artifact types to be selected
     * @return a {@code List} of all project dependencies. Never {@code null}.
     *      This should maintain dependency order to comply with library project resource precedence.
     */
    public Set<Artifact> getFilteredArtifacts( List<String> filteredScopes,
                                               Iterable<Artifact> allArtifacts, String... types )
    {
        final List<String> acceptTypeList = Arrays.asList( types );
        boolean acceptAllArtifacts = acceptTypeList.isEmpty();
        final Set<Artifact> results = new LinkedHashSet<Artifact>();
        for ( Artifact artifact : allArtifacts )
        {
            if ( artifact == null )
            {
                continue;
            }

            if ( filteredScopes.contains( artifact.getScope() ) )
            {
                continue;
            }

            if ( acceptAllArtifacts || acceptTypeList.contains( artifact.getType() ) )
            {
                results.add( artifact );
            }
        }
        return results;
    }

    /**
     * Attempts to resolve an {@link org.apache.maven.artifact.Artifact} to a {@link java.io.File}.
     *
     * @param artifact to resolve
     * @return a {@link java.io.File} to the resolved artifact, never null.
     * @throws org.apache.maven.plugin.MojoExecutionException if the artifact could not be resolved.
     */
    public File resolveArtifactToFile( Artifact artifact ) throws MojoExecutionException
    {
        final Artifact resolvedArtifact = resolveArtifact( artifact );
        final File jar = resolvedArtifact.getFile();
        if ( jar == null )
        {
            throw new MojoExecutionException( "Could not resolve artifact " + artifact.getId()
                    + ". Please install it with \"mvn install:install-file ...\" or deploy it to a repository "
                    + "with \"mvn deploy:deploy-file ...\"" );
        }
        return jar;
    }

    public Set<Artifact> resolveArtifacts( Collection<Artifact> artifacts ) throws MojoExecutionException
    {
        final Set<Artifact> resolvedArtifacts = new LinkedHashSet<Artifact>();
        for ( final Artifact artifact : artifacts )
        {
            resolvedArtifacts.add( resolveArtifact( artifact ) );
        }
        return resolvedArtifacts;
    }

    /**
     * Resolves an artifact to a particular repository.
     *
     * @param artifact  Artifact to resolve
     * @return fully resolved artifact.
     */
    private Artifact resolveArtifact( Artifact artifact ) throws MojoExecutionException
    {
        final ArtifactResolutionRequest artifactResolutionRequest = new ArtifactResolutionRequest();
        artifactResolutionRequest.setArtifact( artifact );
        if ( remoteArtifactRepositories != null && !remoteArtifactRepositories.isEmpty() )
        {
            artifactResolutionRequest.setRemoteRepositories( remoteArtifactRepositories );
        }
        final ArtifactResolutionResult resolutionResult = this.artifactResolver.resolve( artifactResolutionRequest );

        log.debug( "Resolving : " + artifact );
        if ( resolutionResult.getArtifacts().size() == 0 )
        {
            throw new MojoExecutionException( "Could not resolve artifact " + artifact
                    + ". Please install it with \"mvn install:install-file ...\" or deploy it to a repository "
                    + "with \"mvn deploy:deploy-file ...\"" );
        }
        if ( resolutionResult.getArtifacts().size() > 1 )
        {
            log.debug( "Resolved artifacts : " + resolutionResult.getArtifacts() );
            throw new MojoExecutionException( "Could not resolve artifact " + artifact
                    + " to single target. Found the following possible options : " + resolutionResult.getArtifacts() );
        }

        final Artifact resolvedArtifact = resolutionResult.getArtifacts().iterator().next();
        log.debug( "Resolved : " + resolvedArtifact );
        return resolvedArtifact;
    }
}


package com.simpligility.maven.plugins.android;

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;

import java.util.AbstractMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
Parses a list of key/value pairs separated by a space in to a map.

 *
 *
Example input:

 *

 *     list[0] = "firstKey firstValue"
 *     list[1] = "secondKey 'second value with space and single quote escape'
 *

 *
 *
Example output:

 *

 *     map["firstKey"] = "firstValue"
 *     map["secondKey"] = "'second value with space and single quote escape'"
 *

 */
public class InstrumentationArgumentParser
{
    private static final String SEPARATOR = " ";

    /**
     * Parses the given {@code flatArgs} into a map of key/value pairs.
     *
     * @param flatArgs the flat representation of arguments, might be null
     * @return a map representation of the given key/value pair list, might be empty
     * @throws IllegalArgumentException when the given list contains unparseable entries
     */
    public static Map<String, String> parse( final List<String> flatArgs )
    {
        if ( flatArgs == null )
        {
            return Collections.EMPTY_MAP;
        }

        final Map<String, String> mappedArgs = new HashMap<String, String>();

        for ( final String flatArg : flatArgs )
        {
            final AbstractMap.SimpleEntry<String, String> keyValuePair = parseKeyValuePair( flatArg );
            mappedArgs.put( keyValuePair.getKey(), keyValuePair.getValue() );
        }

        return mappedArgs;
    }

    private static AbstractMap.SimpleEntry<String, String> parseKeyValuePair( final String arg )
    {
        final List<String> keyValueSplit = Lists.newArrayList( Splitter.on( SEPARATOR ).limit( 2 ).split( arg ) );

        if ( keyValueSplit.size() == 1 )
        {
            throw new IllegalArgumentException( "Could not separate \"" + arg + "\" by a whitespace into two parts" );
        }

        return new AbstractMap.SimpleEntry<String, String>( keyValueSplit.get( 0 ), keyValueSplit.get( 1 ) );
    }
}