io.selendroid.standalone.android.impl.DefaultAndroidEmulator Maven / Gradle / Ivy
/*
* Copyright 2012-2014 eBay Software Foundation and selendroid committers.
*
* 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 io.selendroid.standalone.android.impl;
import com.android.ddmlib.IDevice;
import com.beust.jcommander.internal.Lists;
import com.google.common.collect.ImmutableMap;
import io.selendroid.common.device.DeviceTargetPlatform;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.standalone.android.AndroidEmulator;
import io.selendroid.standalone.android.AndroidSdk;
import io.selendroid.standalone.android.TelnetClient;
import io.selendroid.standalone.exceptions.AndroidDeviceException;
import io.selendroid.standalone.exceptions.ShellCommandException;
import io.selendroid.standalone.io.ShellCommand;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.Dimension;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DefaultAndroidEmulator extends AbstractDevice implements AndroidEmulator {
private static final String EMULATOR_SERIAL_PREFIX = "emulator-";
private static final Logger log = Logger.getLogger(DefaultAndroidEmulator.class.getName());
public static final String ANDROID_EMULATOR_HARDWARE_CONFIG = "hardware-qemu.ini";
public static final String FILE_LOCKING_SUFIX = ".lock";
private static final ImmutableMap SKIN_NAME_DIMENSIONS = new
ImmutableMap.Builder()
.put("QVGA", new Dimension(240, 320))
.put("WQVGA400", new Dimension(240, 400))
.put("WQVGA432", new Dimension(240, 432))
.put("HVGA", new Dimension(320, 480))
.put("WVGA800", new Dimension(480, 800))
.put("WVGA854", new Dimension(480, 854))
.put("WXGA", new Dimension(1280, 800))
.put("WXGA800", new Dimension(1280, 800))
.build();
private Dimension screenSize;
private DeviceTargetPlatform targetPlatform;
private String avdName;
private File avdRootFolder;
private Locale locale = null;
private boolean wasStartedBySelendroid;
protected DefaultAndroidEmulator() {
this.wasStartedBySelendroid = Boolean.FALSE;
}
public DefaultAndroidEmulator(String avdName, String abi, Dimension screenSize, String target,
String model, File avdFilePath) {
this.avdName = avdName;
this.model = model;
this.screenSize = screenSize;
this.avdRootFolder = avdFilePath;
this.targetPlatform = DeviceTargetPlatform.fromInt(target);
this.wasStartedBySelendroid = !isEmulatorStarted();
}
public File getAvdRootFolder() {
return avdRootFolder;
}
public Dimension getScreenSize() {
return screenSize;
}
public DeviceTargetPlatform getTargetPlatform() {
return targetPlatform;
}
/*
* (non-Javadoc)
*
* @see io.selendroid.android.impl.AndroidEmulator#isEmulatorAlreadyExistent()
*/
@Override
public boolean isEmulatorAlreadyExistent() {
File emulatorFolder =
new File(FileUtils.getUserDirectory(), File.separator + ".android" + File.separator + "avd"
+ File.separator + getAvdName() + ".avd");
return emulatorFolder.exists();
}
public String getAvdName() {
return avdName;
}
public static List listAvailableAvds() throws AndroidDeviceException {
List avds = Lists.newArrayList();
CommandLine cmd = new CommandLine(AndroidSdk.android());
cmd.addArgument("list", false);
cmd.addArgument("avds", false);
String output = null;
try {
output = ShellCommand.exec(cmd, 20000);
} catch (ShellCommandException e) {
throw new AndroidDeviceException(e);
}
Map startedDevices = mapDeviceNamesToSerial();
String[] avdsOutput = StringUtils.splitByWholeSeparator(output, "---------");
if (avdsOutput != null && avdsOutput.length > 0) {
for (String element : avdsOutput) {
if (!element.contains("Name:")) {
continue;
}
String avdName = extractValue("Name: (.*?)$", element);
String abi = extractValue("ABI: (.*?)$", element);
Dimension screenSize = getScreenSizeFromSkin(extractValue("Skin: (.*?)$", element));
String target = extractValue("\\(API level (.*?)\\)", element);
File avdFilePath = new File(extractValue("Path: (.*?)$", element));
String model = extractValue("Device: (.*?)$", element);
DefaultAndroidEmulator emulator =
new DefaultAndroidEmulator(avdName, abi, screenSize, target, model, avdFilePath);
if (startedDevices.containsKey(avdName)) {
emulator.setSerial(startedDevices.get(avdName));
}
avds.add(emulator);
}
}
return avds;
}
public static Dimension getScreenSizeFromSkin(String skinName) {
final Pattern dimensionSkinPattern = Pattern.compile("([0-9]+)x([0-9]+)");
Matcher matcher = dimensionSkinPattern.matcher(skinName);
if (matcher.matches()) {
int width = Integer.parseInt(matcher.group(1));
int height = Integer.parseInt(matcher.group(2));
return new Dimension(width, height);
} else if (SKIN_NAME_DIMENSIONS.containsKey(skinName.toUpperCase())) {
return SKIN_NAME_DIMENSIONS.get(skinName.toUpperCase());
} else {
log.warning("Failed to get dimensions for skin: " + skinName);
return null;
}
}
private static Map mapDeviceNamesToSerial() {
Map mapping = new HashMap();
CommandLine command = new CommandLine(AndroidSdk.adb());
command.addArgument("devices");
Scanner scanner;
try {
scanner = new Scanner(ShellCommand.exec(command));
} catch (ShellCommandException e) {
return mapping;
}
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
Pattern pattern = Pattern.compile("emulator-\\d\\d\\d\\d");
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
String serial = matcher.group(0);
Integer port = Integer.valueOf(serial.replaceAll("emulator-", ""));
TelnetClient client = null;
try {
client = new TelnetClient(port);
String avdName = client.sendCommand("avd name");
mapping.put(avdName, port);
} catch (AndroidDeviceException e) {
// ignore
} finally {
if (client != null) {
client.close();
}
}
}
}
scanner.close();
return mapping;
}
@Override
public boolean isEmulatorStarted() {
File lockedEmulatorHardwareConfig =
new File(avdRootFolder, ANDROID_EMULATOR_HARDWARE_CONFIG + FILE_LOCKING_SUFIX);
return lockedEmulatorHardwareConfig.exists();
}
@Override
public String toString() {
return "AndroidEmulator [screenSize=" + screenSize + ", targetPlatform=" + targetPlatform
+ ", serial=" + serial + ", avdName=" + avdName + ", model=" + model + "]";
}
public void setSerial(int port) {
this.port = port;
serial = EMULATOR_SERIAL_PREFIX + port;
}
public Integer getPort() {
if (isSerialConfigured()) {
return Integer.parseInt(serial.replace(EMULATOR_SERIAL_PREFIX, ""));
}
return null;
}
@Override
public void start(Locale locale, int emulatorPort, Map options)
throws AndroidDeviceException {
if (isEmulatorStarted()) {
throw new SelendroidException("Error - Android emulator is already started " + this);
}
Long timeout = null;
String emulatorOptions = null;
String display = null;
if (options != null) {
if (options.containsKey(TIMEOUT_OPTION)) {
timeout = (Long) options.get(TIMEOUT_OPTION);
}
if (options.containsKey(DISPLAY_OPTION)) {
display = (String) options.get(DISPLAY_OPTION);
}
if (options.containsKey(EMULATOR_OPTIONS)) {
emulatorOptions = (String) options.get(EMULATOR_OPTIONS);
}
}
if (display != null) {
log.info("Using display " + display + " for running the emulator");
}
if (timeout == null) {
timeout = 120000L;
}
log.info("Using timeout of '" + timeout / 1000 + "' seconds to start the emulator.");
this.locale = locale;
CommandLine cmd = new CommandLine(AndroidSdk.emulator());
cmd.addArgument("-no-snapshot-save", false);
cmd.addArgument("-avd", false);
cmd.addArgument(avdName, false);
cmd.addArgument("-port", false);
cmd.addArgument(String.valueOf(emulatorPort), false);
if (locale != null) {
cmd.addArgument("-prop", false);
cmd.addArgument("persist.sys.language=" + locale.getLanguage(), false);
cmd.addArgument("-prop", false);
cmd.addArgument("persist.sys.country=" + locale.getCountry(), false);
}
if (emulatorOptions != null && !emulatorOptions.isEmpty()) {
cmd.addArguments(emulatorOptions.split(" "), false);
}
long start = System.currentTimeMillis();
long timeoutEnd = start + timeout;
try {
ShellCommand.execAsync(display, cmd);
} catch (ShellCommandException e) {
throw new SelendroidException("unable to start the emulator: " + this);
}
setSerial(emulatorPort);
Boolean adbKillServerAttempted = false;
// Without this one seconds, the call to "isDeviceReady" is
// too quickly sent while the emulator is still starting and
// not ready to receive any commands. Because of this the
// while loops failed and sometimes hung in isDeviceReady function.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
while (!isDeviceReady()) {
if (!adbKillServerAttempted && System.currentTimeMillis() - start > 10000) {
CommandLine adbDevicesCmd = new CommandLine(AndroidSdk.adb());
adbDevicesCmd.addArgument("devices", false);
String devices = "";
try {
devices = ShellCommand.exec(adbDevicesCmd, 20000);
} catch (ShellCommandException e) {
// pass
}
if (!devices.contains(String.valueOf(emulatorPort))) {
CommandLine resetAdb = new CommandLine(AndroidSdk.adb());
resetAdb.addArgument("kill-server", false);
try {
ShellCommand.exec(resetAdb, 20000);
} catch (ShellCommandException e) {
throw new SelendroidException("unable to kill the adb server");
}
}
adbKillServerAttempted = true;
}
if (timeoutEnd >= System.currentTimeMillis()) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
throw new AndroidDeviceException("The emulator with avd '" + getAvdName()
+ "' was not started after " + (System.currentTimeMillis() - start) / 1000
+ " seconds.");
}
}
log.info("Emulator start took: " + (System.currentTimeMillis() - start) / 1000 + " seconds");
log.info("Please have in mind, starting an emulator takes usually about 45 seconds.");
unlockEmulatorScreen();
waitForLauncherToComplete();
// we observed that emulators can sometimes not be 'fully loaded'
// if we click on the All Apps button and wait for it to load it is more likely to be in a
// usable state.
allAppsGridView();
waitForLauncherToComplete();
setWasStartedBySelendroid(true);
}
public void unlockEmulatorScreen() throws AndroidDeviceException {
// Send menu key event
CommandLine menuKeyCommand = getAdbCommand();
menuKeyCommand.addArgument("shell", false);
menuKeyCommand.addArgument("input", false);
menuKeyCommand.addArgument("keyevent", false);
menuKeyCommand.addArgument("82", false);
try {
ShellCommand.exec(menuKeyCommand, 20000);
} catch (ShellCommandException e) {
throw new AndroidDeviceException(e);
}
// Send back key event
CommandLine backKeyCommand = getAdbCommand();
backKeyCommand.addArgument("shell", false);
backKeyCommand.addArgument("input", false);
backKeyCommand.addArgument("keyevent", false);
backKeyCommand.addArgument("4", false);
try {
ShellCommand.exec(backKeyCommand, 20000);
} catch (ShellCommandException e) {
throw new AndroidDeviceException(e);
}
}
private void waitForLauncherToComplete() throws AndroidDeviceException {
CommandLine processListCommand = getAdbCommand();
processListCommand.addArgument("shell", false);
processListCommand.addArgument("ps", false);
String processList = null;
do {
try {
processList = ShellCommand.exec(processListCommand, 20000);
} catch (ShellCommandException e) {
throw new AndroidDeviceException(e);
}
//Wait a bit
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (processList == null || !processList.contains("S com.android.launcher"));
}
private CommandLine getAdbCommand() {
CommandLine processListCommand = new CommandLine(AndroidSdk.adb());
if (isSerialConfigured()) {
processListCommand.addArgument("-s", false);
processListCommand.addArgument(serial, false);
}
return processListCommand;
}
private void allAppsGridView() throws AndroidDeviceException {
int x = screenSize.width;
int y = screenSize.height;
if (x > y) {
y = y / 2;
x = x - 30;
} else {
x = x / 2;
y = y - 30;
}
List coordinates = new ArrayList();
coordinates.add("3 0 " + x);
coordinates.add("3 1 " + y);
coordinates.add("1 330 1");
coordinates.add("0 0 0");
coordinates.add("1 330 0");
coordinates.add("0 0 0");
for (String coordinate : coordinates) {
CommandLine event1 = getAdbCommand();
event1.addArgument("shell", false);
event1.addArgument("sendevent", false);
event1.addArgument("dev/input/event0", false);
event1.addArgument(coordinate, false);
try {
ShellCommand.exec(event1);
} catch (ShellCommandException e) {
throw new AndroidDeviceException(e);
}
}
try {
Thread.sleep(750);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void stopEmulator() throws AndroidDeviceException {
TelnetClient client = null;
try {
client = new TelnetClient(getPort());
client.sendQuietly("kill");
} catch (AndroidDeviceException e) {
// ignore
} finally {
if (client != null) {
client.close();
}
}
}
@Override
public void stop() throws AndroidDeviceException {
if (wasStartedBySelendroid) {
stopEmulator();
Boolean killed = false;
while (isEmulatorStarted()) {
log.info("emulator still running, sleeping 0.5, waiting for it to release the lock");
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
if (!killed) {
try {
stopEmulator();
} catch (AndroidDeviceException sce) {
killed = true;
}
}
}
}
}
@Override
public Locale getLocale() {
return locale;
}
@Override
public void setIDevice(IDevice iDevice) {
super.device = iDevice;
}
public String getSerial() {
return serial;
}
public void setWasStartedBySelendroid(boolean wasStartedBySelendroid) {
this.wasStartedBySelendroid = wasStartedBySelendroid;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy