Files
appium/lib/devices/android/android-controller.js
bootstraponline 74a233b6e8 Fix compressXml
The old compressXml did not presist across uiautomator sessions.
By requesting 'DumpWindowHierarchyCompressed' it ensures the result is
always compressed, even if the server had been restarted.

For some reason if compressXml is disabled then the xpath index on Android
will return the wrong result.
2014-05-07 15:23:18 -04:00

1098 lines
31 KiB
JavaScript

"use strict";
var errors = require('../../server/errors.js')
, _ = require('underscore')
, logger = require('../../server/logger.js').get('appium')
, deviceCommon = require('../common.js')
, helpers = require('../../helpers.js')
, jwpSuccess = deviceCommon.jwpSuccess
, status = require('../../server/status.js')
, NotYetImplementedError = errors.NotYetImplementedError
, exec = require('child_process').exec
, fs = require('fs')
, temp = require('temp')
, async = require('async')
, mkdirp = require('mkdirp')
, path = require('path')
, xpath = require("xpath")
, XMLDom = require("xmldom")
, helpers = require('../../helpers.js')
, warnDeprecated = helpers.logDeprecationWarning;
var androidController = {};
var NATIVE_WIN = "NATIVE_APP";
var WEBVIEW_WIN = "WEBVIEW";
var WEBVIEW_BASE = WEBVIEW_WIN + "_";
androidController.keyevent = function (keycode, metastate, cb) {
this.proxy(["pressKeyCode", {keycode: keycode, metastate: metastate}], cb);
};
androidController.defaultContext = function () {
return NATIVE_WIN;
};
androidController.findElement = function (strategy, selector, cb) {
this.findUIElementOrElements(strategy, selector, false, "", cb);
};
androidController.findElements = function (strategy, selector, cb) {
this.findUIElementOrElements(strategy, selector, true, "", cb);
};
androidController.findUIElementOrElements = function (strategy, selector, many, context, cb) {
if (!deviceCommon.checkValidLocStrat(strategy, false, cb)) {
return;
}
if (strategy === "xpath" && context) {
return cb(new Error("Cannot use xpath locator strategy from an element. " +
"It can only be used from the root element"));
}
var params = {
strategy: strategy
, selector: selector
, context: context
, multiple: many
};
if (strategy === "name") {
helpers.logDeprecationWarning("Locator strategy", '"name"', '"accessibility id"');
}
var doFind = function (findCb) {
if (strategy === "xpath") {
this.findUIElementsByXPath(selector, many, function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
} else {
this.proxy(["find", params], function (err, res) {
this.handleFindCb(err, res, many, findCb);
}.bind(this));
}
}.bind(this);
this.waitForCondition(this.implicitWaitMs, doFind, cb);
};
androidController.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);
}
}
};
androidController.findElementFromElement = function (element, strategy, selector, cb) {
this.findUIElementOrElements(strategy, selector, false, element, cb);
};
androidController.findElementsFromElement = function (element, strategy, selector, cb) {
this.findUIElementOrElements(strategy, selector, true, element, cb);
};
var _pathFromDomNode = function (node) {
var path = "";
_.each(node.attributes, function (attrObj) {
if (attrObj.name === "index") {
path = _pathFromDomNode(node.parentNode) + "/" + attrObj.value;
}
});
return path;
};
androidController.findUIElementsByXPath = function (selector, many, cb) {
this.getPageSource(function (err, res) {
if (err || res.status !== status.codes.Success.code) return cb(err, res);
var dom, nodes;
var xmlSource = res.value;
try {
dom = new XMLDom.DOMParser().parseFromString(xmlSource);
nodes = xpath.select(selector, dom);
} catch (e) {
logger.error(e);
return cb(e);
}
if (!many) nodes = nodes.slice(0, 1);
var indexPaths = _.map(nodes, _pathFromDomNode);
if (!many && indexPaths.length < 1) {
// if we don't have any matching nodes, and we wanted at least one, fail
return cb(null, {
status: status.codes.NoSuchElement.code,
value: null
});
} else if (indexPaths.length < 1) {
// and if we don't have any matching nodes, return the empty array
return cb(null, {
status: status.codes.Success.code,
value: []
});
}
var findParams = {
strategy: "index paths",
selector: indexPaths.join(","),
multiple: many
};
this.proxy(["find", findParams], cb);
}.bind(this));
};
androidController.setValueImmediate = function (elementId, value, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.setValue = function (elementId, value, cb) {
this.proxy(["element:setText", {elementId: elementId, text: value}], cb);
};
androidController.click = function (elementId, cb) {
this.proxy(["element:click", {elementId: elementId}], cb);
};
androidController.touchLongClick = function (elementId, x, y, duration, cb) {
var opts = {};
if (elementId) opts.elementId = elementId;
if (x) opts.x = x;
if (y) opts.y = y;
if (duration) opts.duration = duration;
this.proxy(["element:touchLongClick", opts], cb);
};
androidController.touchDown = function (elementId, x, y, cb) {
var opts = {};
if (elementId) opts.elementId = elementId;
if (x) opts.x = x;
if (y) opts.y = y;
this.proxy(["element:touchDown", opts], cb);
};
androidController.touchUp = function (elementId, x, y, cb) {
var opts = {};
if (elementId) opts.elementId = elementId;
if (x) opts.x = x;
if (y) opts.y = y;
this.proxy(["element:touchUp", opts], cb);
};
androidController.touchMove = function (elementId, x, y, cb) {
var opts = {};
if (elementId) opts.elementId = elementId;
if (x) opts.x = x;
if (y) opts.y = y;
this.proxy(["element:touchMove", opts], cb);
};
androidController.getStrings = function (cb) {
this.proxy(["getStrings"], cb);
};
androidController.complexTap = function (tapCount, touchCount, duration, x, y, elementId, cb) {
this.proxy(["click", {x: x, y: y}], cb);
};
androidController.clear = function (elementId, cb) {
this.proxy(["element:clear", {elementId: elementId}], cb);
};
androidController.submit = function (elementId, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getName = function (elementId, cb) {
var p = {elementId: elementId, attribute: "className"};
this.proxy(["element:getAttribute", p], cb);
};
androidController.getText = function (elementId, cb) {
this.proxy(["element:getText", {elementId: elementId}], cb);
};
androidController.getAttribute = function (elementId, attributeName, cb) {
var p = {elementId: elementId, attribute: attributeName};
this.proxy(["element:getAttribute", p], cb);
};
androidController.getLocation = function (elementId, cb) {
this.proxy(["element:getLocation", {elementId: elementId}], cb);
};
androidController.getSize = function (elementId, cb) {
this.proxy(["element:getSize", {elementId: elementId}], cb);
};
androidController.getWindowSize = function (windowHandle, cb) {
this.proxy(["getDeviceSize"], cb);
};
androidController.back = function (cb) {
this.proxy(["pressBack"], cb);
};
androidController.forward = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.refresh = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getPageIndex = function (elementId, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.keys = function (elementId, keys, cb) {
this.proxy(["element:setText", {elementId: elementId, text: keys}], cb);
};
androidController.frame = function (frame, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.leaveWebView = function (cb) {
warnDeprecated('function', 'leaveWebView', 'context(null)');
this.setWindow(this.defaultContext(), cb);
};
androidController.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
});
};
androidController.asyncScriptTimeout = function (ms, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.executeAsync = function (script, args, responseUrl, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.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
});
});
};
androidController.elementEnabled = function (elementId, cb) {
var p = {elementId: elementId, attribute: "enabled"};
this.proxy(["element:getAttribute", p], function (err, res) {
if (err) return cb(err);
var enabled = res.value === 'true';
cb(null, {
status: status.codes.Success.code
, value: enabled
});
});
};
androidController.elementSelected = function (elementId, cb) {
var p = {elementId: elementId, attribute: "selected"};
this.proxy(["element:getAttribute", p], function (err, res) {
if (err) return cb(err);
var selected = res.value === 'true';
cb(null, {
status: status.codes.Success.code
, value: selected
});
});
};
androidController.getCssProperty = function (elementId, propertyName, cb) {
cb(new NotYetImplementedError(), null);
};
var _updateSourceXMLNodeNames = function (source) {
var newSource;
var origDom = new XMLDom.DOMParser().parseFromString(source);
var newDom = new XMLDom.DOMImplementation().createDocument(null);
_buildClassNodeFromPlainNode(newDom, newDom, origDom);
newSource = new XMLDom.XMLSerializer().serializeToString(newDom);
return newSource;
};
var _getNodeClass = function (node) {
var nodeClass = null;
_.each(node.attributes, function (attr) {
if (attr.name === "class") {
nodeClass = attr.value;
}
});
return nodeClass;
};
var _copyNodeAttributes = function (oldNode, newNode) {
_.each(oldNode.attributes, function (attr) {
newNode.setAttribute(attr.name, attr.value);
});
};
var _buildClassNodeFromPlainNode = function (newDom, newParent, oldNode) {
var newNode;
var nodeClass = _getNodeClass(oldNode);
if (nodeClass) {
newNode = newDom.createElement(nodeClass);
_copyNodeAttributes(oldNode, newNode);
} else {
newNode = oldNode.cloneNode(false);
}
newParent.appendChild(newNode);
if (oldNode.hasChildNodes()) {
_.each(oldNode.childNodes, function (childNode) {
_buildClassNodeFromPlainNode(newDom, newNode, childNode);
});
}
};
androidController.getPageSource = function (cb) {
var xmlFile = temp.path({suffix: '.xml'});
var onDeviceXmlPath = this.dataDir + '/local/tmp/dump.xml';
async.series(
[
function (cb) {
var dumpCommand = this.args.compressXml ? "dumpWindowHierarchyCompressed" : "dumpWindowHierarchy";
this.proxy([dumpCommand], cb);
}.bind(this),
function (cb) {
var cmd = this.adb.adbCmd + ' pull ' + onDeviceXmlPath + ' "' + 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 (err) {
if (err) return cb(err);
var xml = fs.readFileSync(xmlFile, 'utf8');
fs.unlinkSync(xmlFile);
try {
xml = _updateSourceXMLNodeNames(xml);
} catch (e) {
logger.error(e);
return cb(e);
}
cb(null, {
status: status.codes.Success.code
, value: xml
});
});
};
androidController.getAlertText = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.setAlertText = function (text, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.postAcceptAlert = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.postDismissAlert = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.lock = function (secs, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getOrientation = function (cb) {
this.proxy(["orientation", {}], cb);
};
androidController.setOrientation = function (orientation, cb) {
this.proxy(["orientation", {orientation: orientation}], cb);
};
androidController.endCoverage = function (intentToBroadcast, ecOnDevicePath, cb) {
var localfile = temp.path({prefix: 'appium', suffix: '.ec'});
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
var b64data = "";
async.series([
function (cb) {
// ensure the ec we're pulling is newly created as a result of the intent.
this.adb.rimraf(ecOnDevicePath, function () { cb(); });
}.bind(this),
function (cb) {
this.adb.broadcastProcessEnd(intentToBroadcast, this.appProcess, cb);
}.bind(this),
function (cb) {
this.adb.pull(ecOnDevicePath, localfile, cb);
}.bind(this),
function (cb) {
fs.readFile(localfile, function (err, data) {
if (err) return cb(err);
b64data = new Buffer(data).toString('base64');
cb();
});
}.bind(this),
],
function (err) {
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
if (err) return cb(err);
cb(null, {
status: status.codes.Success.code
, value: b64data
});
});
};
androidController.pullFile = function (remotePath, cb) {
var localFile = temp.path({prefix: 'appium', suffix: '.tmp'});
var b64data = "";
async.series([
function (cb) {
this.adb.pull(remotePath, localFile, cb);
}.bind(this),
function (cb) {
fs.readFile(localFile, function (err, data) {
if (err) return cb(err);
b64data = new Buffer(data).toString('base64');
cb();
});
}.bind(this),
],
function (err) {
if (fs.existsSync(localFile)) fs.unlinkSync(localFile);
if (err) return cb(err);
cb(null, {
status: status.codes.Success.code
, value: b64data
});
});
};
androidController.pushFile = function (base64Data, remotePath, cb) {
var localFile = temp.path({prefix: 'appium', suffix: '.tmp'});
mkdirp.sync(path.dirname(localFile));
async.series([
function (cb) {
var content = new Buffer(base64Data, 'base64');
var fd = fs.openSync(localFile, 'w');
fs.writeSync(fd, content, 0, content.length, 0);
fs.closeSync(fd);
// adb push creates folders and overwrites existing files.
this.adb.push(localFile, remotePath, cb);
}.bind(this),
],
function (err) {
if (fs.existsSync(localFile)) fs.unlinkSync(localFile);
if (err) return cb(err);
cb(null, {
status: status.codes.Success.code
});
});
};
androidController.getScreenshot = function (cb) {
var localfile = temp.path({prefix: 'appium', suffix: '.png'});
var b64data = "";
async.series([
function (cb) {
var png = "/data/local/tmp/screenshot.png";
var cmd = ['"/system/bin/rm', png + ';', '/system/bin/screencap -p',
png, '"'].join(' ');
this.adb.shell(cmd, cb);
}.bind(this),
function (cb) {
if (fs.existsSync(localfile)) fs.unlinkSync(localfile);
this.adb.pull('/data/local/tmp/screenshot.png', localfile, cb);
}.bind(this),
function (cb) {
fs.readFile(localfile, function (err, data) {
if (err) return cb(err);
b64data = new Buffer(data).toString('base64');
cb();
});
},
function (cb) {
fs.unlink(localfile, function (err) {
if (err) return cb(err);
cb();
});
}
],
// Top level cb
function (err) {
if (err) return cb(err);
cb(null, {
status: status.codes.Success.code
, value: b64data
});
});
};
androidController.fakeFlick = function (xSpeed, ySpeed, swipe, cb) {
this.proxy(["flick", {xSpeed: xSpeed, ySpeed: ySpeed}], cb);
};
androidController.fakeFlickElement = function (elementId, xoffset, yoffset, speed, cb) {
this.proxy(["element:flick", {xoffset: xoffset, yoffset: yoffset, speed: speed, elementId: elementId}], cb);
};
androidController.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);
}
};
androidController.rotate = function (x, y, radius, rotation, duration, touchCount, elId, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.pinchClose = function (startX, startY, endX, endY, duration, percent, steps, elId, cb) {
var pinchOpts = {
direction: 'in'
, elementId: elId
, percent: percent
, steps: steps
};
this.proxy(["element:pinch", pinchOpts], cb);
};
androidController.pinchOpen = function (startX, startY, endX, endY, duration, percent, steps, elId, cb) {
var pinchOpts = {
direction: 'out'
, elementId: elId
, percent: percent
, steps: steps
};
this.proxy(["element:pinch", pinchOpts], cb);
};
androidController.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);
}
};
androidController.drag = function (startX, startY, endX, endY, duration, touchCount, elementId, destElId, cb) {
var dragOpts = {
elementId: elementId
, destElId: destElId
, startX: startX
, startY: startY
, endX: endX
, endY: endY
, steps: Math.round(duration * this.dragStepsPerSec)
};
if (elementId !== null) {
this.proxy(["element:drag", dragOpts], cb);
} else {
this.proxy(["drag", dragOpts], cb);
}
};
androidController.scrollTo = function (elementId, text, direction, 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
, direction: direction
, elementId: elementId
};
this.proxy(["element:scrollTo", opts], cb);
};
androidController.scroll = function (direction, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.shake = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.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
});
});
};
androidController.hideKeyboard = function (keyName, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.url = function (url, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.active = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getCurrentContext = function (cb) {
var response = {
status: status.codes.Success.code
, value: this.curContext || null
};
cb(null, response);
};
androidController.getContexts = function (cb) {
this.listWebviews(function (err, webviews) {
if (err) return cb(err);
this.contexts = [NATIVE_WIN];
_.each(webviews, function (view, idx) {
this.contexts.push(WEBVIEW_BASE + (idx + 1).toString());
}.bind(this));
logger.info("Available contexts: " + this.contexts);
cb(null, {
status: status.codes.Success.code
, value: this.contexts
});
}.bind(this));
};
androidController.setContext = function (name, cb) {
if (name === null) {
name = this.defaultContext();
} else if (name === WEBVIEW_WIN) {
name = WEBVIEW_BASE + "1";
}
this.getContexts(function () {
if (!_.contains(this.contexts, name)) {
return cb(null, {
status: status.codes.NoSuchContext.code
, value: "Context '" + name + "' does not exist"
});
}
if (name === this.curContext) {
return jwpSuccess(cb);
}
var next = function (err) {
if (err) return cb(err);
this.curContext = name;
jwpSuccess(cb);
}.bind(this);
// current ChromeDriver doesn't handle more than a single web view
if (name.indexOf(WEBVIEW_WIN) !== -1) {
this.startChromedriverProxy(next);
} else {
this.stopChromedriverProxy(next);
}
}.bind(this));
};
androidController.getWindowHandle = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getWindowHandles = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.setWindow = function (name, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.closeWindow = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.clearWebView = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.execute = function (script, args, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.convertElementForAtoms = deviceCommon.convertElementForAtoms;
androidController.title = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.moveTo = function (element, xoffset, yoffset, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.clickCurrent = function (button, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.getCookies = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.setCookie = function (cookie, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.deleteCookie = function (cookie, cb) {
cb(new NotYetImplementedError(), null);
};
androidController.deleteCookies = function (cb) {
cb(new NotYetImplementedError(), null);
};
androidController.resetAndStartApp = function (cb) {
async.series([
this.resetApp.bind(this),
this.waitForActivityToStop.bind(this),
this.startApp.bind(this)
], cb);
};
// controller.js#isAppInstalled expects weird response, hence this hack
androidController.isAppInstalled = function (appPackage, cb) {
this.adb.isAppInstalled(appPackage, function (err, installed) {
if (installed) {
return cb(null, [true]);
}
cb(err, []);
});
};
androidController.removeApp = function (appPackage, cb) {
var removeCommand = null;
if (this.args.udid) {
removeCommand = 'adb -s ' + this.args.udid + ' uninstall ' + appPackage;
} else {
removeCommand = 'adb uninstall ' + appPackage;
}
deviceCommon.removeApp(removeCommand, this.args.udid, appPackage, cb);
};
androidController.installApp = function (appPackage, cb) {
var installationCommand = null;
if (this.args.udid) {
installationCommand = 'adb -s ' + this.args.udid + ' install ' + appPackage;
} else {
installationCommand = 'adb install ' + appPackage;
}
deviceCommon.installApp(installationCommand, this.args.udid, appPackage, cb);
};
androidController.unpackApp = function (req, cb) {
deviceCommon.unpackApp(req, '.apk', cb);
};
androidController.tap = function (elementId, x, y, count, cb) {
if (typeof x === "undefined" || x === null) x = 0;
if (typeof y === "undefined" || y === null) y = 0;
if (typeof count === "undefined" || count === null) count = 1;
var i = 0;
var opts = {};
var loop = function (err, res) {
if (err) return cb(err);
if (i++ >= count) return cb(err, res);
this.proxy(opts, loop);
}.bind(this);
if (elementId) {
// we are either tapping on the default location of the element
// or an offset from the top left corner
if (x !== 0 || y !== 0) {
this.proxy(["element:getLocation", {elementId: elementId}], function (err, res) {
if (err) return cb(err);
var value = res.value;
x += parseInt(value.x);
y += parseInt(value.y);
opts = ["click", {x: x, y: y}];
loop();
}.bind(this));
} else {
opts = ["element:click", {elementId: elementId}];
loop();
}
} else {
// we have absolute coordinates
opts = ["click", {x: x, y: y}];
loop();
}
};
androidController.doTouchAction = function (action, opts, cb) {
switch (action) {
case 'tap':
return this.tap(opts.element, opts.x, opts.y, opts.count, cb);
case 'press':
return this.touchDown(opts.element, opts.x, opts.y, cb);
case 'release':
return this.touchUp(opts.element, opts.x, opts.y, cb);
case 'moveTo':
return this.touchMove(opts.element, opts.x, opts.y, cb);
case 'wait':
return setTimeout(function () {
cb(null, {"value": true, "status": status.codes.Success.code});
}, opts.ms);
case 'longPress':
if (typeof opts.duration === 'undefined' || !opts.duration) {
opts.duration = 1000;
}
return this.touchLongClick(opts.element, opts.x, opts.y, opts.duration, cb);
case 'cancel':
// TODO: clarify behavior of 'cancel' action and fix this
logger.warn("Cancel action currently has no effect");
break;
default:
return cb("unknown action '" + action + "'");
}
};
// drag is *not* press-move-release, so we need to translate
// drag works fine for scroll, as well
var doTouchDrag = function (gestures, cb) {
var longPress = gestures[0];
var elementId = longPress.options.element;
this.getLocation(elementId, function (err, res) {
if (err) return cb(err);
var startX = res.value.x;
var startY = res.value.y;
var moveTo = gestures[1];
var destElId = moveTo.options.element;
var endX = moveTo.options.x;
var endY = moveTo.options.y;
if (typeof destElId !== 'undefined' && destElId) {
this.getLocation(destElId, function (err, res) {
if (err) return cb(err);
endX = res.value.x + (endX || 0);
endY = res.value.y + (endY || 0);
return this.drag(startX, startY, endX, endY, 1, 1, elementId, destElId, cb);
}.bind(this));
} else {
return this.drag(startX, startY, endX, endY, 1, 1, elementId, destElId, cb);
}
}.bind(this));
return;
};
androidController.performTouch = function (gestures, cb) {
// some things are special
var actions = _.pluck(gestures, "action");
if (actions[0] === 'longPress' && actions[1] === 'moveTo' && actions[2] === 'release') {
return doTouchDrag.apply(this, [gestures, cb]);
} else if ((actions[actions.length - 2] === 'tap' ||
actions[actions.length - 2] === 'longPress') && actions[actions.length - 1] === 'release') {
// the `longPress` and `tap` methods release on their own
gestures.pop();
}
var cycleThroughGestures = function (err, res) {
if (err) return cb(err);
var gesture = gestures.shift();
if (typeof gesture === "undefined") return cb(null, res);
this.doTouchAction(gesture.action, gesture.options, cycleThroughGestures);
}.bind(this);
cycleThroughGestures();
};
androidController.parseTouch = function (gestures, cb) {
if (_.last(gestures).action === 'release') {
gestures.pop();
}
var needsPoint = function (action) {
return _.contains(['press', 'moveTo', 'tap', 'longPress'], action);
};
var touchStateObjects = [];
async.eachSeries(gestures, function (gesture, done) {
var tapPoint = false;
if (needsPoint(gesture.action)) { // press, longPress, moveTo and tap all need a position
var elementId = gesture.options.element;
if (elementId) {
this.getLocation(elementId, function (err, res) {
if (err) return done(err); // short circuit and quit
var pos = { x: res.value.x, y: res.value.y };
this.getSize(elementId, function (err, res) {
if (err) return done(err);
var size = {w: res.value.width, h: res.value.height};
if (gesture.options.x || gesture.options.y) {
tapPoint = {
offset: false,
x: pos.x + (gesture.options.x || 0),
y: pos.y + (gesture.options.y || 0)
};
} else {
tapPoint = {
offset: false,
x: pos.x + (size.w / 2),
y: pos.y + (size.h / 2)
};
}
var touchStateObject = {
timeOffset: 0.005,
touch: tapPoint
};
touchStateObjects.push(touchStateObject);
done();
});
}.bind(this));
} else {
// expects absolute coordinates, so we need to save these as offsets
// and then translate when everything is done
tapPoint = {
offset: true,
x: (gesture.options.x || 0),
y: (gesture.options.y || 0)
};
touchStateObject = {
timeOffset: 0.005,
touch: tapPoint
};
touchStateObjects.push(touchStateObject);
done();
}
} else {
// in this case we need the previous entry's tap point
tapPoint = false; // temporary marker
var offset = 0.005;
if (gesture.action === 'wait') {
if (typeof gesture.options.ms !== 'undefined' || gesture.options.ms !== null) {
offset = (parseInt(gesture.options.ms) / 1000);
}
}
var touchStateObject = {
timeOffset: offset,
touch: tapPoint
};
touchStateObjects.push(touchStateObject);
done();
}
}.bind(this), function (err) {
if (err) return cb(err);
// we need to change the time (which is now an offset)
// and the position (which may be an offset)
var prevPos = null,
time = 0;
_.each(touchStateObjects, function (state) {
if (state.touch === false) {
// if we have no position (this happens with `wait`) we need the previous one
state.touch = prevPos;
} else if (state.touch.offset && prevPos) {
// the current position is an offset
state.touch.x += prevPos.x;
state.touch.y += prevPos.y;
}
delete state.touch.offset;
prevPos = state.touch;
var timeOffset = state.timeOffset;
time += timeOffset;
state.time = helpers.truncateDecimals(time, 3);
delete state.timeOffset;
});
cb(null, touchStateObjects);
});
};
androidController.performMultiAction = function (elementId, actions, cb) {
var states = [];
async.eachSeries(actions, function (action, done) {
this.parseTouch(action, function (err, val) {
if (err) return done(err);
states.push(val);
done();
}.bind(this));
}.bind(this), function (err) {
if (err) return cb(err);
var opts;
if (elementId) {
opts = {
elementId: elementId,
actions: states
};
return this.proxy(["element:performMultiPointerGesture", opts], cb);
} else {
opts = {
actions: states
};
return this.proxy(["performMultiPointerGesture", opts], cb);
}
}.bind(this));
};
module.exports = androidController;