com.jayway.maven.plugins.android.AbstractEmulatorMojo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of android-maven-plugin Show documentation
Show all versions of android-maven-plugin Show documentation
Maven Plugin for Android Development
/*
* 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.jayway.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.jayway.maven.plugins.android.common.DeviceHelper;
import com.jayway.maven.plugins.android.configuration.Emulator;
import org.apache.commons.lang.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
* @author Bryan O'Neil
* @see com.jayway.maven.plugins.android.configuration.Emulator
* @see com.jayway.maven.plugins.android.standalonemojos.EmulatorStartMojo
* @see com.jayway.maven.plugins.android.standalonemojos.EmulatorStopMojo
* @see com.jayway.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>
* </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.jayway.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.jayway.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.jayway.maven.plugins.android.configuration.Emulator#options
*/
@Parameter( property = "android.emulator.options" )
private String emulatorOptions;
/**
* Override default emulator executable. Default uses just "emulator".
*
* @see com.jayway.maven.plugins.android.configuration.Emulator#executable
*/
@Parameter( property = "android.emulator.executable" )
private String emulatorExecutable;
/**
* 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;
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 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 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 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 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 task = new Callable()
{
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 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.jayway.maven.plugins.android.configuration.Emulator
*/
private String assembleStartCommandLine() throws MojoExecutionException
{
String 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()
{
// exist in pom file
if ( emulator != null )
{
// exists in pom file
if ( emulator.getAvd() != null )
{
parsedAvd = emulator.getAvd();
}
else
{
parsedAvd = determineAvd();
}
// exists in pom file
if ( emulator.getOptions() != null )
{
parsedOptions = emulator.getOptions();
}
else
{
parsedOptions = determineOptions();
}
// exists in pom file
if ( emulator.getWait() != null )
{
parsedWait = emulator.getWait();
}
else
{
parsedWait = determineWait();
}
// exists in pom file
if ( emulator.getExecutable() != null )
{
parsedExecutable = emulator.getExecutable();
}
else
{
parsedExecutable = determineExecutable();
}
}
// commandline options
else
{
parsedAvd = determineAvd();
parsedOptions = determineOptions();
parsedWait = determineWait();
parsedExecutable = determineExecutable();
}
}
/**
* 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;
}
}