/*
 * 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 android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Build;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.*;
import io.selendroid.server.ServerInstrumentation;
import io.selendroid.server.android.*;
import io.selendroid.server.android.internal.DomWindow;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.server.common.exceptions.StaleElementReferenceException;
import io.selendroid.server.common.exceptions.TimeoutException;
import io.selendroid.server.model.internal.WebViewHandleMapper;
import io.selendroid.server.model.js.AndroidAtoms;
import io.selendroid.server.util.SelendroidLogger;
import org.apache.cordova.CordovaChromeClient;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewClient;
import org.apache.cordova.engine.SystemWebChromeClient;
import org.apache.cordova.engine.SystemWebView;
import org.apache.cordova.engine.SystemWebViewClient;
import org.apache.cordova.engine.SystemWebViewEngine;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Field;
import java.util.*;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

public class SelendroidWebDriver {
  private static final String ELEMENT_KEY = "ELEMENT";
  private static final long FOCUS_TIMEOUT = 1000L;
  private static final long POLLING_INTERVAL = 50L;
  private static final long START_LOADING_TIMEOUT = 700L;
  static final long UI_TIMEOUT = 3000L;
  private volatile boolean pageDoneLoading;
  private volatile boolean pageStartedLoading;
  private volatile String result;
  private volatile WebView webview = null;
  private static final String WINDOW_KEY = "WINDOW";
  private volatile boolean editAreaHasFocus;
  private final Object syncObject = new Object();
  private boolean done = false;
  private ServerInstrumentation serverInstrumentation = null;
  private SessionCookieManager sm = new SessionCookieManager();
  private WebChromeClient chromeClient = null;
  private DomWindow currentWindowOrFrame;
  private Queue<String> currentAlertMessage = new LinkedList<String>();
  private TouchScreen touch;
  private KeySender keySender;
  private MotionSender motionSender;
  private long scriptTimeout = 60000L;
  private long asyncScriptTimeout = 0L;
  private long pageLoadTimeout = 30000L;
  private final String contextHandle;

  public SelendroidWebDriver(ServerInstrumentation serverInstrumentation, String handle) {
    this.contextHandle = WebViewHandleMapper.normalizeHandle(handle);
    this.serverInstrumentation = serverInstrumentation;
    init(handle);
    keySender = new WebViewKeySender(serverInstrumentation, webview);
  }

  private static String escapeAndQuote(final String toWrap) {
    StringBuilder toReturn = new StringBuilder("\"");
    for (int i = 0; i < toWrap.length(); i++) {
      char c = toWrap.charAt(i);
      if (c == '\"') {
        toReturn.append("\\\"");
      } else if (c == '\\') {
        toReturn.append("\\\\");
      } else {
        toReturn.append(c);
      }
    }
    toReturn.append("\"");
    return toReturn.toString();
  }

  @SuppressWarnings("unchecked")
  private String convertToJsArgs(JSONArray args, KnownElements ke) throws JSONException {
    StringBuilder toReturn = new StringBuilder();

    int length = args.length();
    for (int i = 0; i < length; i++) {
      toReturn.append((i > 0) ? "," : "");
      toReturn.append(convertToJsArgs(args.get(i), ke));
    }
    SelendroidLogger.info("convertToJsArgs: " + toReturn.toString());
    return toReturn.toString();
  }

  private String convertToJsArgs(Object obj, KnownElements ke) throws JSONException {
    StringBuilder toReturn = new StringBuilder();
    if (obj == null || obj.equals(null)) {
      return "null";
    }
    if (obj instanceof JSONArray) {
      return convertToJsArgs((JSONArray) obj, ke);
    }
    if (obj instanceof List<?>) {
      toReturn.append("[");
      List<Object> aList = (List<Object>) obj;
      for (int j = 0; j < aList.size(); j++) {
        String comma = ((j == 0) ? "" : ",");
        toReturn.append(comma + convertToJsArgs(aList.get(j), ke));
      }
      toReturn.append("]");
    } else if (obj instanceof Map<?, ?>) {
      Map<Object, Object> aMap = (Map<Object, Object>) obj;
      String toAdd = "{";
      for (Object key : aMap.keySet()) {
        toAdd += key + ":" + convertToJsArgs(aMap.get(key), ke) + ",";
      }
      toReturn.append(toAdd.substring(0, toAdd.length() - 1) + "}");
    } else if (obj instanceof AndroidWebElement) {
      // A WebElement is represented in JavaScript by an Object as
      // follow: {"ELEMENT":"id"} where "id" refers to the id
      // of the HTML element in the javascript cache that can
      // be accessed throught bot.inject.cache.getCache_()
      toReturn.append("{\"" + ELEMENT_KEY + "\":\"" + ((AndroidWebElement) obj).getId() + "\"}");
    } else if (obj instanceof DomWindow) {
      // A DomWindow is represented in JavaScript by an Object as
      // follow {"WINDOW":"id"} where "id" refers to the id of the
      // DOM window in the cache.
      toReturn.append("{\"" + WINDOW_KEY + "\":\"" + ((DomWindow) obj).getKey() + "\"}");
    } else if (obj instanceof Number || obj instanceof Boolean) {
      toReturn.append(String.valueOf(obj));
    } else if (obj instanceof String) {
      toReturn.append(escapeAndQuote((String) obj));
    } else if (obj instanceof JSONObject) {
      if (((JSONObject) obj).has(ELEMENT_KEY)) {
        try {
          AndroidElement ae = ke.get(((JSONObject) obj).getString(ELEMENT_KEY));
          toReturn.append(ae.toString());
        } catch (JSONException e) {
          SelendroidLogger.info("exception getting the element id: " + e.toString());
        }
      } else {
        // send across the object since it's not a webelement
        toReturn.append(obj.toString());
      }
    } else {
      SelendroidLogger
          .info("failed to figure out what this is to convert to execute script:" + obj);
    }
    SelendroidLogger.info("convertToJsArgs: " + toReturn.toString());
    return toReturn.toString();
  }

  public String getContextHandle() {
    return contextHandle;
  }

  public Object executeAtom(AndroidAtoms atom, KnownElements ke, Object... args) {
    JSONArray array = new JSONArray();
    for (int i = 0; i < args.length; i++) {
      array.put(args[i]);
    }
    try {
      return executeAtom(atom, array, ke);
    } catch (JSONException je) {
      SelendroidLogger.error("Failed to execute atom", je);
      throw new RuntimeException(je);
    }
  }

  public Object executeAtom(AndroidAtoms atom, JSONArray args, KnownElements ke)
      throws JSONException {
    final String myScript = atom.getValue();
    String scriptInWindow =
        "(function(){ " + " var win; try{win=" + getWindowString() + "}catch(e){win=window;}"
            + "with(win){return (" + myScript + ")(" + convertToJsArgs(args, ke) + ")}})()";
    String jsResult =
        executeJavascriptInWebView("alert('selendroid<' + document.charset + '>:'+"
            + scriptInWindow + ")");


    SelendroidLogger.info("jsResult: " + jsResult);
    if (jsResult == null || "undefined".equals(jsResult)) {
      return null;
    }

    try {
      JSONObject json = new JSONObject(jsResult);
      if (0 != json.optInt("status")) {
        Object value = json.get("value");
        if ((value instanceof String && value.equals("Element does not exist in cache")) ||
            ( value instanceof JSONObject &&
                (((JSONObject) value).getString("message").equals("Element does not exist in cache") ||
                 ((JSONObject) value).getString("message").equals("Element is no longer attached to the DOM")))) {
          throw new StaleElementReferenceException(json.optString("value"));
        }
        throw new SelendroidException(json.optString("value"));
      }
      if (json.isNull("value")) {
        return null;
      } else {
        return json.get("value");
      }
    } catch (JSONException e) {
      throw new SelendroidException(e);
    }
  }

  private String executeJavascriptInWebView(final String script) {
    result = null;
    serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
      public void run() {
        if (webview.getUrl() == null) {
          return;
        }
        // needed in case the AUT re-set the WebChromeClient, which overrides selendroid's
        // which handles the onAlert, which is used to communicate response messages.
        // If this happens to cause undesired behavior for an end user, they should
        // switch back to NATIVE_APP and then to the webview again, this will allow
        // selendroid to wrap the 'new' chromeClient set by the AUT.
        webview.setWebChromeClient(chromeClient);
        webview.loadUrl("javascript:" + script);
      }
    });
    long timeout = System.currentTimeMillis() + scriptTimeout;
    synchronized (syncObject) {
      while (result == null && (System.currentTimeMillis() < timeout)) {
        try {
          syncObject.wait(2000);
        } catch (InterruptedException e) {
          throw new SelendroidException(e);
        }
      }

      return result;
    }
  }

  public Object executeScript(String script) {
    return injectJavascript(script, new JSONArray(), null);
  }

  public Object executeScript(String script, JSONArray args, KnownElements ke) {
    return injectJavascript(script, args, ke);
  }

  public Object executeScript(String script, Object args, KnownElements ke) {
    return injectJavascript(script, args, ke);
  }

  public String getCurrentUrl() {
    if (webview == null) {
      throw new SelendroidException("No open web view.");
    }
    long end = System.currentTimeMillis() + UI_TIMEOUT;
    final String[] url = new String[1];
    done = false;
    Runnable r = new Runnable() {
      public void run() {
        url[0] = webview.getUrl();
        synchronized (this) {
          this.notify();
        }
      }
    };
    runSynchronously(r, UI_TIMEOUT);
    return url[0];
  }


  public void get(final String url) {
    resetPageIsLoading();
    serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
      public void run() {
        webview.loadUrl(url);
      }
    });
    waitForPageToLoad();
  }

  public String getWindowSource() throws JSONException {
    JSONObject source =
        new JSONObject(
            (String) executeScript("return (new XMLSerializer()).serializeToString(document.documentElement);"));
    return source.getString("value");
  }

  protected void init(String handle) {
    SelendroidLogger.info("Selendroid webdriver init");

    webview = WebViewHandleMapper.getWebViewByHandle(handle);

    if (webview == null) {
      throw new SelendroidException("No webview found on current activity.");
    }
    configureWebView(webview);
    currentWindowOrFrame = new DomWindow("");
    motionSender = new WebViewMotionSender(webview, serverInstrumentation);
    touch = new AndroidTouchScreen(serverInstrumentation, motionSender);
  }

  public TouchScreen getTouch() {
    return touch;
  }

  KeySender getKeySender() {
    return keySender;
  }

  MotionSender getMotionSender() {
    return motionSender;
  }

  private void configureWebView(final WebView view) {
    serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {

      @Override
      public void run() {
        try {

          view.clearCache(true);
          view.clearFormData();
          view.clearHistory();
          view.setFocusable(true);
          view.setFocusableInTouchMode(true);
          view.setNetworkAvailable(true);

          try {
            chromeClient = encapulateWebChromeClientForView(view);
            view.setWebChromeClient(chromeClient);
            view.setWebViewClient(encapulateWebViewClientForView(view));
          } catch (ClassCastException cce) {
            // the view can potentially have the class name be an empty string
            // if the app declared its WebView inside another class
            if (cce.getMessage().contains("cannot be cast to org.apache.cordova.CordovaChromeClient")) {
              chromeClient = encapsulatedCordovaChromeClientForView((CordovaWebView) view);
              view.setWebChromeClient(chromeClient);
              try {
                view.setWebViewClient(encapulateCordovaWebViewClientForView((CordovaWebView) view));
              } catch (Exception e) {
                // ignore... it's not *as* important to override the WebViewClient
              }
            } else {
              throw cce;
            }
          }

          WebSettings settings = view.getSettings();
          settings.setJavaScriptCanOpenWindowsAutomatically(true);
          settings.setSupportMultipleWindows(true);
          settings.setBuiltInZoomControls(true);
          settings.setJavaScriptEnabled(true);
          settings.setAppCacheEnabled(true);
          settings.setAppCacheMaxSize(10 * 1024 * 1024);
          settings.setAppCachePath("");
          settings.setDatabaseEnabled(true);
          settings.setDomStorageEnabled(true);
          settings.setGeolocationEnabled(true);
          settings.setSaveFormData(false);
          settings.setSavePassword(false);
          settings.setRenderPriority(WebSettings.RenderPriority.HIGH);
          // Flash settings
          settings.setPluginState(WebSettings.PluginState.ON);

          // Geo location settings
          settings.setGeolocationEnabled(true);
          settings.setGeolocationDatabasePath("/data/data/selendroid");
        } catch (Exception e) {
          SelendroidLogger.error("Error configuring web view", e);
        }
      }
    });
  }

  private WebChromeClient encapulateWebChromeClientForView(WebView view) {
    if (view.getClass().getSimpleName().equalsIgnoreCase("CordovaWebView")) {
      return encapsulatedCordovaChromeClientForView((CordovaWebView)view);
    } else if (view.getClass().getSimpleName().equalsIgnoreCase("SystemWebView")) {
      return new ExtendedSystemWebChromeClient(new SystemWebViewEngine((SystemWebView)view));
    } else {
      try {
        Object webChromeClient = getWebClientViaReflection(view, "mWebChromeClient");
        if (webChromeClient != null) {
          return new WrappedChromeClient((WebChromeClient) webChromeClient);
        }
      } catch(Exception e) {}
      return new SelendroidWebChromeClient();
    }
  }

  private CordovaChromeClient encapsulatedCordovaChromeClientForView(CordovaWebView view) {
    try {
      if (reflectionGet(view, "chromeClient") != null) {
        return new WrappingCordovaChromeClient(null, view);
      }
    } catch (NoSuchFieldException nsfe) {}
    catch (IllegalAccessException iae) {}
    return new ExtendedCordovaChromeClient(view);
  }

  private WebViewClient encapulateWebViewClientForView(WebView view) {
    if (view.getClass().getSimpleName().equalsIgnoreCase("CordovaWebView")) {
      try {
        return encapulateCordovaWebViewClientForView((CordovaWebView) view);
      } catch (NoSuchFieldException nsfe) {}
      catch (IllegalAccessException iae) {}
      return new SelendroidWebClient();
    } else if (view.getClass().getSimpleName().equalsIgnoreCase("SystemWebView")) {
      return new SelendroidSystemWebViewClient((SystemWebView)view);
    } else {
      try {
        Object webViewClient = getWebClientViaReflection(view, "mWebViewClient");
        if (webViewClient != null) {
          return new WrappingSelendroidWebClient((WebViewClient) webViewClient);
        }
      } catch (NoSuchFieldException nsfe) {}
      catch (IllegalAccessException iae) {}
      return new SelendroidWebClient();
    }
  }

  private CordovaWebViewClient encapulateCordovaWebViewClientForView(CordovaWebView view) throws NoSuchFieldException, IllegalAccessException {
    if (reflectionGet(view, "viewClient") != null) {
      return new WrappingSelendroidCordovaWebClient(view);
    }
    return new SelendroidCordovaWebClient((CordovaInterface)reflectionGet(view, "cordova"), view);
  }

  private Object reflectionGet(Object object, String field) throws NoSuchFieldException, IllegalAccessException {
    Field f = null;
    Class clazz = object.getClass();
    NoSuchFieldException ex = null;
    // Look for the field in the object's class or its class hierarchy.
    // This is useful if you are testing a subclass of WebView.
    while (f == null && clazz != null) {
      try {
        f = clazz.getDeclaredField(field);
        break;
      } catch (NoSuchFieldException nsfe) {
        if (ex == null) {
          ex = nsfe;
        }
        clazz = clazz.getSuperclass();
      }
    }
    if (f == null) {
      throw ex;
    }
    f.setAccessible(true);
    return f.get(object);
  }

  private Object getWebClientViaReflection(WebView view, String clientField) throws NoSuchFieldException, IllegalAccessException {
    return reflectionGet(reflectionGet(reflectionGet(view, "mProvider"), "mContentsClientAdapter"), clientField);
  }

  private String getWindowString() {
    String window = "";
    if (!currentWindowOrFrame.getKey().equals("")) {
      window = "document['$wdc_']['" + currentWindowOrFrame.getKey() + "'] ||";
    }
    return (window += "window");
  }

  Object injectJavascript(String toExecute, Object args, KnownElements ke) {
    try {
      String executeScript = AndroidAtoms.EXECUTE_SCRIPT.getValue();
      toExecute =
          "var win_context; try{win_context= " + getWindowString() + "}catch(e){"
              + "win_context=window;}with(win_context){" + toExecute + "}";
      String wrappedScript =
          "(function(){ var win; try{win=" + getWindowString() + "}catch(e){win=window}"
              + "with(win){return (" + executeScript + ")(" + escapeAndQuote(toExecute) + ", ["
              + convertToJsArgs(args, ke) + "], true)}})()";
      return executeJavascriptInWebView("alert('selendroid<' + document.charset + '>:'+"
          + wrappedScript + ")");
    } catch (JSONException e) {
      SelendroidLogger.error("Failed to convert args to jsArgs", e);
      throw new RuntimeException(e);
    }
  }

  Object injectAtomJavascript(String toExecute, Object args, KnownElements ke) throws JSONException {
    return executeJavascriptInWebView("alert('selendroid<' + document.charset +'>:'+ (" + toExecute
        + ")(" + convertToJsArgs(args, ke) + "))");
  }

  public Object executeAsyncJavascript(String toExecute, JSONArray args, KnownElements ke) {
    try {
      String callbackFunction =
          "function(result){alert('selendroid<' + document.charset + '>:'+result);}";
      String script =
          "try {("
              + AndroidAtoms.EXECUTE_ASYNC_SCRIPT.getValue()
              + ")("
              + escapeAndQuote(toExecute)
              + ", ["
              + convertToJsArgs(args, ke)
              + "], "
              + asyncScriptTimeout
              + ", "
              + callbackFunction
              + ","
              + "true, "
              + getWindowString()
              + ")}catch(e){alert('selendroid<' + document.charset + '>:{\"status\":13,\"value\":\"' + e + '\"}')}";
      return executeJavascriptInWebView(script);
    } catch (JSONException je) {
      SelendroidLogger.error("Failed convert JSONArray to jsArgs", je);
      throw new RuntimeException(je);
    }
  }

  Boolean isInFrame() {
    return !currentWindowOrFrame.getKey().equals("");
  }

  void resetPageIsLoading() {
    pageStartedLoading = false;
    pageDoneLoading = false;
  }

  void setEditAreaHasFocus(boolean focused) {
    editAreaHasFocus = focused;
  }

  void waitForPageToLoad() {
    synchronized (syncObject) {
      long timeout = System.currentTimeMillis() + START_LOADING_TIMEOUT;
      while (!pageStartedLoading && (System.currentTimeMillis() < timeout)) {
        try {
          syncObject.wait(POLLING_INTERVAL);
        } catch (InterruptedException e) {
          throw new RuntimeException();
        }
      }
      long end = System.currentTimeMillis() + pageLoadTimeout;
      while (!pageDoneLoading && pageStartedLoading && (System.currentTimeMillis() < end)) {
        try {
          syncObject.wait(POLLING_INTERVAL);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }
      if (!pageDoneLoading && pageStartedLoading) {
        throw new TimeoutException(String.format("Timed out after %d seconds waiting for page to load",
          SECONDS.convert(pageLoadTimeout, MILLISECONDS)));
      }
    }
  }

  void waitUntilEditAreaHasFocus() {
    long timeout = System.currentTimeMillis() + FOCUS_TIMEOUT;
    while (!editAreaHasFocus && (System.currentTimeMillis() < timeout)) {
      try {
        Thread.sleep(POLLING_INTERVAL);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public class ExtendedCordovaChromeClient extends CordovaChromeClient {

    public Boolean callSuper = true;

    public ExtendedCordovaChromeClient(CordovaWebView app) {
      super(null, app);
    }

    /**
     * Unconventional way of adding a Javascript interface but the main reason why I took this way
     * is that it is working stable compared to the webview.addJavascriptInterface way.
     */
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult jsResult) {
      if (message != null && message.startsWith("selendroid<")) {
        jsResult.confirm();

        synchronized (syncObject) {
          String res = message.replaceFirst("selendroid<", "");
          int i = res.indexOf(">:");
          String enc = res.substring(0, i);
          res = res.substring(i + 2);
          /*
           * Workaround for Japanese character encodings: Replace U+00A5 with backslash so that we
           * can properly parse JSON strings contains backslash escapes, since WebKit maps 0x5C
           * (used for character escaping in all of the Japanses character encodings) to U+00A5 (YEN
           * SIGN) and breaks escape characters.
           */
          if (("EUC-JP".equals(enc) || "Shift_JIS".equals(enc) || "ISO-2022-JP".equals(enc))
              && res.contains("\u00a5")) {
            SelendroidLogger.info("Perform workaround for japanese character encodings");
            SelendroidLogger.debug("Original String: " + res);
            res = res.replace("\u00a5", "\\");
            SelendroidLogger.debug("Replaced result: " + res);
          }
          result = res;
          syncObject.notify();
        }

        return true;
      } else if (callSuper) {
        currentAlertMessage.add(message == null ? "null" : message);
        SelendroidLogger.info("new alert message: " + message);
        return super.onJsAlert(view, url, message, jsResult);
      } else {
        return false;
      }
    }
  }

  public class WrappingCordovaChromeClient extends CordovaChromeClient {

    private CordovaChromeClient WCC;
    private ExtendedCordovaChromeClient selendroidCCC;

    public WrappingCordovaChromeClient(CordovaInterface ci, CordovaWebView view) throws NoSuchFieldException, IllegalAccessException {
      super(ci, view);
      selendroidCCC = new ExtendedCordovaChromeClient(view);
      selendroidCCC.callSuper = false;
      WCC = (CordovaChromeClient) reflectionGet(view, "chromeClient");
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
      // if the alert was a selendroid one, this method will return true, otherwise false
      // and the alert should be propagated to the original WebChromeClient
      if (!selendroidCCC.onJsAlert(view, url, message, result)) {
        return WCC.onJsAlert(view, url, message, result);
      }
      return true;
    }

    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
      return WCC.onJsConfirm(view, url, message, result);
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
      return WCC.onJsPrompt(view, url, message, defaultValue, result);
    }

    @Override
    public void onProgressChanged(WebView view, int newProgress) {
      WCC.onProgressChanged(view, newProgress);
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
      WCC.onReceivedTitle(view, title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
      WCC.onReceivedIcon(view, icon);
    }

    @Override
    public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
      WCC.onReceivedTouchIconUrl(view, url, precomposed);
    }

    @Override
    public void onShowCustomView(View view, CustomViewCallback callback) {
      WCC.onShowCustomView(view, callback);
    }

    @Override
    public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        WCC.onShowCustomView(view, requestedOrientation, callback);
      }
    }

    @Override
    public void onHideCustomView() {
      WCC.onHideCustomView();
    }

    @Override
    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
      return WCC.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
    }

    @Override
    public void onRequestFocus(WebView view) {
      WCC.onRequestFocus(view);
    }

    @Override
    public void onCloseWindow(WebView window) {
      WCC.onCloseWindow(window);
    }

    @Override
    public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
      return WCC.onJsBeforeUnload(view, url, message, result);
    }

    @Override
    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) {
      WCC.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater);
    }

    @Override
    public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) {
      WCC.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater);
    }

    @Override
    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
      WCC.onGeolocationPermissionsShowPrompt(origin, callback);
    }

    @Override
    public void onGeolocationPermissionsHidePrompt() {
      WCC.onGeolocationPermissionsHidePrompt();
    }

// can't build for things above 4.1.1.4
//    @Override
//    public void onPermissionRequest(PermissionRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        WCC.onPermissionRequest(request);
//      }
//    }
//
//    @Override
//    public void onPermissionRequestCanceled(PermissionRequest request) {
//      WCC.onPermissionRequestCanceled(request);
//    }

    @Override
    public boolean onJsTimeout() {
      return WCC.onJsTimeout();
    }

    @Override
    public void onConsoleMessage(String message, int lineNumber, String sourceID) {
      WCC.onConsoleMessage(message, lineNumber, sourceID);
    }

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
      return WCC.onConsoleMessage(consoleMessage);
    }

    @Override
    public Bitmap getDefaultVideoPoster() {
      return WCC.getDefaultVideoPoster();
    }

    @Override
    public View getVideoLoadingProgressView() {
      return WCC.getVideoLoadingProgressView();
    }

    @Override
    public void getVisitedHistory(ValueCallback<String[]> callback) {
      WCC.getVisitedHistory(callback);
    }

// can't build over 4.1.1.4
//    @Override
//    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        return WCC.onShowFileChooser(webView, filePathCallback, fileChooserParams);
//      }
//      return false;
//    }
  }

  //Like ExtendedCordovaClient, but for Cordova 4.0.0
  public class ExtendedSystemWebChromeClient extends SystemWebChromeClient {


    public  ExtendedSystemWebChromeClient(SystemWebViewEngine parentEngine) {
      super(parentEngine);
    }

    /**
     * Unconventional way of adding a Javascript interface but the main reason why I took this way
     * is that it is working stable compared to the webview.addJavascriptInterface way.
     **/
   @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult jsResult) {
      if (message != null && message.startsWith("selendroid<")) {
        jsResult.confirm();

        synchronized (syncObject) {
          String res = message.replaceFirst("selendroid<", "");
          int i = res.indexOf(">:");
          String enc = res.substring(0, i);
          res = res.substring(i + 2);

          /* Workaround for Japanese character encodings: Replace U+00A5 with backslash so that we
           * can properly parse JSON strings contains backslash escapes, since WebKit maps 0x5C
           * (used for character escaping in all of the Japanses character encodings) to U+00A5 (YEN
           * SIGN) and breaks escape characters.
           */
          if (("EUC-JP".equals(enc) || "Shift_JIS".equals(enc) || "ISO-2022-JP".equals(enc))
              && res.contains("\u00a5")) {
            SelendroidLogger.info("Perform workaround for japanese character encodings");
            SelendroidLogger.debug("Original String: " + res);
            res = res.replace("\u00a5", "\\");
            SelendroidLogger.debug("Replaced result: " + res);
          }
          result = res;
          syncObject.notify();
        }

        return true;
      } else {
        currentAlertMessage.add(message == null ? "null" : message);
        SelendroidLogger.info("new alert message: " + message);
        return super.onJsAlert(view, url, message, jsResult);
      }
    }
  }

  public class SelendroidSystemWebViewClient extends SystemWebViewClient {
    public SelendroidSystemWebViewClient(SystemWebView view) {
      super(new SystemWebViewEngine(view));
    }
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      synchronized (syncObject) {
        pageStartedLoading = true;
        syncObject.notify();
      }
    }
    @Override
    public void onPageFinished(WebView view, String url) {
      synchronized (syncObject) {
        pageDoneLoading = true;
        syncObject.notify();
      }
    }
  }

  public class SelendroidCordovaWebClient extends CordovaWebViewClient {

    public SelendroidCordovaWebClient(CordovaInterface cordova, CordovaWebView view) {
      super(cordova, view);
    }
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      synchronized (syncObject) {
        pageStartedLoading = true;
        syncObject.notify();
      }
    }
    @Override
    public void onPageFinished(WebView view, String url) {
      synchronized (syncObject) {
        pageDoneLoading = true;
        syncObject.notify();
      }
    }
  }

  public class WrappingSelendroidCordovaWebClient extends CordovaWebViewClient {
    private CordovaWebViewClient selendroidCWVC;
    private CordovaWebViewClient CWVC;

    public WrappingSelendroidCordovaWebClient(CordovaWebView webView) throws NoSuchFieldException, IllegalAccessException {
      super(null, webView);
      CWVC = (CordovaWebViewClient)reflectionGet(webView, "viewClient");
      selendroidCWVC = new SelendroidCordovaWebClient(null, webView);
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      selendroidCWVC.onPageStarted(view, url, favicon);
      CWVC.onPageStarted(view, url, favicon);
    }

    @Override
    public void onPageFinished(WebView view, String url) {
      selendroidCWVC.onPageFinished(view, url);
      CWVC.onPageFinished(view, url);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      return CWVC.shouldOverrideUrlLoading(view, url);
    }

    @Override
    public void onLoadResource(WebView view, String url) {
      CWVC.onLoadResource(view, url);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onPageCommitVisible(WebView view, String url) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        CWVC.onPageCommitVisible(view, url);
//      }
//    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        return CWVC.shouldInterceptRequest(view, url);
      }
      return null;
    }

// can't build over 4.1.1.4
//    @Override
//    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        return CWVC.shouldInterceptRequest(view, request);
//      }
//      return null;
//    }

    @Override
    public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) {
      CWVC.onTooManyRedirects(view, cancelMsg, continueMsg);
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
      CWVC.onReceivedError(view, errorCode, description, failingUrl);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        CWVC.onReceivedError(view, request, error);
//      }
//    }
//
//    @Override
//    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        CWVC.onReceivedHttpError(view, request, errorResponse);
//      }
//    }

    @Override
    public void onFormResubmission(WebView view, Message dontResend, Message resend) {
      CWVC.onFormResubmission(view, dontResend, resend);
    }

    @Override
    public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
      CWVC.doUpdateVisitedHistory(view, url, isReload);
    }

    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
      CWVC.onReceivedSslError(view, handler, error);
    }

// can't build above 4.1.1.4 with maven! actually a good reason to switch to gradle.
//    @Override
//    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        CWVC.onReceivedClientCertRequest(view, request);
//      }
//    }

    @Override
    public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
      CWVC.onReceivedHttpAuthRequest(view, handler, host, realm);
    }

    @Override
    public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
      return CWVC.shouldOverrideKeyEvent(view, event);
    }

    @Override
    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
      CWVC.onUnhandledKeyEvent(view, event);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onUnhandledInputEvent(WebView view, InputEvent event) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        CWVC.onUnhandledInputEvent(view, event);
