/* * 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.beust.jcommander.internal.Lists; import io.netty.handler.codec.http.HttpMethod; import io.selendroid.common.SelendroidCapabilities; import io.selendroid.server.common.ServerDetails; import io.selendroid.server.common.exceptions.AppCrashedException; import io.selendroid.server.common.exceptions.SelendroidException; import io.selendroid.server.common.exceptions.SessionNotCreatedException; import io.selendroid.standalone.SelendroidConfiguration; import io.selendroid.standalone.android.AndroidApp; import io.selendroid.standalone.android.AndroidDevice; import io.selendroid.standalone.android.AndroidEmulator; import io.selendroid.standalone.android.AndroidSdk; import io.selendroid.standalone.android.DeviceManager; import io.selendroid.standalone.android.impl.DefaultAndroidEmulator; import io.selendroid.standalone.android.impl.DefaultDeviceManager; import io.selendroid.standalone.android.impl.DefaultHardwareDevice; import io.selendroid.standalone.android.impl.InstalledAndroidApp; import io.selendroid.standalone.builder.AndroidDriverAPKBuilder; import io.selendroid.standalone.builder.SelendroidServerBuilder; import io.selendroid.standalone.exceptions.AndroidDeviceException; import io.selendroid.standalone.exceptions.AndroidSdkException; import io.selendroid.standalone.server.util.FolderMonitor; import io.selendroid.standalone.server.util.HttpClientUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.openqa.selenium.By; import org.openqa.selenium.remote.BrowserType; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; public class SelendroidStandaloneDriver implements ServerDetails { public static final String WD_RESP_KEY_VALUE = "value"; public static final String WD_RESP_KEY_STATUS = "status"; public static final String WD_RESP_KEY_SESSION_ID = "sessionId"; public static final String APP_BASE_PACKAGE = "basePackage"; public static final String APP_ID = "appId"; private static int selendroidServerPort = 38080; private static final Logger log = Logger.getLogger(SelendroidStandaloneDriver.class.getName()); private Map<String, AndroidApp> appsStore = new HashMap<String, AndroidApp>(); private Map<String, AndroidApp> selendroidServers = new HashMap<String, AndroidApp>(); private Map<String, ActiveSession> sessions = new HashMap<String, ActiveSession>(); private DeviceStore deviceStore = null; private SelendroidServerBuilder selendroidApkBuilder = null; private AndroidDriverAPKBuilder androidDriverAPKBuilder = null; private SelendroidConfiguration serverConfiguration = null; private DeviceManager deviceManager; private FolderMonitor folderMonitor = null; private SelendroidStandaloneDriverEventListener eventListener = new DummySelendroidStandaloneDriverEventListener(); public SelendroidStandaloneDriver(SelendroidConfiguration serverConfiguration) throws AndroidSdkException, AndroidDeviceException { this.serverConfiguration = serverConfiguration; selendroidApkBuilder = new SelendroidServerBuilder(serverConfiguration); androidDriverAPKBuilder = new AndroidDriverAPKBuilder(); selendroidServerPort = serverConfiguration.getSelendroidServerPort(); if (serverConfiguration.getAppFolderToMonitor() != null) { startFolderMonitor(); } initApplicationsUnderTest(serverConfiguration); initAndroidDevices(); deviceStore.setClearData(!serverConfiguration.isNoClearData()); deviceStore.setKeepEmulator(serverConfiguration.isKeepEmulator()); } /** * For testing only */ SelendroidStandaloneDriver(SelendroidServerBuilder builder, DeviceManager deviceManager, AndroidDriverAPKBuilder androidDriverAPKBuilder) { this.selendroidApkBuilder = builder; this.deviceManager = deviceManager; this.androidDriverAPKBuilder = androidDriverAPKBuilder; } /** * This function will sign an android app and add it to the App Store. The function is made public because it also be * invoked by the Folder Monitor each time a new application dropped into this folder. * * @param file * - The file to be added to the app store * @throws AndroidSdkException */ public void addToAppsStore(File file) throws AndroidSdkException { AndroidApp app = null; try { app = selendroidApkBuilder.resignApp(file); } catch (Exception e) { throw new SessionNotCreatedException( "An error occurred while resigning the app '" + file.getName() + "'. ", e); } String appId = null; try { appId = app.getAppId(); } catch (AndroidSdkException e) { log.info("Ignoring app because an error occurred reading the app details: " + file.getAbsolutePath()); log.info(e.getMessage()); } if (appId != null && !appsStore.containsKey(appId)) { appsStore.put(appId, app); log.info("App " + appId + " has been added to selendroid standalone server."); } } /* package */void initApplicationsUnderTest(SelendroidConfiguration serverConfiguration) throws AndroidSdkException { if (serverConfiguration == null) { throw new SelendroidException("Configuration error - serverConfiguration can't be null."); } this.serverConfiguration = serverConfiguration; // each of the apps specified on the command line need to get resigned // and 'stored' to be installed on the device for (String appPath : serverConfiguration.getSupportedApps()) { File file = new File(appPath); if (file.exists()) { addToAppsStore(file); } else { log.severe("Ignoring app because it was not found: " + file.getAbsolutePath()); } } if (!serverConfiguration.isNoWebViewApp()) { // extract the 'AndroidDriver' app and show it as available try { // using "android" as the app name, because that is the desired capability default in // selenium for // DesiredCapabilities.ANDROID File androidAPK = androidDriverAPKBuilder.extractAndroidDriverAPK(); if(serverConfiguration != null && serverConfiguration.isDeleteTmpFiles()) { androidAPK.deleteOnExit(); //Deletes temporary files if flag set } AndroidApp app = selendroidApkBuilder.resignApp(androidAPK); appsStore.put(BrowserType.ANDROID, app); } catch (Exception e) { throw new RuntimeException(e); } } } /* package */void initAndroidDevices() throws AndroidDeviceException { deviceManager = new DefaultDeviceManager(AndroidSdk.adb().getAbsolutePath(), serverConfiguration.shouldKeepAdbAlive()); deviceStore = new DeviceStore(serverConfiguration.getEmulatorPort(), deviceManager); deviceStore.initAndroidDevices(new DefaultHardwareDeviceListener(deviceStore, this), serverConfiguration.shouldKeepAdbAlive()); } @Override public String getServerVersion() { return SelendroidServerBuilder.getJarVersionNumber(); } @Override public String getCpuArch() { return System.getProperty("os.arch"); } @Override public String getOsVersion() { return System.getProperty("os.version"); } @Override public String getOsName() { return System.getProperty("os.name"); } protected SelendroidConfiguration getSelendroidConfiguration() { return serverConfiguration; } public String createNewTestSession(JSONObject caps) { return createNewTestSession(caps, serverConfiguration.getServerStartRetries()); } public String createNewTestSession(JSONObject caps, Integer retries) { AndroidDevice device = null; AndroidApp app = null; Exception lastException = null; while (retries >= 0) { try { SelendroidCapabilities desiredCapabilities = getSelendroidCapabilities(caps); String desiredAut = desiredCapabilities.getDefaultApp(appsStore.keySet()); app = getAndroidApp(desiredCapabilities, desiredAut); log.info("'" + desiredAut + "' will be used as app under test."); device = deviceStore.findAndroidDevice(desiredCapabilities); // If we are using an emulator need to start it up if (device instanceof AndroidEmulator) { startAndroidEmulator(desiredCapabilities, (AndroidEmulator) device); // If we are using an android device } else { device.unlockScreen(); } boolean appInstalledOnDevice = device.isInstalled(app) || app instanceof InstalledAndroidApp; if (!appInstalledOnDevice || serverConfiguration.isForceReinstall()) { device.install(app); } else { log.info("the app under test is already installed."); } if(!serverConfiguration.isNoClearData()) { device.clearUserData(app); } int port = serverConfiguration.isReuseSelendroidServerPort() ? serverConfiguration.getSelendroidServerPort() : getNextSelendroidServerPort(); boolean serverInstalled = device.isInstalled("io.selendroid." + app.getBasePackage()); if (!serverInstalled || serverConfiguration.isForceReinstall()) { try { device.install(createSelendroidServerApk(app)); } catch (AndroidSdkException e) { throw new SessionNotCreatedException("Could not install selendroid-server on the device", e); } } else { log.info( "Not creating and installing selendroid-server because it is already installed for this app under test."); } // Run any adb commands requested in the capabilities List<String> preSessionAdbCommands = desiredCapabilities.getPreSessionAdbCommands(); runPreSessionCommands(device, preSessionAdbCommands); // Push extension dex to device if specified String extensionFile = desiredCapabilities.getSelendroidExtensions(); pushExtensionsToDevice(device, extensionFile); // Configure logging on the device device.setLoggingEnabled(serverConfiguration.isDeviceLog()); // It's GO TIME! // start the selendroid server on the device and make sure it's up eventListener.onBeforeDeviceServerStart(); device.startSelendroid(app, port, desiredCapabilities); waitForServerStart(device); eventListener.onAfterDeviceServerStart(); // arbitrary sleeps? yay... // looks like after the server starts responding // we need to give it a moment before starting a session? try { Thread.sleep(500); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } // create the new session on the device server RemoteWebDriver driver = new RemoteWebDriver(new URL("http://localhost:" + port + "/wd/hub"), desiredCapabilities); String sessionId = driver.getSessionId().toString(); SelendroidCapabilities requiredCapabilities = new SelendroidCapabilities(driver.getCapabilities().asMap()); ActiveSession session = new ActiveSession(sessionId, requiredCapabilities, app, device, port, this); this.sessions.put(sessionId, session); // We are requesting an "AndroidDriver" so automatically switch to the webview if (BrowserType.ANDROID.equals(desiredCapabilities.getAut())) { switchToWebView(driver); } return sessionId; } catch (Exception e) { lastException = e; log.log(Level.SEVERE, "Error occurred while starting Selendroid session", e); retries--; // Return device to store if (device != null) { deviceStore.release(device, app); device = null; } } } if (lastException instanceof RuntimeException) { // Don't wrap the exception throw (RuntimeException)lastException; } else { throw new SessionNotCreatedException("Error starting Selendroid session", lastException); } } private void switchToWebView(RemoteWebDriver driver) { // arbitrarily high wait time, will this cover our slowest possible device/emulator? WebDriverWait wait = new WebDriverWait(driver, 60); // wait for the WebView to appear wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("android.webkit.WebView"))); driver.switchTo().window("WEBVIEW"); // the 'android-driver' webview has an h1 with id 'AndroidDriver' embedded in it wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("AndroidDriver"))); } private void waitForServerStart(AndroidDevice device) { long startTimeout = serverConfiguration.getServerStartTimeout(); long timeoutEnd = System.currentTimeMillis() + startTimeout; log.info("Waiting for the Selendroid server to start."); while (!device.isSelendroidRunning()) { if (timeoutEnd >= System.currentTimeMillis()) { try { Thread.sleep(2000); String crashMessage = device.getCrashLog(); if (!crashMessage.isEmpty()) { throw new AppCrashedException(crashMessage); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } else { throw new SelendroidException("Selendroid server on the device didn't come up after " + startTimeout / 1000 + "sec:"); } } log.info("Selendroid server has started."); } private void pushExtensionsToDevice(AndroidDevice device, String extensionFile) { if (extensionFile != null) { String externalStorageDirectory = device.getExternalStoragePath(); String deviceDexPath = new File(externalStorageDirectory, "extension.dex").getAbsolutePath(); device.runAdbCommand(String.format("push %s %s", extensionFile, deviceDexPath)); } } private void runPreSessionCommands(AndroidDevice device, List<String> preSessionAdbCommands) { List<String> adbCommands = new ArrayList<String>(); adbCommands.add("shell setprop log.tag.SELENDROID " + serverConfiguration.getLogLevel().name()); adbCommands.addAll(preSessionAdbCommands); for (String adbCommandParameter : adbCommands) { device.runAdbCommand(adbCommandParameter); } } private void startAndroidEmulator(SelendroidCapabilities desiredCapabilities, AndroidEmulator device) throws AndroidDeviceException { AndroidEmulator emulator = device; if (emulator.isEmulatorStarted()) { emulator.unlockScreen(); } else { Map<String, Object> config = new HashMap<String, Object>(); if (serverConfiguration.getEmulatorOptions() != null) { config.put(AndroidEmulator.EMULATOR_OPTIONS, serverConfiguration.getEmulatorOptions()); } config.put(AndroidEmulator.TIMEOUT_OPTION, serverConfiguration.getTimeoutEmulatorStart()); if (desiredCapabilities.asMap().containsKey(SelendroidCapabilities.DISPLAY)) { Object d = desiredCapabilities.getCapability(SelendroidCapabilities.DISPLAY); config.put(AndroidEmulator.DISPLAY_OPTION, String.valueOf(d)); } Locale locale = parseLocale(desiredCapabilities); emulator.start(locale, deviceStore.nextEmulatorPort(), config); } emulator.setIDevice(deviceManager.getVirtualDevice(emulator.getAvdName())); } private AndroidApp getAndroidApp(SelendroidCapabilities desiredCapabilities, String aut) { AndroidApp app = appsStore.get(aut); if (app == null) { if (desiredCapabilities.getLaunchActivity() != null) { String appInfo = String.format("%s/%s", aut, desiredCapabilities.getLaunchActivity()); log.log(Level.INFO, "The requested application under test is not configured in selendroid server, " + "assuming the " + appInfo + " is installed on the device."); app = new InstalledAndroidApp(appInfo); } else { throw new SessionNotCreatedException( "The requested application under test is not configured in selendroid server."); } } // adjust app based on capabilities (some parameters are session specific) app = augmentApp(app, desiredCapabilities); return app; } private SelendroidCapabilities getSelendroidCapabilities(JSONObject caps) { SelendroidCapabilities desiredCapabilities;// Convert the JSON capabilities to SelendroidCapabilities try { desiredCapabilities = new SelendroidCapabilities(caps); desiredCapabilities.setUseJunitRunner(serverConfiguration.isUseJUnitBootstrap()); } catch (JSONException e) { throw new SelendroidException("Desired capabilities cannot be parsed."); } return desiredCapabilities; } /** * Augment the application with parameters from {@code desiredCapabilities} * * @param app to be augmented * @param desiredCapabilities configuration requested for this session */ private AndroidApp augmentApp(AndroidApp app, SelendroidCapabilities desiredCapabilities) { if (desiredCapabilities.getLaunchActivity() != null) { app.setMainActivity(desiredCapabilities.getLaunchActivity()); } return app; } private AndroidApp createSelendroidServerApk(AndroidApp aut) throws AndroidSdkException { if (!selendroidServers.containsKey(aut.getAppId())) { try { AndroidApp selendroidServer = selendroidApkBuilder.createSelendroidServer(aut); selendroidServers.put(aut.getAppId(), selendroidServer); } catch (Exception e) { log.log(Level.SEVERE, "Cannot build the Selendroid server APK", e); throw new SessionNotCreatedException( "Cannot build the Selendroid server APK for application '" + aut + "': " + e.getMessage()); } } return selendroidServers.get(aut.getAppId()); } private Locale parseLocale(SelendroidCapabilities capa) { if (capa.getLocale() == null) { return null; } String[] localeStr = capa.getLocale().split("_"); return new Locale(localeStr[0], localeStr[1]); } // This function will start a separate thread to monitor the // Applications folder. private void startFolderMonitor() { if (serverConfiguration.getAppFolderToMonitor() != null) { try { folderMonitor = new FolderMonitor(this, serverConfiguration); folderMonitor.start(); } catch (IOException e) { log.warning("Could not monitor the given folder: " + serverConfiguration.getAppFolderToMonitor()); } } } /** * For testing only */ /* package */Map<String, AndroidApp> getConfiguredApps() { return Collections.unmodifiableMap(appsStore); } /** * For testing only */ /* package */void setDeviceStore(DeviceStore store) { this.deviceStore = store; } private synchronized int getNextSelendroidServerPort() { return selendroidServerPort++; } /** * FOR TESTING ONLY */ public List<ActiveSession> getActiveSessions() { return Lists.newArrayList(sessions.values()); } public boolean isValidSession(String sessionId) { return sessionId != null && !sessionId.isEmpty() && sessions.containsKey(sessionId); } public void stopSession(String sessionId) throws AndroidDeviceException { if (isValidSession(sessionId)) { ActiveSession session = sessions.get(sessionId); session.stopSessionTimer(); try { HttpClientUtil.executeRequest( "http://localhost:" + session.getSelendroidServerPort() + "/wd/hub/session/" + sessionId, HttpMethod.DELETE); } catch (Exception e) { log.log(Level.WARNING, "Error stopping session, safe to ignore", e); } deviceStore.release(session.getDevice(), session.getAut()); sessions.remove(sessionId); } } public void quitSelendroid() { List<String> sessionsToQuit = Lists.newArrayList(sessions.keySet()); if (!sessionsToQuit.isEmpty()) { for (String sessionId : sessionsToQuit) { try { stopSession(sessionId); } catch (AndroidDeviceException e) { log.log(Level.SEVERE, "Error occurred while stopping session", e); } } } deviceManager.shutdown(); } public SelendroidCapabilities getSessionCapabilities(String sessionId) { if (sessions.containsKey(sessionId)) { return sessions.get(sessionId).getDesiredCapabilities(); } return null; } public ActiveSession getActiveSession(String sessionId) { if (sessionId != null && sessions.containsKey(sessionId)) { return sessions.get(sessionId); } return null; } @Override public synchronized JSONArray getSupportedApps() { JSONArray list = new JSONArray(); for (AndroidApp app : appsStore.values()) { JSONObject appInfo = new JSONObject(); try { appInfo.put(APP_ID, app.getAppId()); appInfo.put(APP_BASE_PACKAGE, app.getBasePackage()); appInfo.put("mainActivity", app.getMainActivity()); list.put(appInfo); } catch (Exception e) { } } return list; } @Override public synchronized JSONArray getSupportedDevices() { JSONArray list = new JSONArray(); for (AndroidDevice device : deviceStore.getDevices()) { JSONObject deviceInfo = new JSONObject(); try { if (device instanceof DefaultAndroidEmulator) { deviceInfo.put(SelendroidCapabilities.EMULATOR, true); deviceInfo.put("avdName", ((DefaultAndroidEmulator) device).getAvdName()); } else { deviceInfo.put(SelendroidCapabilities.EMULATOR, false); deviceInfo.put(SelendroidCapabilities.MODEL, ((DefaultHardwareDevice) device).getModel()); deviceInfo.put(SelendroidCapabilities.SERIAL,((DefaultHardwareDevice) device).getSerial()); } deviceInfo.put(SelendroidCapabilities.API_TARGET_TYPE, device.getAPITargetType()); deviceInfo .put(SelendroidCapabilities.PLATFORM_VERSION, device.getTargetPlatform().getApi()); deviceInfo.put(SelendroidCapabilities.SCREEN_SIZE, device.getScreenSize()); list.put(deviceInfo); } catch (Exception e) { log.info("Error occurred when building supported device info: " + e.getMessage()); } } return list; } protected ActiveSession findActiveSession(AndroidDevice device) { for (ActiveSession session : sessions.values()) { if (session.getDevice().equals(device)) { return session; } } return null; } public byte[] takeScreenshot(String sessionId) throws AndroidDeviceException { if (sessionId == null || !sessions.containsKey(sessionId)) { throw new SelendroidException("The given session id '" + sessionId + "' was not found."); } return sessions.get(sessionId).getDevice().takeScreenshot(); } public void setEventListener(SelendroidStandaloneDriverEventListener eventListener) { this.eventListener = eventListener; } }