/* * 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.server.model; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import io.selendroid.common.SelendroidCapabilities; import io.selendroid.common.device.DeviceTargetPlatform; import io.selendroid.server.common.exceptions.SelendroidException; import io.selendroid.standalone.android.AndroidApp; import io.selendroid.standalone.android.AndroidDevice; import io.selendroid.standalone.android.AndroidEmulator; import io.selendroid.standalone.android.AndroidEmulatorPowerStateListener; import io.selendroid.standalone.android.DeviceManager; import io.selendroid.standalone.android.HardwareDeviceListener; import io.selendroid.standalone.android.impl.DefaultAndroidEmulator; import io.selendroid.standalone.android.impl.DefaultHardwareDevice; import io.selendroid.standalone.android.impl.InstalledAndroidApp; import io.selendroid.standalone.exceptions.AndroidDeviceException; import io.selendroid.standalone.exceptions.AndroidSdkException; import io.selendroid.standalone.exceptions.DeviceStoreException; import io.selendroid.standalone.server.model.impl.DefaultPortFinder; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; public class DeviceStore { private static final Logger log = Logger.getLogger(DeviceStore.class.getName()); private List<AndroidDevice> devicesInUse = new ArrayList<AndroidDevice>(); private Map<DeviceTargetPlatform, List<AndroidDevice>> androidDevices = new HashMap<DeviceTargetPlatform, List<AndroidDevice>>(); private EmulatorPortFinder androidEmulatorPortFinder = null; private boolean clearData = true; private boolean keepEmulator = false; private AndroidEmulatorPowerStateListener emulatorPowerStateListener = null; private DeviceManager deviceManager = null; public DeviceStore(Integer emulatorPort, DeviceManager deviceManager) { this.deviceManager = deviceManager; androidEmulatorPortFinder = new DefaultPortFinder(emulatorPort, emulatorPort + 30); } public DeviceStore(EmulatorPortFinder androidEmulatorPortFinder, DeviceManager deviceManager) { this.deviceManager = deviceManager; this.androidEmulatorPortFinder = androidEmulatorPortFinder; } public Integer nextEmulatorPort() { return androidEmulatorPortFinder.next(); } /** * After a test session a device should be released. That means id will be removed from the list * of devices in use and in case of an emulator it will be stopped. * * @param device The device to release * @see {@link #findAndroidDevice(SelendroidCapabilities)} */ public void release(AndroidDevice device, AndroidApp aut) { log.info("Releasing device " + device); if (devicesInUse.contains(device)) { if (aut != null) { // stop the app anyway - better in case people do use snapshots try { device.kill(aut); } catch (Exception e) { log.log(Level.WARNING, "Failed to kill android application when releasing device", e); } if (clearData) { try { device.clearUserData(aut); } catch (AndroidSdkException e) { log.log(Level.WARNING, "Failed to clear user data of application", e); } } } if (device instanceof AndroidEmulator && !(aut instanceof InstalledAndroidApp) && !keepEmulator) { AndroidEmulator emulator = (AndroidEmulator) device; try { emulator.stop(); } catch (AndroidDeviceException e) { log.severe("Failed to stop emulator: " + e.getMessage()); } androidEmulatorPortFinder.release(emulator.getPort()); } devicesInUse.remove(device); } } /* package */void initAndroidDevices(HardwareDeviceListener hardwareDeviceListener, boolean shouldKeepAdbAlive) throws AndroidDeviceException { emulatorPowerStateListener = new DefaultEmulatorPowerStateListener(); deviceManager.initialize(hardwareDeviceListener, emulatorPowerStateListener); List<AndroidEmulator> emulators = DefaultAndroidEmulator.listAvailableAvds(); addEmulators(emulators); if (getDevices().isEmpty()) { SelendroidException e = new SelendroidException( "No android virtual devices were found. " + "Please start the android tool and create emulators and restart the selendroid-standalone " + "or plugin an Android hardware device via USB."); log.warning("Warning: " + e); } } public synchronized void addDevice(AndroidDevice androidDevice) throws AndroidDeviceException { if (androidDevice == null) { log.info("No Android devices were found."); return; } if (androidDevice instanceof AndroidEmulator) { throw new AndroidDeviceException( "For adding emulator instances please use #addEmulator method."); } if (androidDevice.isDeviceReady() == true) { log.info("Adding: " + androidDevice); addDeviceToStore(androidDevice); } } public synchronized void updateDevice(AndroidDevice device) throws AndroidDeviceException { boolean deviceRemoved = false; for (DeviceTargetPlatform targetPlatform : androidDevices.keySet()) { List<AndroidDevice> platformDevices = androidDevices.get(targetPlatform); // Attempt to remove the device from this target platform; deviceRemoved |= platformDevices.remove(device); } if (deviceRemoved) { addDeviceToStore(device); } else { log.warning("Attempted to update device which did could not be found in the device store"); } } public void addEmulators(List<AndroidEmulator> emulators) throws AndroidDeviceException { if (emulators == null || emulators.isEmpty()) { log.info("No emulators has been found."); return; } for (AndroidEmulator emulator : emulators) { log.info("Adding: " + emulator); addDeviceToStore((AndroidDevice) emulator); } } /** * Internal method to add an actual device to the store. * * @param device The device to add. * @throws AndroidDeviceException */ protected synchronized void addDeviceToStore(AndroidDevice device) throws AndroidDeviceException { if (androidDevices.containsKey(device.getTargetPlatform())) { List<AndroidDevice> platformDevices = androidDevices.get(device.getTargetPlatform()); if (!platformDevices.contains(device)) { platformDevices.add(device); } } else { androidDevices.put(device.getTargetPlatform(), Lists.newArrayList(device)); } } /** * Finds a device for the requested capabilities. <b>important note:</b> if the device is not any * longer used, call the {@link #release(AndroidDevice, AndroidApp)} method. * * @param caps The desired test session capabilities. * @return Matching device for a test session. * @throws DeviceStoreException * @see {@link #release(AndroidDevice, AndroidApp)} */ public synchronized AndroidDevice findAndroidDevice(SelendroidCapabilities caps) throws DeviceStoreException { Preconditions.checkArgument(caps != null, "Error: capabilities are null"); if (androidDevices.isEmpty()) { throw new DeviceStoreException("Fatal Error: Device Store does not contain any Android Device."); } String platformVersion = caps.getPlatformVersion(); Iterable<AndroidDevice> candidateDevices = Strings.isNullOrEmpty(platformVersion) ? Iterables.concat(androidDevices.values()) : androidDevices.get(DeviceTargetPlatform.fromPlatformVersion(platformVersion)); candidateDevices = Objects.firstNonNull(candidateDevices, Collections.EMPTY_LIST); FluentIterable<AndroidDevice> allMatchingDevices = FluentIterable.from(candidateDevices) .filter(deviceNotInUse()) .filter(deviceSatisfiesCapabilities(caps)); if (!allMatchingDevices.isEmpty()) { AndroidDevice matchingDevice = allMatchingDevices.filter(deviceRunning()).first() .or(allMatchingDevices.first()).get(); if (!deviceRunning().apply(matchingDevice)) { log.info("Using potential match: " + matchingDevice); } devicesInUse.add(matchingDevice); return matchingDevice; } else { throw new DeviceStoreException("No devices are found. " + "This can happen if the devices are in use or no device screen " + "matches the required capabilities."); } } private boolean isEmulatorSwitchedOff(AndroidDevice device) throws DeviceStoreException { if (device instanceof AndroidEmulator) { try { return !((AndroidEmulator) device).isEmulatorStarted(); } catch (AndroidDeviceException e) { throw new DeviceStoreException(e); } } return true; } public List<AndroidDevice> getDevices() { List<AndroidDevice> devices = new ArrayList<AndroidDevice>(); for (Map.Entry<DeviceTargetPlatform, List<AndroidDevice>> entry : androidDevices.entrySet()) { devices.addAll(entry.getValue()); } return devices; } /** * For testing only */ /* package */List<AndroidDevice> getDevicesInUse() { return devicesInUse; } /** * For testing only */ /* package */Map<DeviceTargetPlatform, List<AndroidDevice>> getDevicesList() { return androidDevices; } /** * Removes the given device from store so that it cannot be any longer be used for testing. This * can happen if e.g. the hardware device gets unplugged from the computer. * * @param device the device to remove. * @throws DeviceStoreException when parameter is not type of 'DefaultHardwareDevice'. */ public void removeAndroidDevice(AndroidDevice device) throws DeviceStoreException { if (device == null) { return; } boolean hardwareDevice = device instanceof DefaultHardwareDevice; if (hardwareDevice == false) { throw new DeviceStoreException("Only devices of type 'DefaultHardwareDevice' can be removed."); } release(device, null); DeviceTargetPlatform apiLevel = device.getTargetPlatform(); if (androidDevices.containsKey(apiLevel)) { log.info("Removing: " + device); androidDevices.get(apiLevel).remove(device); if (androidDevices.get(apiLevel).isEmpty()) { androidDevices.remove(apiLevel); } } else { for (List<AndroidDevice> targetDevices : androidDevices.values()) { if (targetDevices.contains(device)) { log.warning("Device in devicestore"); } } log.warning("The target platform version of the device is not found in device store."); log.warning("The device was propably already removed."); } } public void setClearData(boolean clearData) { this.clearData = clearData; } public void setKeepEmulator(boolean keepEmulator) { this.keepEmulator = keepEmulator; } private Predicate<AndroidDevice> deviceNotInUse() { return new Predicate<AndroidDevice>() { @Override public boolean apply(AndroidDevice candidate) { return !devicesInUse.contains(candidate); } }; } private Predicate<AndroidDevice> deviceSatisfiesCapabilities(final SelendroidCapabilities capabilities) { return new Predicate<AndroidDevice>() { @Override public boolean apply(AndroidDevice candidate) { ArrayList<Boolean> booleanExpressions = Lists.newArrayList( candidate.screenSizeMatches(capabilities.getScreenSize()), capabilities.getEmulator() == null ? true : capabilities.getEmulator() ? candidate instanceof DefaultAndroidEmulator : candidate instanceof DefaultHardwareDevice, StringUtils.isNotBlank(capabilities.getSerial()) ? capabilities.getSerial().equals(candidate.getSerial()) : true, StringUtils.isNotBlank(capabilities.getModel()) ? candidate.getModel().contains(capabilities.getModel()) : true, StringUtils.isNotBlank(capabilities.getAPITargetType()) ? candidate.getAPITargetType() != null && candidate.getAPITargetType().contains(capabilities.getAPITargetType()) : true ); return Iterables.all(booleanExpressions, Predicates.equalTo(true)); } }; } private Predicate<AndroidDevice> deviceRunning() { return new Predicate<AndroidDevice>() { @Override public boolean apply(AndroidDevice candidate) { return !(candidate instanceof DefaultAndroidEmulator && !((DefaultAndroidEmulator) candidate).isEmulatorStarted()); } }; } class DefaultEmulatorPowerStateListener implements AndroidEmulatorPowerStateListener { @Override public void onDeviceStarted(String avdName, String serial) { AndroidEmulator emulator = findEmulator(avdName); if (emulator != null) { Integer port = Integer.parseInt(serial.replace("emulator-", "")); emulator.setSerial(port); emulator.setWasStartedBySelendroid(false); } } AndroidEmulator findEmulator(String avdName) { for (AndroidDevice device : getDevices()) { if (device instanceof AndroidEmulator) { AndroidEmulator emulator = (AndroidEmulator) device; if (avdName.equals(emulator.getAvdName())) { return emulator; } } } return null; } @Override public void onDeviceStopped(String avdName) { // do nothing } } }