io.selendroid.standalone.android.impl.DefaultAndroidEmulator.java Source code

Java tutorial

Introduction

Here is the source code for io.selendroid.standalone.android.impl.DefaultAndroidEmulator.java

Source

/*
 * 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<String, Dimension> SKIN_NAME_DIMENSIONS = new ImmutableMap.Builder<String, Dimension>()
            .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("WXGA720", new Dimension(1280, 720))
            .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;
    }

    // this contructor is used only for test purposes in setting the capabilities information. Change to public if there
    // is ever a desire to construct one of these besides reading the avdOutput
    DefaultAndroidEmulator(String avdName, String abi, Dimension screenSize, String target, String model,
            File avdFilePath, String apiTargetType) {
        this.avdName = avdName;
        this.model = model;
        this.screenSize = screenSize;
        this.avdRootFolder = avdFilePath;
        this.targetPlatform = DeviceTargetPlatform.fromInt(target);
        this.wasStartedBySelendroid = !isEmulatorStarted();
        this.apiTargetType = apiTargetType;
    }

    // avdOutput is expected to look like the following
    /*Name: Android_TV
    Device: tv_720p (Google)
    Path: /Users/antnguyen/.android/avd/Android_TV.avd
    Target: Android 5.0.1 (API level 21)
    Tag/ABI: android-tv/armeabi-v7a
    Skin: tv_720p
    Sdcard: 100M
    Snapshot: no*/
    public DefaultAndroidEmulator(String avdOutput) {
        this.avdName = extractValue("Name: (.*?)$", avdOutput);
        this.screenSize = getScreenSizeFromSkin(extractValue("Skin: (.*?)$", avdOutput));
        this.targetPlatform = DeviceTargetPlatform.fromInt(extractValue("\\(API level (.*?)\\)", avdOutput));
        this.avdRootFolder = new File(extractValue("Path: (.*?)$", avdOutput));
        this.model = extractValue("Device: (.*?)$", avdOutput);
        extractAPITargetType(avdOutput);
    }

    private void extractAPITargetType(String avdOutput) {
        String target = extractValue("Target: (.*?)$", avdOutput);
        // chose to compare against both of these strings because currently some targets say google_api [Google APIs] so
        // perhaps the actual name which looks to be google_api will be the only string in the target in the future
        if (StringUtils.containsIgnoreCase(target, "Google APIs")
                || StringUtils.containsIgnoreCase(target, "google_apis")) {
            this.apiTargetType = "google";
        }
    }

    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<AndroidEmulator> listAvailableAvds() throws AndroidDeviceException {
        List<AndroidEmulator> 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<String, Integer> startedDevices = mapDeviceNamesToSerial();

        String[] avdsOutput = StringUtils.splitByWholeSeparator(output, "---------");
        if (avdsOutput != null && avdsOutput.length > 0) {
            for (String element : avdsOutput) {
                if (!element.contains("Name:")) {
                    continue;
                }
                DefaultAndroidEmulator emulator = new DefaultAndroidEmulator(element);
                if (startedDevices.containsKey(emulator.getAvdName())) {
                    emulator.setSerial(startedDevices.get(emulator.getAvdName()));
                }
                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<String, Integer> mapDeviceNamesToSerial() {
        Map<String, Integer> mapping = new HashMap<String, Integer>();
        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 + ", apiTargetType=" + apiTargetType + "]";
    }

    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<String, Object> 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.");
        unlockScreen();

        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 unlockScreen() 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<String> coordinates = new ArrayList<String>();
        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;
    }
}