mirror of
https://github.com/appium/appium.git
synced 2026-02-09 03:09:02 -06:00
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.
472 lines
14 KiB
JavaScript
472 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
var errors = require('../../server/errors.js')
|
|
, exec = require('child_process').exec
|
|
, path = require('path')
|
|
, fs = require('fs')
|
|
, ADB = require('./adb.js')
|
|
, Device = require('../device.js')
|
|
, helpers = require('../../helpers.js')
|
|
, getTempPath = helpers.getTempPath
|
|
, _ = require('underscore')
|
|
, logger = require('../../server/logger.js').get('appium')
|
|
, deviceCommon = require('../common.js')
|
|
, status = require("../../server/status.js")
|
|
, helperJarPath = path.resolve(__dirname, 'helpers')
|
|
, async = require('async')
|
|
, androidController = require('./android-controller.js')
|
|
, androidCommon = require('./android-common.js')
|
|
, androidHybrid = require('./android-hybrid.js')
|
|
, UiAutomator = require('./uiautomator.js')
|
|
, UnknownError = errors.UnknownError;
|
|
|
|
var Android = function () {
|
|
this.init();
|
|
};
|
|
|
|
_.extend(Android.prototype, Device.prototype);
|
|
|
|
Android.prototype._deviceInit = Device.prototype.init;
|
|
Android.prototype.init = function () {
|
|
this._deviceInit();
|
|
this.appExt = ".apk";
|
|
this.capabilities = {
|
|
platform: 'LINUX'
|
|
, browserName: 'Android'
|
|
, platformVersion: '4.1'
|
|
, webStorageEnabled: false
|
|
, takesScreenshot: true
|
|
, javascriptEnabled: true
|
|
, databaseEnabled: false
|
|
};
|
|
this.args.devicePort = 4724;
|
|
this.appMd5Hash = null;
|
|
this.args.avd = null;
|
|
this.initQueue();
|
|
this.implicitWaitMs = 0;
|
|
this.shuttingDown = false;
|
|
this.adb = null;
|
|
this.uiautomator = null;
|
|
this.uiautomatorRestartOnExit = false;
|
|
this.uiautomatorIgnoreExit = false;
|
|
this.swipeStepsPerSec = 28;
|
|
this.dragStepsPerSec = 40;
|
|
this.asyncWaitMs = 0;
|
|
this.remote = null;
|
|
this.contexts = [];
|
|
this.curContext = this.defaultContext();
|
|
this.didLaunch = false;
|
|
this.launchCb = function () {};
|
|
this.uiautomatorExitCb = function () {};
|
|
this.dataDir = null;
|
|
this.chromedriver = null;
|
|
this.isProxy = false;
|
|
this.proxyHost = null;
|
|
this.proxyPort = null;
|
|
this.proxySessionId = null;
|
|
this.avoidProxy = [
|
|
['POST', new RegExp('^/wd/hub/session/[^/]+/window')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/window_handle')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/window_handles')]
|
|
, ['POST', new RegExp('^/wd/hub/session/[^/]+/context')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/context')]
|
|
, ['GET', new RegExp('^/wd/hub/session/[^/]+/contexts')]
|
|
];
|
|
};
|
|
|
|
Android.prototype._deviceConfigure = Device.prototype.configure;
|
|
|
|
Android.prototype.start = function (cb, onDie) {
|
|
this.launchCb = cb;
|
|
this.uiautomatorExitCb = onDie;
|
|
|
|
if (this.adb === null) {
|
|
// Pass Android opts and Android ref to adb.
|
|
logger.info("Starting android appium");
|
|
this.adb = new ADB(this.args);
|
|
this.uiautomator = new UiAutomator(this.adb, this.args);
|
|
this.uiautomator.setExitHandler(this.onUiautomatorExit.bind(this));
|
|
|
|
logger.debug("Using fast reset? " + this.args.fastReset);
|
|
async.series([
|
|
this.prepareDevice.bind(this),
|
|
this.packageAndLaunchActivityFromManifest.bind(this),
|
|
this.checkApiLevel.bind(this),
|
|
this.pushStrings.bind(this),
|
|
this.processFromManifest.bind(this),
|
|
this.uninstallApp.bind(this),
|
|
this.installApp.bind(this),
|
|
this.forwardPort.bind(this),
|
|
this.pushAppium.bind(this),
|
|
this.pushUnlock.bind(this),
|
|
this.uiautomator.start.bind(this.uiautomator),
|
|
this.wakeUp.bind(this),
|
|
this.unlockScreen.bind(this),
|
|
this.getDataDir.bind(this),
|
|
this.startApp.bind(this)
|
|
], function (err) {
|
|
if (err) {
|
|
this.shutdown(function () {
|
|
this.launchCb(err);
|
|
}.bind(this));
|
|
} else {
|
|
this.didLaunch = true;
|
|
this.launchCb();
|
|
}
|
|
}.bind(this));
|
|
} else {
|
|
logger.error("Tried to start ADB when we already have one running!");
|
|
}
|
|
};
|
|
|
|
Android.prototype.onLaunch = function (err) {
|
|
var readyToGo = function () {
|
|
this.didLaunch = true;
|
|
this.launchCb();
|
|
}.bind(this);
|
|
|
|
var giveUp = function (err) {
|
|
this.shutdown(function () {
|
|
this.launchCb(err);
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
if (err) {
|
|
if (this.checkShouldRelaunch(err)) {
|
|
logger.error(err);
|
|
logger.error("Above error isn't fatal, maybe relaunching adb will help....");
|
|
this.adb.waitForDevice(function (err) {
|
|
if (err) return giveUp(err);
|
|
readyToGo();
|
|
});
|
|
} else {
|
|
giveUp(err);
|
|
}
|
|
} else {
|
|
readyToGo();
|
|
}
|
|
};
|
|
|
|
Android.prototype.restartUiautomator = function (cb) {
|
|
async.series([
|
|
this.forwardPort.bind(this)
|
|
, this.uiautomator.start.bind(this.uiautomator)
|
|
], cb);
|
|
};
|
|
|
|
/*
|
|
* Execute an arbitrary function and handle potential ADB disconnection before
|
|
* proceeding
|
|
*/
|
|
Android.prototype.wrapActionAndHandleADBDisconnect = function (action, ocb) {
|
|
async.series([
|
|
function (cb) {
|
|
this.uiautomatorIgnoreExit = true;
|
|
action(cb);
|
|
}.bind(this)
|
|
, this.adb.restart.bind(this.adb)
|
|
, this.restartUiautomator.bind(this)
|
|
], function (err) {
|
|
this.uiautomatorIgnoreExit = false;
|
|
ocb(err);
|
|
}.bind(this));
|
|
};
|
|
|
|
Android.prototype.onUiautomatorExit = function () {
|
|
logger.info("UiAutomator exited");
|
|
var respondToClient = function () {
|
|
this.cleanupChromedriver(function () {
|
|
this.cleanup();
|
|
if (!this.didLaunch) {
|
|
var msg = "UiAutomator quit before it successfully launched";
|
|
logger.error(msg);
|
|
this.launchCb(new Error(msg));
|
|
return;
|
|
} else if (typeof this.cbForCurrentCmd === "function") {
|
|
var error = new UnknownError("UiAutomator died while responding to " +
|
|
"command, please check appium logs!");
|
|
this.cbForCurrentCmd(error, null);
|
|
}
|
|
// make sure appium.js knows we crashed so it can clean up
|
|
this.uiautomatorExitCb();
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
if (this.adb) {
|
|
var uninstall = function () {
|
|
logger.info("Attempting to uninstall app");
|
|
this.uninstallApp(function () {
|
|
this.shuttingDown = false;
|
|
respondToClient();
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
if (!this.uiautomatorIgnoreExit) {
|
|
this.adb.ping(function (err, ok) {
|
|
if (ok) {
|
|
uninstall();
|
|
} else {
|
|
logger.debug(err);
|
|
this.adb.restart(function (err) {
|
|
if (err) {
|
|
logger.debug(err);
|
|
}
|
|
if (this.uiautomatorRestartOnExit) {
|
|
this.uiautomatorRestartOnExit = false;
|
|
this.restartUiautomator(function (err) {
|
|
if (err) {
|
|
logger.debug(err);
|
|
uninstall();
|
|
}
|
|
}.bind(this));
|
|
} else {
|
|
uninstall();
|
|
}
|
|
}.bind(this));
|
|
}
|
|
}.bind(this));
|
|
} else {
|
|
this.uiautomatorIgnoreExit = false;
|
|
}
|
|
} else {
|
|
logger.info("We're in uiautomator's exit callback but adb is gone already");
|
|
respondToClient();
|
|
}
|
|
};
|
|
|
|
Android.prototype.checkShouldRelaunch = function (launchErr) {
|
|
if (launchErr.message === null || typeof launchErr.message === 'undefined') {
|
|
logger.error("We're checking if we should relaunch based on something " +
|
|
"which isn't an error object. Check the codez!");
|
|
return false;
|
|
}
|
|
var msg = launchErr.message.toString();
|
|
var relaunchOn = [
|
|
'Could not find a connected Android device'
|
|
, 'Device did not become ready'
|
|
];
|
|
var relaunch = false;
|
|
_.each(relaunchOn, function (relaunchMsg) {
|
|
relaunch = relaunch || msg.indexOf(relaunchMsg) !== -1;
|
|
});
|
|
return relaunch;
|
|
};
|
|
|
|
Android.prototype.checkApiLevel = function (cb) {
|
|
this.adb.getApiLevel(function (err, apiLevel) {
|
|
if (err) return cb(err);
|
|
|
|
if (parseInt(apiLevel) < 17) {
|
|
var msg = "Android devices must be of API level 17 or higher. Please change your device to Selendroid or upgrade Android on your device.";
|
|
logger.error(msg); // logs the error when we encounter it
|
|
return cb(new Error(msg)); // send the error up the chain
|
|
}
|
|
|
|
cb();
|
|
});
|
|
};
|
|
|
|
Android.prototype.processFromManifest = function (cb) {
|
|
if (!this.args.app) {
|
|
return cb();
|
|
} else { // apk must be local to process the manifest.
|
|
this.adb.processFromManifest(this.args.app, function (err, process) {
|
|
var value = process || this.args.appPackage;
|
|
// must trim to last 15 for android's ps binary
|
|
if (value.length > 15) value = value.substr(value.length - 15);
|
|
|
|
this.appProcess = value;
|
|
logger.debug("Set app process to: " + this.appProcess);
|
|
|
|
cb();
|
|
}.bind(this));
|
|
}
|
|
};
|
|
|
|
Android.prototype.pushStrings = function (cb) {
|
|
var remotePath = '/data/local/tmp';
|
|
var stringsJson = 'strings.json';
|
|
if (!fs.existsSync(this.args.app)) {
|
|
// apk doesn't exist locally so remove old strings.json
|
|
logger.debug("Apk doesn't exist. Removing old strings.json");
|
|
this.adb.rimraf(remotePath + '/' + stringsJson, function (err) {
|
|
if (err) return cb(new Error("Could not remove old strings"));
|
|
cb();
|
|
});
|
|
} else {
|
|
var stringsFromApkJarPath = path.resolve(helperJarPath,
|
|
'strings_from_apk.jar');
|
|
if (!this.args.appPackage) {
|
|
return cb(new Error("Parameter 'appPackage' is required for launching application"));
|
|
}
|
|
var outputPath = path.resolve(getTempPath(), this.args.appPackage);
|
|
var makeStrings = ['java -jar "', stringsFromApkJarPath,
|
|
'" "', this.args.app, '" "', outputPath, '"'].join('');
|
|
logger.debug(makeStrings);
|
|
exec(makeStrings, { maxBuffer: 524288 }, function (err, stdout, stderr) {
|
|
if (err) {
|
|
// if we can't get strings, just dump an empty json and continue
|
|
logger.warn("Error getting strings.xml from apk");
|
|
logger.debug(stderr);
|
|
var remoteFile = path.resolve(remotePath, stringsJson);
|
|
return this.adb.shell("echo '{}' > " + remoteFile, cb);
|
|
}
|
|
var jsonFile = path.resolve(outputPath, stringsJson);
|
|
this.adb.push(jsonFile, remotePath, function (err) {
|
|
if (err) return cb(new Error("Could not push strings.json"));
|
|
cb();
|
|
});
|
|
}.bind(this));
|
|
}
|
|
};
|
|
|
|
Android.prototype.pushAppium = function (cb) {
|
|
logger.debug("Pushing appium bootstrap to device...");
|
|
var binPath = path.resolve(__dirname, "..", "..", "..", "build",
|
|
"android_bootstrap", "AppiumBootstrap.jar");
|
|
fs.stat(binPath, function (err) {
|
|
if (err) {
|
|
cb(new Error("Could not find AppiumBootstrap.jar; please run " +
|
|
"'grunt buildAndroidBootstrap'"));
|
|
} else {
|
|
this.adb.push(binPath, this.remoteTempPath(), cb);
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
Android.prototype.startApp = function (cb) {
|
|
if (!this.args.androidCoverage) {
|
|
this.adb.startApp(this.args.appPackage, this.args.appActivity, this.args.appWaitPackage,
|
|
this.args.appWaitActivity, cb);
|
|
} else {
|
|
this.adb.androidCoverage(this.args.androidCoverage, this.args.appWaitPackage,
|
|
this.args.appWaitActivity, cb);
|
|
}
|
|
};
|
|
|
|
Android.prototype.stop = function (cb) {
|
|
this.shuttingDown = true;
|
|
|
|
var completeShutdown = function (cb) {
|
|
if (this.adb) {
|
|
this.adb.goToHome(function () {
|
|
this.shutdown(cb);
|
|
}.bind(this));
|
|
} else {
|
|
this.shutdown(cb);
|
|
}
|
|
}.bind(this);
|
|
|
|
if (this.args.fullReset) {
|
|
logger.info("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);
|
|
}
|
|
|
|
|
|
};
|
|
|
|
Android.prototype.cleanup = function () {
|
|
logger.info("Cleaning up android objects");
|
|
this.adb = null;
|
|
this.uiautomator = null;
|
|
this.shuttingDown = false;
|
|
};
|
|
|
|
Android.prototype.shutdown = function (cb) {
|
|
var next = function () {
|
|
this.cleanupChromedriver(function () {
|
|
if (this.uiautomator) {
|
|
this.uiautomator.shutdown(function () {
|
|
this.cleanup();
|
|
cb();
|
|
}.bind(this));
|
|
} else {
|
|
this.cleanup();
|
|
cb();
|
|
}
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
if (this.adb) {
|
|
this.adb.endAndroidCoverage();
|
|
this.adb.stopLogcat(function () {
|
|
next();
|
|
}.bind(this));
|
|
} else {
|
|
next();
|
|
}
|
|
};
|
|
|
|
Android.prototype.proxy = deviceCommon.proxy;
|
|
Android.prototype.respond = deviceCommon.respond;
|
|
|
|
Android.prototype.initQueue = function () {
|
|
this.queue = async.queue(function (task, cb) {
|
|
var action = task.action,
|
|
params = task.params;
|
|
|
|
this.cbForCurrentCmd = cb;
|
|
|
|
if (this.adb && !this.shuttingDown) {
|
|
this.uiautomator.sendAction(action, params, function (response) {
|
|
this.cbForCurrentCmd = null;
|
|
if (typeof cb === 'function') {
|
|
this.respond(response, cb);
|
|
}
|
|
}.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);
|
|
}
|
|
}.bind(this), 1);
|
|
};
|
|
|
|
Android.prototype.push = function (elem) {
|
|
this.queue.push({action: elem[0][0], params: elem[0][1] || {}}, elem[1]);
|
|
};
|
|
|
|
Android.prototype.wakeUp = function (cb) {
|
|
// requires an appium bootstrap connection loaded
|
|
logger.debug("Waking up device if it's not alive");
|
|
this.proxy(["wake", {}], cb);
|
|
};
|
|
|
|
Android.prototype.getDataDir = function (cb) {
|
|
this.proxy(["getDataDir", {}], function (err, res) {
|
|
if (err) return cb(err);
|
|
this.dataDir = res.value;
|
|
logger.debug("dataDir set to: " + this.dataDir);
|
|
cb();
|
|
}.bind(this));
|
|
};
|
|
|
|
Android.prototype.waitForActivityToStop = function (cb) {
|
|
this.adb.waitForNotActivity(this.args.appWaitPackage, this.args.appWaitActivity, cb);
|
|
};
|
|
|
|
Android.prototype.waitForCondition = deviceCommon.waitForCondition;
|
|
|
|
_.extend(Android.prototype, androidController);
|
|
_.extend(Android.prototype, androidCommon);
|
|
_.extend(Android.prototype, androidHybrid);
|
|
|
|
module.exports = Android;
|