All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.selendroid.server.model.SelendroidStandaloneDriver Maven / Gradle / Ivy

There is a newer version: 0.17.0
Show newest version
/*
 * 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.server.model;

import io.selendroid.SelendroidCapabilities;
import io.selendroid.SelendroidConfiguration;
import io.selendroid.android.AndroidApp;
import io.selendroid.android.AndroidDevice;
import io.selendroid.android.AndroidEmulator;
import io.selendroid.android.AndroidSdk;
import io.selendroid.android.DeviceManager;
import io.selendroid.android.impl.DefaultAndroidApp;
import io.selendroid.android.impl.DefaultAndroidEmulator;
import io.selendroid.android.impl.DefaultDeviceManager;
import io.selendroid.android.impl.DefaultHardwareDevice;
import io.selendroid.android.impl.MultiActivityAndroidApp;
import io.selendroid.builder.AndroidDriverAPKBuilder;
import io.selendroid.builder.SelendroidServerBuilder;
import io.selendroid.exceptions.AndroidDeviceException;
import io.selendroid.exceptions.AndroidSdkException;
import io.selendroid.exceptions.DeviceStoreException;
import io.selendroid.exceptions.SelendroidException;
import io.selendroid.exceptions.SessionNotCreatedException;
import io.selendroid.exceptions.ShellCommandException;
import io.selendroid.log.LogLevelEnum;
import io.selendroid.server.ServerDetails;
import io.selendroid.server.util.HttpClientUtil;

import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.logging.Logger;

import org.jboss.netty.handler.codec.http.HttpMethod;
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 com.beust.jcommander.internal.Lists;

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";
  private static int selendroidServerPort = 38080;
  private static final Logger log = Logger.getLogger(SelendroidStandaloneDriver.class.getName());
  private Map appsStore = new HashMap();
  private Map selendroidServers = new HashMap();
  private Map sessions = new HashMap();
  private DeviceStore deviceStore = null;
  private SelendroidServerBuilder selendroidApkBuilder = null;
  private AndroidDriverAPKBuilder androidDriverAPKBuilder = null;
  private SelendroidConfiguration serverConfiguration = null;
  private DeviceManager deviceManager;


  public SelendroidStandaloneDriver(SelendroidConfiguration serverConfiguration)
      throws AndroidSdkException, AndroidDeviceException {
    this.serverConfiguration = serverConfiguration;
    selendroidApkBuilder = new SelendroidServerBuilder(serverConfiguration);
    androidDriverAPKBuilder = new AndroidDriverAPKBuilder();

    selendroidServerPort = serverConfiguration.getSelendroidServerPort();

    initApplicationsUnderTest(serverConfiguration);
    initAndroidDevices();
    deviceStore.setClearData(!serverConfiguration.isNoClearData());
  }

  /**
   * For testing only
   */
  SelendroidStandaloneDriver(SelendroidServerBuilder builder, DeviceManager deviceManager,
                             AndroidDriverAPKBuilder androidDriverAPKBuilder) {
    this.selendroidApkBuilder = builder;
    this.deviceManager = deviceManager;
    this.androidDriverAPKBuilder = androidDriverAPKBuilder;
  }

  /* 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()) {

        AndroidApp app = null;
        try {
          app = selendroidApkBuilder.resignApp(file);
        } catch (ShellCommandException e1) {
          throw new SessionNotCreatedException("An error occurred while resigning the app '"
                                               + file.getName() + "'. ", e1);
        }
        String appId = null;
        try {
          appId = app.getAppId();
        } catch (SelendroidException 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.");
        }
      } 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
        AndroidApp app =
            selendroidApkBuilder.resignApp(androidDriverAPKBuilder.extractAndroidDriverAPK());
        appsStore.put(BrowserType.ANDROID, app);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    } else if (appsStore.isEmpty()) {
      // note this only happens now when someone uses -noWebViewApp & forgets to specify -aut/app
      // (or the app doesn't exist or some other error condition above ^ )
      throw new SelendroidException(
          "Fatal error initializing SelendroidDriver: configured app(s) have not been found.");
    }

  }

  /* 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 selendroidApkBuilder.getJarVersionNumber();
  }

  @Override
  public String getCpuArch() {
    String arch = System.getProperty("os.arch");
    return arch;
  }

  @Override
  public String getOsVersion() {
    String os = System.getProperty("os.version");
    return os;
  }

  @Override
  public String getOsName() {
    String os = System.getProperty("os.name");
    return os;
  }

  protected SelendroidConfiguration getSelendroidConfiguration() {
    return serverConfiguration;
  }

  public String createNewTestSession(JSONObject caps, Integer retries) throws AndroidSdkException,
                                                                              JSONException {
    SelendroidCapabilities desiredCapabilities = null;

    // Convert the JSON capabilities to SelendroidCapabilities
    try {
      desiredCapabilities = new SelendroidCapabilities(caps);
    } catch (JSONException e) {
      throw new SelendroidException("Desired capabilities cannot be parsed.");
    }

    // Find the App being requested for use
    AndroidApp app = appsStore.get(desiredCapabilities.getAut());
    if (app == null) {
      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);

    // Find a device to match the capabilities
    AndroidDevice device = null;
    try {
      device = getAndroidDevice(desiredCapabilities);
    } catch (AndroidDeviceException e) {
      SessionNotCreatedException error =
          new SessionNotCreatedException("Error occured while finding android device: "
                                         + e.getMessage());
      e.printStackTrace();
      log.severe(error.getMessage());
      throw error;
    }

    // If we are using an emulator need to start it up
    if (device instanceof AndroidEmulator) {
      AndroidEmulator emulator = (AndroidEmulator) device;
      try {
        if (emulator.isEmulatorStarted()) {
          emulator.unlockEmulatorScreen();
        } else {
          Map config = new HashMap();
          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);
        }
      } catch (AndroidDeviceException e) {
        deviceStore.release(device, app);
        if (retries > 0) {
          return createNewTestSession(caps, retries - 1);
        }
        throw new SessionNotCreatedException("Error occured while interacting with the emulator: "
                                             + emulator + ": " + e.getMessage());
      }
      emulator.setIDevice(deviceManager.getVirtualDevice(emulator.getAvdName()));
    }
    boolean appInstalledOnDevice = device.isInstalled(app);
    if (!appInstalledOnDevice || serverConfiguration.isForceReinstall()) {
      device.install(app);
    } else {
      log.info("the app under test is already installed.");
    }

    int port = getNextSelendroidServerPort();
    Boolean selendroidInstalledSuccessfully =
        device.isInstalled("io.selendroid." + app.getBasePackage());
    if (!selendroidInstalledSuccessfully || serverConfiguration.isForceReinstall()) {
      AndroidApp selendroidServer = createSelendroidServerApk(app);

      selendroidInstalledSuccessfully = device.install(selendroidServer);
      if (!selendroidInstalledSuccessfully) {
        if (!device.install(selendroidServer)) {
          deviceStore.release(device, app);

          if (retries > 0) {
            return createNewTestSession(caps, retries - 1);
          }
        }
      }
    } else {
      log.info(
          "selendroid-server will not be created and installed because it already exists for the app under test.");
    }

    // Run any adb commands requested in the capabilities
    List adbCommands = new ArrayList();
    adbCommands.add("shell setprop log.tag.SELENDROID " + serverConfiguration.getLogLevel().name());
    adbCommands.addAll(desiredCapabilities.getPreSessionAdbCommands());

    for (String adbCommandParameter : adbCommands) {
      device.runAdbCommand(adbCommandParameter);
    }

    // It's GO TIME!
    // start the selendroid server on the device and make sure it's up
    try {
      device.startSelendroid(app, port);
    } catch (AndroidSdkException e) {
      log.info("error while starting selendroid: " + e.getMessage());

      deviceStore.release(device, app);
      if (retries > 0) {
        return createNewTestSession(caps, retries - 1);
      }
      throw new SessionNotCreatedException("Error occurred while starting instrumentation: "
                                           + e.getMessage());
    }
    long start = System.currentTimeMillis();
    long startTimeOut = 20000;
    long timemoutEnd = start + startTimeOut;
    while (device.isSelendroidRunning() == false) {
      if (timemoutEnd >= System.currentTimeMillis()) {
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
      } else {
        throw new SelendroidException("Selendroid server on the device didn't came up after "
                                      + startTimeOut / 1000 + "sec:");
      }
    }

    // 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) {
      e1.printStackTrace();
    }

    // create the new session on the device server
    RemoteWebDriver driver;
    try {
      driver =
          new RemoteWebDriver(new URL("http://localhost:" + port + "/wd/hub"), desiredCapabilities);
    } catch (Exception e) {
      e.printStackTrace();
      deviceStore.release(device, app);
      throw new SessionNotCreatedException(
          "Error occurred while creating session on Android device", e);
    }
    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())) {
      // 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")));
    }

    return sessionId;
  }

  /**
   * 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) {
	  AndroidApp returnApp = app;
	  // override mainActivity of the app
	  if (desiredCapabilities.getLaunchActivity() != null) {
		  MultiActivityAndroidApp augmentedApp = new MultiActivityAndroidApp((DefaultAndroidApp)returnApp);
		  augmentedApp.setMainActivity(desiredCapabilities.getLaunchActivity());
		  // set the app for return
		  returnApp = augmentedApp;
	  }
	  return returnApp;
}

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) {
        e.printStackTrace();
        throw new SessionNotCreatedException(
            "An error occurred while building the selendroid-server.apk for aut '" + aut + "': "
            + e.getMessage()
        );
      }
    }
    return selendroidServers.get(aut.getAppId());
  }

  private Locale parseLocale(SelendroidCapabilities capa) {
    if (capa.getLocale() == null) {
      return null;
    }
    String[] localeStr = capa.getLocale().split("_");
    Locale locale = new Locale(localeStr[0], localeStr[1]);

    return locale;
  }

  /* package */AndroidDevice getAndroidDevice(SelendroidCapabilities caps)
      throws AndroidDeviceException {
    AndroidDevice device = null;
    try {
      device = deviceStore.findAndroidDevice(caps);
    } catch (DeviceStoreException e) {
      e.printStackTrace();
      log.fine(caps.getRawCapabilities().toString());
      throw new AndroidDeviceException("Error occurred while looking for devices/emulators.", e);
    }

    return device;
  }

  /**
   * For testing only
   */
  /* package */Map 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 getActiveSessions() {
    return Lists.newArrayList(sessions.values());
  }

  public boolean isValidSession(String sessionId) {
    if (sessionId != null && sessionId.isEmpty() == false) {
      return sessions.containsKey(sessionId);
    }
    return false;
  }

  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) {
        // can happen, ignore
      }
      deviceStore.release(session.getDevice(), session.getAut());

      // remove session
      sessions.remove(sessionId);
      session = null;
    }
  }

  public void quitSelendroid() {
    List sessionsToQuit = Lists.newArrayList(sessions.keySet());
    if (sessionsToQuit != null && sessionsToQuit.isEmpty() == false) {
      for (String sessionId : sessionsToQuit) {
        try {
          stopSession(sessionId);
        } catch (AndroidDeviceException e) {
          log.severe("Error occured while stopping session: " + e.getMessage());
          e.printStackTrace();
        }
      }
    }
    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("appId", app.getAppId());
        appInfo.put("basePackage", 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("model", ((DefaultHardwareDevice) device).getModel());
        }
        deviceInfo
            .put(SelendroidCapabilities.PLATFORM_VERSION, device.getTargetPlatform().getApi());
        deviceInfo.put(SelendroidCapabilities.SCREEN_SIZE, device.getScreenSize());

        list.put(deviceInfo);
      } catch (Exception e) {
        log.info("Error occured when building suported 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) == false) {
      throw new SelendroidException("The gicen session id '" + sessionId + "' was not found.");
    }
    return sessions.get(sessionId).getDevice().takeScreenshot();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy