"use strict";

var errors = require('./errors')
  , adb = require('../android/adb')
  , _ = require('underscore')
  , logger = require('../logger').get('appium')
  , deviceCommon = require('./device')
  , status = require("./uiauto/lib/status")
  //, NotImplementedError = errors.NotImplementedError
  , NotYetImplementedError = errors.NotYetImplementedError
  , parseXpath = require('./uiauto/appium/xpath').parseXpath
  , exec = require('child_process').exec
  , fs = require('fs')
  , os = require('os')
  , temp = require('temp')
  , async = require('async')
  , path = require('path')
  , UnknownError = errors.UnknownError
  , helpers = require('./helpers')
  , isWindows = helpers.isWindows();

var Android = function(opts) {
  this.initialize(opts);
};

Android.prototype.initialize = function(opts) {
  this.compressXml = opts.compressXml;
  this.rest = opts.rest;
  this.webSocket = opts.webSocket;
  this.opts = opts;
  this.apkPath = opts.apkPath;
  this.udid = opts.udid;
  this.appPackage = opts.appPackage;
  this.appActivity = opts.appActivity;
  this.appWaitActivity = opts.appWaitActivity;
  this.avdName = opts.avdName;
  this.appDeviceReadyTimeout = opts.appDeviceReadyTimeout;
  this.verbose = opts.verbose;
  this.queue = [];
  this.progress = 0;
  this.onStop = function() {};
  this.implicitWaitMs = 0;
  this.commandTimeoutMs = 60 * 1000;
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeout = null;
  this.shuttingDown = false;
  this.adb = null;
  this.swipeStepsPerSec = 28;
  this.asyncWaitMs = 0;
  this.remote = null;
  this.webElementIds = [];
  this.curWindowHandle = null;
  this.capabilities = {
    platform: 'LINUX'
    , browserName: 'Android'
    , version: '4.1'
    , webStorageEnabled: false
    , takesScreenshot: true
    , javascriptEnabled: true
    , databaseEnabled: false
  };
};

Android.prototype.inWebView = function() {
  return this.curWindowHandle !== null;
};

// Clear data, close app, then start app.
Android.prototype.fastReset = function(cb) {
  async.series([
    function(cb) { this.adb.runFastReset(cb); }.bind(this),
    function(cb) { this.adb.waitForNotActivity(cb); }.bind(this),
    function(cb) { this.adb.startApp(cb); }.bind(this),
  ], cb);
};

Android.prototype.keyevent = function(keycode, metastate, cb) {
  this.proxy(["pressKeyCode", {keycode: keycode, metastate: metastate}], cb);
};

Android.prototype.start = function(cb, onDie) {
  if (typeof onDie === "function") {
    this.onStop = onDie;
  }
  var didLaunch = false;

  var onLaunch = function(err, launchCb) {
    if (typeof launchCb === "undefined" || launchCb === null) {
      launchCb = cb;
    }
    var relaunchOn = [
      'Could not find a connected Android device'
      , 'Device did not become ready'
    ];
    var checkShouldRelaunch = function(msg) {
      var relaunch = false;
      _.each(relaunchOn, function(relaunchMsg) {
        relaunch = relaunch || msg.indexOf(relaunchMsg) !== -1;
      });
      return relaunch;
    };

    if (err) {
      // This message is from adb.js. Must update when adb.js changes.
      if (err.message === null ||
          typeof err.message === 'undefined' ||
          checkShouldRelaunch(err.message.toString())) {
        logger.error(err);
        logger.error("Above error isn't fatal, maybe relaunching adb will help....");
        this.adb.waitForDevice(function(err) {
          if (err) return launchCb(err);
          didLaunch = true;
          launchCb();
        });
      } else {
        // error is already printed by ADB.prototype.waitForActivity
        this.shutdown();
        this.adb = null;
        this.onStop = null;
        launchCb(err);
      }
    } else {
      logger.info("ADB launched! Ready for commands (will time out in " +
                  (this.commandTimeoutMs / 1000) + "secs)");
      this.resetTimeout();
      didLaunch = true;
      launchCb(null);
    }
  }.bind(this);

  var onExit = function(code) {
    if (!didLaunch) {
      logger.error("ADB quit before it successfully launched");
      cb("ADB quit unexpectedly before successfully launching");
      code = code || 1;
    } else if (typeof this.cbForCurrentCmd === "function") {
      var error = new UnknownError("ADB died while responding to command, " +
                                   "please check appium logs!");
      this.cbForCurrentCmd(error, null);
      code = code || 1;
    }

    if (this.adb) {
      this.adb.uninstallApp(function() {
        this.adb = null;
        this.shuttingDown = false;
        this.onStop(code);
        this.onStop = null;
      }.bind(this));
    } else {
      logger.info("We're in android's exit callback but adb is gone already");
    }
  }.bind(this);

  if (this.adb === null) {
    // Pass Android opts and Android ref to adb.
    this.adb = adb(this.opts, this);
    this.startAppium(onLaunch, onExit);
  } else {
    logger.error("Tried to start ADB when we already have one running!");
  }
};

Android.prototype.startAppium = function(onLaunch, onExit) {
  this.adb.startAppium(onLaunch, onExit);
};

Android.prototype.timeoutWaitingForCommand = function() {
  logger.info("Didn't get a new command in " + (this.commandTimeoutMs / 1000) +
              " secs, shutting down...");
  this.stop();
};

Android.prototype.stop = function(cb) {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  if (this.adb === null) {
    logger.info("Trying to stop adb but it already exited");
    if (cb) {
      cb();
    }
  } else {
    if (cb) {
      this.onStop = cb;
    }
    this.shuttingDown = true;
    this.adb.goToHome(function() {
      this.shutdown();
    }.bind(this));
    this.queue = [];
    this.progress = 0;
  }
};

Android.prototype.shutdown = function() {
  this.adb.sendShutdownCommand(function() {
    logger.info("Sent shutdown command, waiting for ADB to stop...");
  }.bind(this));
};

Android.prototype.resetTimeout = function() {
  if (this.commandTimeout) {
    clearTimeout(this.commandTimeout);
  }
  this.commandTimeout = setTimeout(this.timeoutWaitingForCommand.bind(this),
      this.commandTimeoutMs);
};

Android.prototype.proxy = deviceCommon.proxy;
Android.prototype.respond = deviceCommon.respond;

Android.prototype.push = function(elem) {
  this.resetTimeout();

  this.queue.push(elem);

  var next = function() {
    if (this.queue.length <= 0) {
      return;
    }

    if (this.queue[0] === null) {
      this.queue.shift();
      return;
    }

    // Always send the command.
    if (this.progress > 0) {
      this.progress = 0;
    }

    var target = this.queue.shift()
      , action = target[0][0]
      , params = typeof target[0][1] === "undefined" ? {} : target[0][1]
      , cb = target[1];

    this.cbForCurrentCmd = cb;

    this.progress++;

    if (this.adb && !this.shuttingDown) {
      this.adb.sendAutomatorCommand(action, params, function(response) {
        this.cbForCurrentCmd = null;
        if (typeof cb === 'function') {
          this.respond(response, cb);
        }

        // maybe there's moar work to do
        this.progress--;
        next();
      }.bind(this));
    } else {
      this.cbForCurrentCmd = null;
      var msg = "Tried to send command to non-existent Android device, " +
                 "maybe it shut down?";
      if (this.shuttingDown) {
        msg = "We're in the middle of shutting down the Android device, " +
              "so your request won't be executed. Sorry!";
      }

      this.respond({
        status: status.codes.UnknownError.code
        , value: msg
      }, cb);
      this.progress--;
      next();
    }
  }.bind(this);

  next();
};

Android.prototype.waitForCondition = deviceCommon.waitForCondition;

Android.prototype.executeAtom = function(atom, args, cb, alwaysDefaultFrame) {
  var frames = alwaysDefaultFrame === true ? [] : this.curWebFrames;
  this.returnedFromExecuteAtom = false;
  this.processingRemoteCmd = true;
  this.remote.executeAtom(atom, args, frames, function(err, res) {
    this.processingRemoteCmd = false;
    if (!this.returnedFromExecuteAtom) {
      this.returnedFromExecuteAtom = true;
      res = this.parseExecuteResponse(res);
      cb(err, res);
    }
  }.bind(this));
};

Android.prototype.parseExecuteResponse = deviceCommon.parseExecuteResponse;
Android.prototype.parseElementResponse = deviceCommon.parseElementResponse;
Android.prototype.useAtomsElement = deviceCommon.useAtomsElement;

Android.prototype.setCommandTimeout = function(secs, cb) {
  logger.info("Setting command timeout for android to " + secs + " secs");
  this.origCommandTimeoutMs = this.commandTimeoutMs;
  this.commandTimeoutMs = secs * 1000;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: secs
  });
};

Android.prototype.resetCommandTimeout = function(cb) {
  this.commandTimeoutMs = this.origCommandTimeoutMs;
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: ''
  });
};

Android.prototype.getCommandTimeout = function(cb) {
  this.resetTimeout();
  cb(null, {
    status: status.codes.Success.code
    , value: this.commandTimeoutMs / 1000
  });
};

Android.prototype.findElement = function(strategy, selector, cb) {
  if (this.inWebView()) {
    this.findWebElementOrElements(strategy, selector, false, "", cb);
  } else {
    this.findUIElementOrElements(strategy, selector, false, "", cb);
  }
};

Android.prototype.findElements = function(strategy, selector, cb) {
  if (this.inWebView()) {
    this.findWebElementOrElements(strategy, selector, true, "", cb);
  } else {
    this.findUIElementOrElements(strategy, selector, true, "", cb);
  }
};

Android.prototype.findWebElementOrElements = function(strategy, selector, many, ctx, cb) {
  var ext = many ? 's' : '';
  var atomsElement = this.getAtomsElement(ctx);
  var doFind = function(findCb) {
    this.executeAtom('find_element' + ext, [strategy, selector, atomsElement], function(err, res) {
      this.handleFindCb(err, res, many, findCb);
    }.bind(this));
  }.bind(this);
  this.waitForCondition(this.implicitWaitMs, doFind, cb);
};

Android.prototype.getAtomsElement = deviceCommon.getAtomsElement;

Android.prototype.findUIElementOrElements = function(strategy, selector, many, context, cb) {
  var params = {
    strategy: strategy
    , selector: selector
    , context: context
    , multiple: many
  };
  var xpathError = false;
  if (strategy === "xpath") {
    var xpathParams = parseXpath(selector);
    if (!xpathParams) {
      xpathError = true;
    } else {
      // massage for the javas
      if (xpathParams.attr === null) {
        xpathParams.attr = "";
      }
      if (xpathParams.constraint === null) {
        xpathParams.constraint = "";
      }
      params = _.extend(params, xpathParams);
    }
  }
  var doFind = function(findCb) {
    this.proxy(["find", params], function(err, res) {
      this.handleFindCb(err, res, many, findCb);
    }.bind(this));
  }.bind(this);
  if (!xpathError) {
    this.waitForCondition(this.implicitWaitMs, doFind, cb);
  } else {
    cb(null, {
      status: status.codes.XPathLookupError.code
      , value: "Could not parse xpath data from " + selector
    });
  }
};

Android.prototype.handleFindCb = function(err, res, many, findCb) {
  if (err) {
    findCb(false, err, res);
  } else {
    if (!many && res.status === 0 && res.value !== null) {
      findCb(true, err, res);
    } else if (many && typeof res.value !== 'undefined' && res.value.length > 0) {
      findCb(true, err, res);
    } else {
      findCb(false, err, res);
    }
  }
};

Android.prototype.findElementFromElement = function(element, strategy, selector, cb) {
  if (this.inWebView()) {
    this.findWebElementOrElements(strategy, selector, false, element, cb);
  } else {
    this.findUIElementOrElements(strategy, selector, false, element, cb);
  }
};

Android.prototype.findElementsFromElement = function(element, strategy, selector, cb) {
  if (this.inWebView()) {
    this.findWebElementOrElements(strategy, selector, true, element, cb);
  } else {
    this.findUIElementOrElements(strategy, selector, true, element, cb);
  }
};

Android.prototype.setValueImmediate = function(elementId, value, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.findElementNameContains = function(name, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setValue = function(elementId, value, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: value}], cb);
};

Android.prototype.click = function(elementId, cb) {
  if (this.inWebView()) {
    this.useAtomsElement(elementId, cb, function(atomsElement) {
      this.executeAtom('tap', [atomsElement], cb);
    }.bind(this));
  } else {
    this.proxy(["element:click", {elementId: elementId}], cb);
  }
};

Android.prototype.touchLongClick = function(elementId, cb) {
  this.proxy(["element:touchLongClick", {elementId: elementId}], cb);
};

Android.prototype.fireEvent = function(evt, elementId, value, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getStrings = function(cb) {
  this.proxy(["getStrings"], cb);
};

Android.prototype.complexTap = function(tapCount, touchCount, duration, x, y, elementId, cb) {
  this.proxy(["click", {x: x, y: y}], cb);
};

Android.prototype.clear = function(elementId, cb) {
  this.proxy(["element:clear", {elementId: elementId}], cb);
};

Android.prototype.submit = function(elementId, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getName = function(elementId, cb) {
  this.proxy(["element:getName", {elementId: elementId}], cb);
};

Android.prototype.getText = function(elementId, cb) {
  this.proxy(["element:getText", {elementId: elementId}], cb);
};

Android.prototype.getAttribute = function(elementId, attributeName, cb) {
  var p = {elementId: elementId, attribute: attributeName};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getLocation = function(elementId, cb) {
  this.proxy(["element:getLocation", {elementId: elementId}], cb);
};

Android.prototype.getSize = function(elementId, cb) {
  this.proxy(["element:getSize", {elementId: elementId}], cb);
};

Android.prototype.getWindowSize = function(windowHandle, cb) {
  this.proxy(["getDeviceSize"], cb);
};

Android.prototype.back = function(cb) {
  this.proxy(["pressBack"], cb);
};

Android.prototype.forward = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.refresh = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getPageIndex = function(elementId, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.keys = function(elementId, keys, cb) {
  this.proxy(["element:setText", {elementId: elementId, text: keys}], cb);
};

Android.prototype.frame = function(frame, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.leaveWebView = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.implicitWait = function(ms, cb) {
  this.implicitWaitMs = parseInt(ms, 10);
  logger.info("Set Android implicit wait to " + ms + "ms");
  cb(null, {
    status: status.codes.Success.code
    , value: null
  });
};

Android.prototype.asyncScriptTimeout = function(ms, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.executeAsync = function(script, args, responseUrl, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.elementDisplayed = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "displayed"};
  this.proxy(["element:getAttribute", p], function(err, res) {
    if (err) return cb(err);
    var displayed = res.value === 'true';
    cb(null, {
      status: status.codes.Success.code
      , value: displayed
    });
  });
};

Android.prototype.elementEnabled = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "enabled"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.elementSelected = function(elementId, cb) {
  var p = {elementId: elementId, attribute: "selected"};
  this.proxy(["element:getAttribute", p], cb);
};

Android.prototype.getCssProperty = function(elementId, propertyName, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getPageSource = function(cb) {
  var xmlFile = temp.path({suffix: '.xml'});
  var jsonFile = xmlFile + '.json';
  var json = '';
  async.series(
        [
          function(cb) {
            // /data/local/tmp/dump.xml
            this.proxy(["dumpWindowHierarchy"], cb);
          }.bind(this),
          function(cb) {
            var cmd = this.adb.adbCmd + ' pull /data/local/tmp/dump.xml "' + xmlFile + '"';
            logger.debug('transferPageSource command: ' + cmd);
            exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
              if (err) {
                logger.warn(stderr);
                return cb(err);
              }
              cb(null);
            });
          }.bind(this),
          function(cb) {
            var jar = path.resolve(__dirname, '../app/android/dump2json.jar');
            var cmd = 'java -jar "' + jar + '" "' + xmlFile + '"';
            logger.debug('json command: ' + cmd);
            exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
              if (err) {
                logger.warn(stderr);
                return cb(err);
              }
              cb(null);
            });
          },
          function(cb) {
            json = fs.readFileSync(jsonFile, 'utf8');
            fs.unlinkSync(jsonFile);
            fs.unlinkSync(xmlFile);
            cb(null);
          }
        ],
        // Top level cb
        function(){
          cb(null, {
               status: status.codes.Success.code
               , value: json
             });
        });
};

Android.prototype.getPageSourceXML = function(cb) {
  var xmlFile = temp.path({suffix: '.xml'});
  async.series(
        [
          function(cb) {
            var cmd = this.adb.adbCmd + ' shell uiautomator dump /data/local/tmp/dump.xml';
            logger.debug('getPageSourceXML command: ' + cmd);
            exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
              if (err) {
                logger.warn(stderr);
                return cb(err);
              }
              cb(null);
            });
          }.bind(this),
          function(cb) {
            var cmd = this.adb.adbCmd + ' pull /data/local/tmp/dump.xml "' + xmlFile + '"';
            logger.debug('transferPageSourceXML command: ' + cmd);
            exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
              if (err) {
                logger.warn(stderr);
                return cb(err);
              }
              cb(null);
            });
          }.bind(this)

        ],
        // Top level cb
        function(){
          var xml = fs.readFileSync(xmlFile, 'utf8');
          fs.unlinkSync(xmlFile);
          cb(null, {
               status: status.codes.Success.code
               , value: xml
             });
        });
};

Android.prototype.waitForPageLoad = function(timeout, cb) {
  this.proxy(["waitForIdle", {timeout: timeout}], cb);
};

Android.prototype.getAlertText = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.setAlertText = function(text, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postAcceptAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.postDismissAlert = function(cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.lock = function(secs, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.background = function(secs, cb) {
    cb(new NotYetImplementedError(), null);
};

Android.prototype.getOrientation = function(cb) {
  this.proxy(["orientation", {}], cb);
};

Android.prototype.setOrientation = function(orientation, cb) {
  this.proxy(["orientation", {orientation: orientation}], cb);
};

Android.prototype.localScreenshot = function(file, cb) {
  this.adb.requireDeviceId();
  async.series([
    function(cb) {
      this.proxy(["takeScreenshot"], cb);
    }.bind(this),
    function(cb) {
      var cmd = this.adb.adbCmd + ' pull /data/local/tmp/screenshot.png "' + file + '"';
      exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
        if (err) {
          logger.warn(stderr);
          return cb(err);
        }
        cb(null);
      });
    }.bind(this),
  ],
  function(){
    cb(null, {
      status: status.codes.Success.code
    });
  });
};

Android.prototype.getScreenshot = function(cb) {
  this.adb.requireDeviceId();
  var localfile = temp.path({prefix: 'appium', suffix: '.png'});
  var b64data = "";

  async.series([
    function(cb) {
      this.proxy(["takeScreenshot"], cb);
    }.bind(this),
    function(cb) {
      var cmd = this.adb.adbCmd + ' pull /data/local/tmp/screenshot.png "' + localfile + '"';
      logger.debug("screenshot cmd: " + cmd);
      exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
        if (err) {
          logger.warn(stderr);
          return cb(err);
        }
        cb(null);
      });
    }.bind(this),
    function(cb) {
      fs.readFile(localfile, function read(err, data) {
        if (err) {
          cb(err);
        } else {
          b64data = new Buffer(data).toString('base64');
          cb(null);
        }
      });
    },
    function(cb) {
      fs.unlink(localfile, function(err) {
        if (err) {
          cb(err);
        } else {
          cb(null);
        }
      });
    }
  ],
  // Top level cb
  function(err, res) {
    var screenshotStatus = status.codes.Success.code;
    try {
      screenshotStatus = res[0].value === false ? status.codes.UnknownError.code : screenshotStatus;
    } catch(e) {
    }
    cb(null, {
      status: screenshotStatus
      , value: b64data
    });
  });
};

Android.prototype.fakeFlick = function(xSpeed, ySpeed, swipe, cb) {
  this.proxy(["flick", {xSpeed: xSpeed, ySpeed: ySpeed}], cb);
};

Android.prototype.fakeFlickElement = function(elementId, xoffset, yoffset, speed, cb) {
  this.proxy(["element:flick", {xoffset: xoffset, yoffset: yoffset, speed: speed, elementId: elementId}], cb);
};

Android.prototype.swipe = function(startX, startY, endX, endY, duration, touchCount, elId, cb) {
  if (startX === 'null') {
    startX = 0.5;
  }
  if (startY === 'null') {
    startY = 0.5;
  }
  var swipeOpts = {
    startX: startX
    , startY: startY
    , endX: endX
    , endY: endY
    , steps: Math.round(duration * this.swipeStepsPerSec)
  };
  if (elId !== null) {
    swipeOpts.elementId = elId;
    this.proxy(["element:swipe", swipeOpts], cb);
  } else {
    this.proxy(["swipe", swipeOpts], cb);
  }
};

Android.prototype.rotate = function(x, y, radius, rotation, duration, touchCount, elId, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.pinchClose = function(startX, startY, endX, endY, duration, elId, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.pinchOpen = function(startX, startY, endX, endY, duration, elId, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.flick = function(startX, startY, endX, endY, touchCount, elId, cb) {
  if (startX === 'null') {
    startX = 0.5;
  }
  if (startY === 'null') {
    startY = 0.5;
  }
  var swipeOpts = {
    startX: startX
    , startY: startY
    , endX: endX
    , endY: endY
    , steps: Math.round(0.2 * this.swipeStepsPerSec)
  };
  if (elId !== null) {
    swipeOpts.elementId = elId;
    this.proxy(["element:swipe", swipeOpts], cb);
  } else {
    this.proxy(["swipe", swipeOpts], cb);
  }
};

Android.prototype.drag = function(startX, startY, endX, endY, steps, elementId, destElId, cb) {
  var dragOpts = {
    elementId: elementId
    , destElId: destElId
    , startX: startX
    , startY: startY
    , endX: endX
    , endY: endY
    , steps: steps
  };

  if (elementId !== null) {
    this.proxy(["element:drag", dragOpts], cb);
  } else {
    this.proxy(["drag", dragOpts], cb);
  }
};

Android.prototype.scrollTo = function(elementId, text, cb) {
  // instead of the elementId as the element to be scrolled too,
  // it's the scrollable view to swipe until the uiobject that has the
  // text is found.
  var opts = {
    text: text
    , elementId: elementId
  };
  this.proxy(["element:scrollTo", opts], cb);
};

Android.prototype.shake = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setLocation = function(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, course, speed, cb) {
  var cmd = "geo fix " + longitude + " " + latitude;
  this.adb.sendTelnetCommand(cmd, function(err, res) {
    if (err) {
      return cb(null, {
        status: status.codes.UnknownError.code
        , value: "Could not set geolocation via telnet to device"
      });
    }
    cb(null, {
      status: status.codes.Success.code
      , value: res
    });
  });
};

Android.prototype.hideKeyboard = function(keyName, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.url = function(url, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.active = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandle = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getWindowHandles = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setWindow = function(name, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.closeWindow = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.clearWebView = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.execute = function(script, args, cb) {
  if (this.inWebView()) {
    this.convertElementForAtoms(args, function(err, res) {
      if (err) return cb(null, res);
      this.executeAtom('execute_script', [script, res], cb);
    }.bind(this));
  } else {
    cb(new NotYetImplementedError(), null);
  }
};

Android.prototype.convertElementForAtoms = deviceCommon.convertElementForAtoms;

Android.prototype.title = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.moveTo = function(element, xoffset, yoffset, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.clickCurrent = function(button, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getCookies = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.setCookie = function(cookie, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.deleteCookie = function(cookie, cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.deleteCookies = function(cb) {
  cb(new NotYetImplementedError(), null);
};

Android.prototype.getCurrentActivity = function(cb) {
  this.adb.getFocusedPackageAndActivity(function(err, curPackage, activity) {
    if (err) {
      return cb(null, {
        status: status.codes.UnknownError.code
        , value: err.message
      });
    }
    cb(null, {
      status: status.codes.Success.code
      , value: activity
    });
  });
};

Android.prototype.isAppInstalled = function(appPackage, cb) {
  var isInstalledCommand = null;
  if (this.udid) {
    isInstalledCommand = 'adb -s ' + this.udid + ' shell pm path ' + appPackage;
  } else {
    isInstalledCommand = 'adb shell pm path ' + appPackage;
  }
  deviceCommon.isAppInstalled(isInstalledCommand, cb);
};

Android.prototype.removeApp = function(appPackage, cb) {
  var removeCommand = null;
  if (this.udid) {
    removeCommand = 'adb -s ' + this.udid + ' uninstall ' + appPackage;
  } else {
    removeCommand = 'adb uninstall ' + appPackage;
  }
  deviceCommon.removeApp(removeCommand, this.udid, appPackage, cb);
};

Android.prototype.installApp = function(appPackage, cb) {
  var installationCommand = null;
  if (this.udid) {
    installationCommand = 'adb -s ' + this.udid + ' install ' + appPackage;
  } else {
    installationCommand = 'adb install ' + appPackage;
  }
  deviceCommon.installApp(installationCommand, this.udid, appPackage, cb);
};

Android.prototype.unpackApp = function(req, cb) {
  deviceCommon.unpackApp(req, '.apk', cb);
};

Android.prototype.getLog = deviceCommon.getLog;
Android.prototype.getLogTypes = deviceCommon.getLogTypes;

module.exports = function(opts) {
  return new Android(opts);
};

module.exports.Android = Android;
