mirror of
https://github.com/appium/appium.git
synced 2026-02-10 20:09:49 -06:00
460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
var ADB = require('./adb.js')
|
|
, Device = require('../device.js')
|
|
, mkdirp = require('mkdirp')
|
|
, _ = require('underscore')
|
|
, deviceCommon = require('../common.js')
|
|
, androidController = require('./android-controller.js')
|
|
, androidContextController = require('./android-context-controller.js')
|
|
, proxyTo = deviceCommon.proxyTo
|
|
, doRequest = deviceCommon.doRequest
|
|
, logger = require('../../server/logger.js').get('appium')
|
|
, status = require("../../server/status.js")
|
|
, fs = require('fs')
|
|
, async = require('async')
|
|
, androidCommon = require('./android-common.js')
|
|
, androidHybrid = require('./android-hybrid.js')
|
|
, path = require('path');
|
|
|
|
var Selendroid = function () {
|
|
this.init();
|
|
};
|
|
|
|
_.extend(Selendroid.prototype, Device.prototype);
|
|
Selendroid.prototype._deviceInit = Device.prototype.init;
|
|
Selendroid.prototype.init = function () {
|
|
this._deviceInit();
|
|
this.selendroidHost = 'localhost';
|
|
this.selendroidPort = 8080;
|
|
this.selendroidSessionId = null;
|
|
this.appExt = ".apk";
|
|
this.args.devicePort = this.selendroidPort;
|
|
this.serverApk = null;
|
|
this.onStop = function () {};
|
|
this.adb = null;
|
|
this.isProxy = true;
|
|
this.mobileMethodsSupported = [
|
|
'setLocation'
|
|
, 'setCommandTimeout'
|
|
, 'reset'
|
|
, 'lock'
|
|
, 'background'
|
|
, 'keyevent'
|
|
, 'currentActivity'
|
|
, 'installApp'
|
|
, 'uninstallApp'
|
|
, 'removeApp'
|
|
, 'closeApp'
|
|
, 'isAppInstalled'
|
|
, 'launchApp'
|
|
, 'toggleData'
|
|
, 'toggleFlightMode'
|
|
, 'toggleWiFi'
|
|
, 'toggleLocationServices'
|
|
, 'getStrings'
|
|
];
|
|
this.proxyHost = this.selendroidHost;
|
|
this.avoidProxy = [
|
|
['GET', new RegExp('^/wd/hub/session/[^/]+/log/types$')]
|
|
, ['POST', new RegExp('^/wd/hub/session/[^/]+/log')]
|
|
, ['POST', new RegExp('^/wd/hub/session/[^/]+/location')]
|
|
, ['POST', new RegExp('^/wd/hub/session/[^/]+/appium')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/appium')]
|
|
, ['POST', new RegExp('^/wd/hub/session/[^/]+/context')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/context')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/contexts')]
|
|
];
|
|
};
|
|
|
|
_.extend(Selendroid.prototype, androidCommon);
|
|
Selendroid.prototype._deviceConfigure = Device.prototype.configure;
|
|
Selendroid.prototype._setAndroidArgs = androidCommon.setAndroidArgs;
|
|
Selendroid.prototype.setAndroidArgs = function () {
|
|
this._setAndroidArgs();
|
|
this.args.systemPort = this.args.selendroidPort;
|
|
this.proxyPort = this.args.systemPort;
|
|
};
|
|
|
|
Selendroid.prototype.start = function (cb) {
|
|
logger.debug("Starting selendroid server");
|
|
this.adb = new ADB(this.args);
|
|
|
|
var modServerExists = false
|
|
, modAppPkg = null;
|
|
|
|
var checkModServerExists = function (cb) {
|
|
this.selendroidServerPath = path.resolve(this.args.tmpDir,
|
|
'selendroid.' + this.args.appPackage + '.apk');
|
|
modAppPkg = this.args.appPackage + '.selendroid';
|
|
fs.stat(this.selendroidServerPath, function (err) {
|
|
modServerExists = !err;
|
|
cb();
|
|
});
|
|
}.bind(this);
|
|
|
|
var conditionalUninstallSelendroid = function (cb) {
|
|
if (!modServerExists) {
|
|
logger.debug("Rebuilt selendroid apk does not exist, uninstalling " +
|
|
"any instances of it on device to make way for new one");
|
|
this.adb.uninstallApk(modAppPkg, cb);
|
|
} else {
|
|
logger.debug("Rebuilt selendroid apk exists, doing nothing");
|
|
cb();
|
|
}
|
|
}.bind(this);
|
|
|
|
var conditionalInsertManifest = function (cb) {
|
|
if (!modServerExists) {
|
|
logger.debug("Rebuilt selendroid server does not exist, inserting " +
|
|
"modified manifest");
|
|
this.insertSelendroidManifest(this.serverApk, cb);
|
|
} else {
|
|
logger.debug("Rebuilt selendroid server already exists, no need to " +
|
|
"rebuild it with a new manifest");
|
|
cb();
|
|
}
|
|
}.bind(this);
|
|
|
|
var conditionalInstallSelendroid = function (cb) {
|
|
this.adb.isAppInstalled(modAppPkg, function (e, installed) {
|
|
if (!installed) {
|
|
logger.debug("Rebuilt selendroid is not installed, installing it");
|
|
this.adb.install(this.selendroidServerPath, cb);
|
|
} else {
|
|
logger.debug("Rebuilt selendroid is already installed");
|
|
cb();
|
|
}
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
async.series([
|
|
this.ensureServerExists.bind(this),
|
|
this.prepareDevice.bind(this),
|
|
this.packageAndLaunchActivityFromManifest.bind(this),
|
|
checkModServerExists,
|
|
conditionalUninstallSelendroid,
|
|
conditionalInsertManifest,
|
|
this.checkSelendroidCerts.bind(this),
|
|
conditionalInstallSelendroid,
|
|
this.extractStringsSelendroid.bind(this),
|
|
this.uninstallApp.bind(this),
|
|
this.installApp.bind(this),
|
|
this.forwardPort.bind(this),
|
|
this.pushUnlock.bind(this),
|
|
this.unlockScreen.bind(this),
|
|
this.pushSelendroid.bind(this),
|
|
this.waitForServer.bind(this)
|
|
], function (err) {
|
|
if (err) return cb(err);
|
|
async.series([
|
|
this.createSession.bind(this),
|
|
this.initAutoWebview.bind(this)
|
|
], function (err, res) {
|
|
if (err) return cb(err);
|
|
// `createSession` returns session id, so send that along
|
|
cb(null, res[0]);
|
|
});
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.pushSelendroid = function (cb) {
|
|
var instrumentWith = this.args.appPackage + ".selendroid/" +
|
|
"io.selendroid.ServerInstrumentation";
|
|
this.adb.instrument(this.args.appPackage, this.args.appActivity, instrumentWith, cb);
|
|
};
|
|
|
|
Selendroid.prototype.checkSelendroidCerts = function (cb) {
|
|
var alreadyReturned = false
|
|
, checks = 0;
|
|
|
|
var onDoneSigning = function () {
|
|
checks++;
|
|
if (checks === 2 && !alreadyReturned) {
|
|
cb();
|
|
}
|
|
};
|
|
|
|
// these run in parallel
|
|
var apks = [this.selendroidServerPath, this.args.app];
|
|
_.each(apks, function (apk) {
|
|
logger.debug("Checking signed status of " + apk);
|
|
this.adb.checkApkCert(apk, this.args.appPackage, function (err, isSigned) {
|
|
if (err) return cb(err);
|
|
if (isSigned) return onDoneSigning();
|
|
this.adb.sign(apk, function (err) {
|
|
if (err && !alreadyReturned) {
|
|
alreadyReturned = true;
|
|
return cb(err);
|
|
}
|
|
onDoneSigning();
|
|
});
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.stop = function (cb) {
|
|
|
|
var completeShutdown = function (cb) {
|
|
logger.debug("Stopping selendroid server");
|
|
this.deleteSession(function (err) {
|
|
cb(err ? 1 : 0);
|
|
});
|
|
}.bind(this);
|
|
|
|
if (this.args.fullReset) {
|
|
logger.debug("Removing app from device");
|
|
this.uninstallApp(function (err) {
|
|
if (err) {
|
|
// simply warn on error here, because we don't want to stop the shutdown
|
|
// process
|
|
logger.warn(err);
|
|
}
|
|
completeShutdown(cb);
|
|
});
|
|
} else {
|
|
completeShutdown(cb);
|
|
}
|
|
|
|
|
|
};
|
|
|
|
Selendroid.prototype.keyevent = function (keycode, metastate, cb) {
|
|
this.adb.keyevent(keycode, function () {
|
|
cb(null, {
|
|
status: status.codes.Success.code
|
|
, value: null
|
|
});
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Execute an arbitrary function and handle potential ADB disconnection before
|
|
* proceeding
|
|
*/
|
|
Selendroid.prototype.wrapActionAndHandleADBDisconnect = function (action, ocb) {
|
|
async.series([
|
|
function (cb) {
|
|
action(cb);
|
|
}.bind(this)
|
|
, this.adb.restart.bind(this.adb)
|
|
, this.forwardPort.bind(this)
|
|
], function (err) {
|
|
ocb(err);
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.ensureServerExists = function (cb) {
|
|
logger.debug("Checking whether selendroid is built yet");
|
|
var selBin = path.resolve(__dirname, "..", "..", "..", "build", "selendroid",
|
|
"selendroid.apk");
|
|
fs.stat(selBin, function (err) {
|
|
if (err) {
|
|
logger.debug("Selendroid needs to be built; please run ./reset.sh " +
|
|
"--selendroid");
|
|
return cb(err);
|
|
}
|
|
logger.debug("Selendroid server exists!");
|
|
this.serverApk = selBin;
|
|
cb(null);
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.waitForServer = function (cb) {
|
|
var waitMs = 20000
|
|
, intMs = 800
|
|
, start = Date.now();
|
|
|
|
var pingServer = function () {
|
|
this.proxyTo('/wd/hub/status', 'GET', null, function (err, res, body) {
|
|
if (body === null || typeof body === "undefined" || !body.trim()) {
|
|
if (Date.now() - start < waitMs) {
|
|
setTimeout(pingServer, intMs);
|
|
} else {
|
|
cb(new Error("Waited " + (waitMs / 1000) + " secs for " +
|
|
"selendroid server and it never showed up"));
|
|
}
|
|
} else {
|
|
logger.debug("Selendroid server is alive!");
|
|
cb(null);
|
|
}
|
|
});
|
|
}.bind(this);
|
|
|
|
pingServer();
|
|
};
|
|
|
|
Selendroid.prototype.createSession = function (cb) {
|
|
logger.debug("Listening for Selendroid logs");
|
|
this.adb.logcat.on('log', function (logObj) {
|
|
if (/System/.test(logObj.message)) {
|
|
var type = "";
|
|
if (/System\.err/.test(logObj.message)) {
|
|
type = " ERR";
|
|
}
|
|
var msg = logObj.message.replace(/^.+: /, '');
|
|
logger.debug("[SELENDROID" + type + "] " + msg);
|
|
}
|
|
}.bind(this));
|
|
logger.debug("Creating Selendroid session");
|
|
var data = {desiredCapabilities: this.capabilities};
|
|
this.proxyTo('/wd/hub/session', 'POST', data, function (err, res, body) {
|
|
if (err) return cb(err);
|
|
|
|
if (res.statusCode === 301 && body.sessionId) {
|
|
logger.debug("Successfully started selendroid session");
|
|
this.selendroidSessionId = body.sessionId;
|
|
this.proxySessionId = this.selendroidSessionId;
|
|
this.adb.waitForActivity(this.args.appWaitPackage, this.args.appWaitActivity, 1800,
|
|
function (err) {
|
|
if (err) {
|
|
logger.debug("Selendroid hasn't started app yet, let's do it " +
|
|
"manually with adb.startApp");
|
|
var onStart = function (err) {
|
|
if (err) return cb(err);
|
|
return cb(null, body.sessionId);
|
|
}.bind(this);
|
|
|
|
return this.adb.startApp({
|
|
pkg: this.args.appPackage,
|
|
activity: this.args.appActivity,
|
|
action: this.args.intentAction,
|
|
category: this.args.intentCategory,
|
|
flags: this.args.intentFlags,
|
|
waitPkg: this.args.appWaitPackage,
|
|
waitActivity: this.args.appWaitActivity,
|
|
optionalIntentArguments: this.args.optionalIntentArguments,
|
|
retry: false
|
|
}, onStart);
|
|
}
|
|
return cb(null, body.sessionId);
|
|
}.bind(this), 1800);
|
|
} else {
|
|
logger.error("Selendroid create session did not work. Status was " +
|
|
res.statusCode + " and body was " + body);
|
|
cb(new Error("Did not get session redirect from selendroid"));
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.deleteSession = function (cb) {
|
|
var url = '/wd/hub/session/' + this.selendroidSessionId;
|
|
this.proxyTo(url, 'DELETE', null, function (err, res) {
|
|
if (err) return cb(err);
|
|
if (res.statusCode !== 200) return cb(new Error("Status was not 200"));
|
|
this.adb.forceStop(this.args.appPackage, function (err) {
|
|
if (err) return cb(err);
|
|
this.adb.stopLogcat(cb);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.proxyTo = proxyTo;
|
|
|
|
Selendroid.prototype.insertSelendroidManifest = function (serverPath, cb) {
|
|
logger.debug("Inserting selendroid manifest");
|
|
var newServerPath = this.selendroidServerPath
|
|
, newPackage = this.args.appPackage + '.selendroid'
|
|
, srcManifest = path.resolve(__dirname, '..', '..', '..', 'build',
|
|
'selendroid', 'AndroidManifest.xml')
|
|
, dstDir = path.resolve(this.args.tmpDir, this.args.appPackage)
|
|
, dstManifest = path.resolve(dstDir, 'AndroidManifest.xml');
|
|
|
|
try {
|
|
fs.mkdirSync(dstDir);
|
|
} catch (e) {
|
|
if (e.message.indexOf("EEXIST") === -1) {
|
|
throw e;
|
|
}
|
|
}
|
|
fs.writeFileSync(dstManifest, fs.readFileSync(srcManifest, "utf8"), "utf8");
|
|
async.series([
|
|
function (cb) { mkdirp(dstDir, cb); }.bind(this),
|
|
function (cb) { this.adb.checkSdkBinaryPresent("aapt", cb); }.bind(this),
|
|
function (cb) {
|
|
this.adb.compileManifest(dstManifest, newPackage,
|
|
this.args.appPackage, cb);
|
|
}.bind(this),
|
|
function (cb) {
|
|
this.adb.insertManifest(dstManifest, serverPath,
|
|
newServerPath, cb);
|
|
}.bind(this)
|
|
], cb);
|
|
};
|
|
|
|
Selendroid.prototype.setLocation = androidController.setLocation;
|
|
Selendroid.prototype.removeApp = androidController.removeApp;
|
|
Selendroid.prototype.unpackApp = androidController.unpackApp;
|
|
|
|
Selendroid.prototype.translatePath = function (req) {
|
|
var path = req.originalUrl;
|
|
if (path.indexOf("contexts") !== -1) {
|
|
logger.debug("Temporarily translating 'contexts' to 'window_handles");
|
|
path = path.replace("contexts", "window_handles");
|
|
} else if (path.indexOf("context") !== -1) {
|
|
logger.debug("Temporarily translating 'context' to 'window'");
|
|
path = path.replace("context", "window");
|
|
}
|
|
req.originalUrl = path;
|
|
};
|
|
|
|
Selendroid.prototype.extractStringsSelendroid = function (cb) {
|
|
this.extractStrings(function () {
|
|
cb();
|
|
});
|
|
};
|
|
|
|
Selendroid.prototype.getStrings = function (language, cb) {
|
|
if (this.language && this.language === language) {
|
|
// Return last strings
|
|
return cb(null, {
|
|
status: status.codes.Success.code,
|
|
value: this.apkStrings
|
|
});
|
|
}
|
|
|
|
// Extract and return strings
|
|
return this.extractStrings(function () {
|
|
cb(null, {
|
|
status: status.codes.Success.code,
|
|
value: this.apkStrings
|
|
});
|
|
}.bind(this), language);
|
|
};
|
|
|
|
|
|
_.extend(Selendroid.prototype, androidHybrid);
|
|
_.extend(Selendroid.prototype, androidContextController);
|
|
|
|
|
|
Selendroid.prototype.isChromedriverContext = function (windowName) {
|
|
return windowName === this.CHROMIUM_WIN;
|
|
};
|
|
|
|
Selendroid.prototype.getContexts = function (cb) {
|
|
var chromiumViews = [];
|
|
this.listWebviews(function (err, webviews) {
|
|
if (err) return cb(err);
|
|
if (_.contains(webviews, this.CHROMIUM_WIN)) {
|
|
chromiumViews = [this.CHROMIUM_WIN];
|
|
} else {
|
|
chromiumViews = [];
|
|
}
|
|
|
|
var selendroidViews = [];
|
|
var reqUrl = this.selendroidHost + ':' + this.selendroidPort + '/wd/hub/session/' + this.selendroidSessionId;
|
|
doRequest(reqUrl + '/window_handles', 'GET', {}, null, function (err, res) {
|
|
if (err) return cb(err);
|
|
selendroidViews = JSON.parse(res.body).value;
|
|
this.contexts = _.union(selendroidViews, chromiumViews);
|
|
cb(null, {sessionId: this.selendroidSessionId, status: status.codes.Success.code, value: this.contexts});
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
Selendroid.prototype.defaultWebviewName = function () {
|
|
return this.WEBVIEW_WIN + "_0";
|
|
};
|
|
|
|
module.exports = Selendroid;
|