//      }
//    }

    @Override
    public void onScaleChanged(WebView view, float oldScale, float newScale) {
      CWVC.onScaleChanged(view, oldScale, newScale);
    }

    @Override
    public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        CWVC.onReceivedLoginRequest(view, realm, account, args);
      }
    }
  }

  public class SelendroidWebChromeClient extends WebChromeClient {

    private boolean callSuper = true;

    public SelendroidWebChromeClient() {
    }

    public SelendroidWebChromeClient(boolean callSuper) {
      this.callSuper = callSuper;
    }

    /**
     * Unconventional way of adding a Javascript interface but the main reason why I took this way
     * is that it is working stable compared to the webview.addJavascriptInterface way.
     */
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult jsResult) {
      if (message != null && message.startsWith("selendroid<")) {
        jsResult.confirm();

        synchronized (syncObject) {
          String res = message.replaceFirst("selendroid<", "");
          int i = res.indexOf(">:");
          String enc = res.substring(0, i);
          res = res.substring(i + 2);
          /*
           * Workaround for Japanese character encodings: Replace U+00A5 with backslash so that we
           * can properly parse JSON strings contains backslash escapes, since WebKit maps 0x5C
           * (used for character escaping in all of the Japanses character encodings) to U+00A5 (YEN
           * SIGN) and breaks escape characters.
           */
          if (("EUC-JP".equals(enc) || "Shift_JIS".equals(enc) || "ISO-2022-JP".equals(enc))
              && res.contains("\u00a5")) {
            SelendroidLogger.info("Perform workaround for japanese character encodings");
            SelendroidLogger.debug("Original String: " + res);
            res = res.replace("\u00a5", "\\");
            SelendroidLogger.debug("Replaced result: " + res);
          }
          result = res;
          syncObject.notify();
        }

        return true;
      } else if (callSuper){
        currentAlertMessage.add(message == null ? "null" : message);
        SelendroidLogger.info("new alert message: " + message);
        return super.onJsAlert(view, url, message, jsResult);
      }
      return false;
    }

    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
      currentAlertMessage.add(message == null ? "null" : message);
      SelendroidLogger.info("new confirm message: " + message);
      if (callSuper) {
        return super.onJsConfirm(view, url, message, result);
      }
      return false;
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
        JsPromptResult result) {
      currentAlertMessage.add(message == null ? "null" : message);
      SelendroidLogger.info("new prompt message: " + message);
      if (callSuper) {
        return super.onJsPrompt(view, url, message, defaultValue, result);
      }
      return false;
    }

  }

  // Would be so awesome to use a Proxy here and just intercept the two methods we care about.
  // alas, WebChromeClient is a concrete class and there's no interface provided.
  // And trying to do alternatives in Android proved to be too difficult
  public class WrappedChromeClient extends WebChromeClient {

    private WebChromeClient WCC;
    private WebChromeClient selendroidWCC = new SelendroidWebChromeClient(false);

    public WrappedChromeClient(WebChromeClient webChromeClient) {
      WCC = webChromeClient;
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
      // if the alert was a selendroid one, this method will return true, otherwise false
      // and the alert should be propagated to the original WebChromeClient
      if (!selendroidWCC.onJsAlert(view, url, message, result)) {
        return WCC.onJsAlert(view, url, message, result);
      }
      return true;
    }

    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
      selendroidWCC.onJsConfirm(view, url, message, result);
      return WCC.onJsConfirm(view, url, message, result);
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
      selendroidWCC.onJsPrompt(view, url, message, defaultValue, result);
      return WCC.onJsPrompt(view, url, message, defaultValue, result);
    }

    @Override
    public void onProgressChanged(WebView view, int newProgress) {
      WCC.onProgressChanged(view, newProgress);
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
      WCC.onReceivedTitle(view, title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
      WCC.onReceivedIcon(view, icon);
    }

    @Override
    public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
      WCC.onReceivedTouchIconUrl(view, url, precomposed);
    }

    @Override
    public void onShowCustomView(View view, CustomViewCallback callback) {
      WCC.onShowCustomView(view, callback);
    }

    @Override
    public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        WCC.onShowCustomView(view, requestedOrientation, callback);
      }
    }

    @Override
    public void onHideCustomView() {
      WCC.onHideCustomView();
    }

    @Override
    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
      return WCC.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
    }

    @Override
    public void onRequestFocus(WebView view) {
      WCC.onRequestFocus(view);
    }

    @Override
    public void onCloseWindow(WebView window) {
      WCC.onCloseWindow(window);
    }

    @Override
    public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
      return WCC.onJsBeforeUnload(view, url, message, result);
    }

    @Override
    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) {
      WCC.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater);
    }

    @Override
    public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) {
      WCC.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater);
    }

    @Override
    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
      WCC.onGeolocationPermissionsShowPrompt(origin, callback);
    }

    @Override
    public void onGeolocationPermissionsHidePrompt() {
      WCC.onGeolocationPermissionsHidePrompt();
    }

// can't build for things above 4.1.1.4
//    @Override
//    public void onPermissionRequest(PermissionRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        WCC.onPermissionRequest(request);
//      }
//    }
//
//    @Override
//    public void onPermissionRequestCanceled(PermissionRequest request) {
//      WCC.onPermissionRequestCanceled(request);
//    }

    @Override
    public boolean onJsTimeout() {
      return WCC.onJsTimeout();
    }

    @Override
    public void onConsoleMessage(String message, int lineNumber, String sourceID) {
      WCC.onConsoleMessage(message, lineNumber, sourceID);
    }

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
      return WCC.onConsoleMessage(consoleMessage);
    }

    @Override
    public Bitmap getDefaultVideoPoster() {
      return WCC.getDefaultVideoPoster();
    }

    @Override
    public View getVideoLoadingProgressView() {
      return WCC.getVideoLoadingProgressView();
    }

    @Override
    public void getVisitedHistory(ValueCallback<String[]> callback) {
      WCC.getVisitedHistory(callback);
    }

// can't build over 4.1.1.4
//    @Override
//    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        return WCC.onShowFileChooser(webView, filePathCallback, fileChooserParams);
//      }
//      return false;
//    }
  }

  public class SelendroidWebClient extends WebViewClient {
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      synchronized (syncObject) {
        pageStartedLoading = true;
        syncObject.notify();
      }
    }
    @Override
    public void onPageFinished(WebView view, String url) {
      synchronized (syncObject) {
        pageDoneLoading = true;
        syncObject.notify();
      }
    }
  }

  // Would be so awesome to use a Proxy here and just intercept the two methods we care about.
  // alas, WebViewClient is a concrete class and there's no interface provided.
  // And trying to do alternatives in Android proved to be too difficult
  public class WrappingSelendroidWebClient extends WebViewClient {
    private WebViewClient selendroidWVC = new SelendroidWebClient();
    private WebViewClient WVC;

    public WrappingSelendroidWebClient(WebViewClient webViewClient) {
      WVC = webViewClient;
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
      selendroidWVC.onPageStarted(view, url, favicon);
      WVC.onPageStarted(view, url, favicon);
    }

    @Override
    public void onPageFinished(WebView view, String url) {
      selendroidWVC.onPageFinished(view, url);
      WVC.onPageFinished(view, url);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      return WVC.shouldOverrideUrlLoading(view, url);
    }

    @Override
    public void onLoadResource(WebView view, String url) {
      WVC.onLoadResource(view, url);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onPageCommitVisible(WebView view, String url) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        WVC.onPageCommitVisible(view, url);
//      }
//    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        return WVC.shouldInterceptRequest(view, url);
      }
      return null;
    }

// can't build over 4.1.1.4
//    @Override
//    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        return WVC.shouldInterceptRequest(view, request);
//      }
//      return null;
//    }

    @Override
    public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) {
      WVC.onTooManyRedirects(view, cancelMsg, continueMsg);
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
      WVC.onReceivedError(view, errorCode, description, failingUrl);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        WVC.onReceivedError(view, request, error);
//      }
//    }
//
//    @Override
//    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
//      if (Build.VERSION.SDK_INT >= 23) {
//        WVC.onReceivedHttpError(view, request, errorResponse);
//      }
//    }

    @Override
    public void onFormResubmission(WebView view, Message dontResend, Message resend) {
      WVC.onFormResubmission(view, dontResend, resend);
    }

    @Override
    public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
      WVC.doUpdateVisitedHistory(view, url, isReload);
    }

    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
      WVC.onReceivedSslError(view, handler, error);
    }

// can't build above 4.1.1.4 with maven! actually a good reason to switch to gradle.
//    @Override
//    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        WVC.onReceivedClientCertRequest(view, request);
//      }
//    }

    @Override
    public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
      WVC.onReceivedHttpAuthRequest(view, handler, host, realm);
    }

    @Override
    public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
      return WVC.shouldOverrideKeyEvent(view, event);
    }

    @Override
    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
      WVC.onUnhandledKeyEvent(view, event);
    }

// can't build over 4.1.1.4
//    @Override
//    public void onUnhandledInputEvent(WebView view, InputEvent event) {
//      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//        WVC.onUnhandledInputEvent(view, event);
//      }
//    }

    @Override
    public void onScaleChanged(WebView view, float oldScale, float newScale) {
      WVC.onScaleChanged(view, oldScale, newScale);
    }

    @Override
    public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        WVC.onReceivedLoginRequest(view, realm, account, args);
      }
    }
  }


  public String getTitle() {
    if (webview == null) {
      throw new SelendroidException("No open web view.");
    }
    long end = System.currentTimeMillis() + UI_TIMEOUT;
    final String[] title = new String[1];
    done = false;
    serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
      public void run() {
        synchronized (syncObject) {
          title[0] = webview.getTitle();
          done = true;
          syncObject.notify();
        }
      }
    });
    waitForDone(end, UI_TIMEOUT, "Failed to get title");
    return title[0];
  }

  private void waitForDone(long end, long timeout, String error) {
    synchronized (syncObject) {
      while (!done && System.currentTimeMillis() < end) {
        try {
          syncObject.wait(timeout);
        } catch (InterruptedException e) {
          throw new SelendroidException(error, e);
        }
      }
    }
  }

  private void runSynchronously(Runnable r, long timeout) {
    synchronized (r) {
      serverInstrumentation.getCurrentActivity().runOnUiThread(r);
      try {
        r.wait(timeout);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    }
  }

  public WebView getWebview() {
    return webview;
  }

  public Set<Cookie> getCookies(String url) {

    return sm.getAllCookies(url);

  }

  public void removeAllCookie(String url) {

    sm.removeAllCookies(url);

  }

  public void remove(String url, String name) {

    sm.remove(url, name);

  }

  public void setCookies(String url, Cookie cookie) {

    sm.addCookie(url, cookie);

  }

  public void frame(int index) throws JSONException {
    currentWindowOrFrame =
        processFrameExecutionResult(injectAtomJavascript(AndroidAtoms.FRAME_BY_INDEX.getValue(),
            index, null));
  }

  public void frame(String frameNameOrId) throws JSONException {
    currentWindowOrFrame =
        processFrameExecutionResult(injectAtomJavascript(
            AndroidAtoms.FRAME_BY_ID_OR_NAME.getValue(), frameNameOrId, null));
  }

  public void frame(AndroidWebElement frameElement) {
    currentWindowOrFrame =
        processFrameExecutionResult(executeScript("return arguments[0].contentWindow;",
            frameElement, null));
  }

  public void switchToDefaultContent() {
    currentWindowOrFrame = new DomWindow("");
  }

  private DomWindow processFrameExecutionResult(Object result) {
    if (result == null || "undefined".equals(result)) {
      return null;
    }
    try {
      JSONObject json = new JSONObject((String) result);
      JSONObject value = json.getJSONObject("value");
      return new DomWindow(value.getString("WINDOW"));
    } catch (JSONException e) {
      throw new RuntimeException("Failed to parse JavaScript result: " + result.toString(), e);
    }
  }

  public void back() {
    resetPageIsLoading();
    runSynchronously(new Runnable() {
      public void run() {
        webview.goBack();
      }
    }, 500);
    waitForPageToLoad();
  }

  public void forward() {
    resetPageIsLoading();
    runSynchronously(new Runnable() {
      public void run() {
        webview.goForward();
      }
    }, 500);
    waitForPageToLoad();
  }

  public void refresh() {
    resetPageIsLoading();
    runSynchronously(new Runnable() {
      public void run() {
        webview.reload();
      }
    }, 500);
    waitForPageToLoad();
  }

  public boolean isAlertPresent() {
    SelendroidLogger.info("checking currentAlertMessage: " + currentAlertMessage.size());
    return !currentAlertMessage.isEmpty();
  }

  public String getCurrentAlertMessage() {
    SelendroidLogger.info("getting currentAlertMessage: " + currentAlertMessage.peek());
    return currentAlertMessage.peek();
  }

  public void clearCurrentAlertMessage() {
    SelendroidLogger.info("clearing the current alert message: " + currentAlertMessage.remove());
  }

  public void setAsyncScriptTimeout(long timeout) {
    asyncScriptTimeout = timeout;
  }

  public void setPageLoadTimeout(long timeout) {
    pageLoadTimeout = timeout;
  }
}
