/* * ### * Android Maven Plugin - android-maven-plugin * * Copyright (C) 1999 - 2012 Photon Infotech Inc. * * 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. * ### */ /* * Copyright (C) 2009 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.photon.maven.plugins.android; import com.photon.maven.plugins.android.common.DeviceHelper; import com.photon.maven.plugins.android.configuration.Emulator; import org.apache.commons.lang.StringUtils; import org.apache.maven.plugin.MojoExecutionException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; 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.List; import java.util.Locale; import java.util.Arrays; /** * AbstractEmulatorMojo contains all code related to the interaction with the Android emulator. At this stage that is * starting and stopping the emulator. * * @see com.photon.maven.plugins.android.configuration.Emulator * @see com.photon.maven.plugins.android.standalonemojos.EmulatorStartMojo * @see com.photon.maven.plugins.android.standalonemojos.EmulatorStopMojo * @see com.photon.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); /** * Configuration for the emulator goals. Either use the plugin configuration like this * <pre> * <emulator> * <avd>Default</avd> * <wait>20000</wait> * <options>-no-skin</options> * </emulator> * </pre> * or configure as properties on the command line as android.emulator.avd, android.emulator.wait and * android.emulator.options 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" * * @parameter expression="${android.emulator.avd}" * @see com.photon.maven.plugins.android.configuration.Emulator#avd */ private String emulatorAvd; /** * Wait time for the emulator start up. * * @parameter expression="${android.emulator.wait}" * @see com.photon.maven.plugins.android.configuration.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". * * @parameter expression="${android.emulator.options}" * @see com.photon.maven.plugins.android.configuration.Emulator#options */ private String emulatorOptions; /** * 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 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 SCRIPTFOLDER = 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 = null; for (IDevice device : devices) { if (device.isEmulator() && isExistingEmulator(device)) { existingEmulator = device; break; } } 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 Thread.sleep(new Long(parsedWait)); } 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); } } /** * Checks whether the given device has the same AVD name as the device which the current command * is related to. <code>true</code> returned if the device AVD names are identical (independent of case) * and <code>false</code> 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().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 =SCRIPTFOLDER + "\\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 =SCRIPTFOLDER + "/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.photon.maven.plugins.android.configuration.Emulator */ private String assembleStartCommandLine() throws MojoExecutionException { StringBuilder startCommandline = new StringBuilder() .append(getAndroidSdk().getEmulatorPath()) .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(); } } // commandline options else { parsedAvd = determineAvd(); parsedOptions = determineOptions(); parsedWait = determineWait(); } } /** * Get wait value for emulator from command line option. * * @return if available return command line value otherwise return default value (5000). */ private 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"). */ private String determineAvd() { String avd; if (emulatorAvd != null) { avd = emulatorAvd; } else { avd = "Default"; } return avd; } }