appium-adb-test
Version:
Android Debug Bridge interface
594 lines (522 loc) • 18.8 kB
JavaScript
import log from '../logger.js';
import { getIMEListFromOutput, isShowingLockscreen, isCurrentFocusOnKeyguard,
getSurfaceOrientation, isScreenOnFully } from '../helpers.js';
import path from 'path';
import _ from 'lodash';
import { fs } from 'appium-support';
import net from 'net';
import Logcat from '../logcat';
import { sleep, retryInterval } from 'asyncbox';
import { SubProcess } from 'teen_process';
let methods = {};
methods.getAdbWithCorrectAdbPath = async function () {
this.executable.path = await this.getSdkBinaryPath("adb");
this.binaries.adb = this.executable.path;
return this.adb;
};
methods.initAapt = async function () {
this.binaries.aapt = await this.getSdkBinaryPath("aapt");
};
methods.initZipAlign = async function () {
this.binaries.zipalign = await this.getSdkBinaryPath("zipalign");
};
methods.getApiLevel = async function () {
if (!this._apiLevel) {
try {
this._apiLevel = await this.shell(['getprop', 'ro.build.version.sdk']);
} catch (e) {
log.errorAndThrow(`Error getting device API level. Original error: ${e.message}`);
}
}
log.debug(`Device API level: ${this._apiLevel}`);
return this._apiLevel;
};
methods.getPlatformVersion = async function () {
log.info("Getting device platform version");
try {
return await this.shell(['getprop', 'ro.build.version.release']);
} catch (e) {
log.errorAndThrow(`Error getting device platform version. Original error: ${e.message}`);
}
};
methods.isDeviceConnected = async function () {
let devices = await this.getConnectedDevices();
return devices.length > 0;
};
methods.mkdir = async function (remotePath) {
return await this.shell(['mkdir', '-p', remotePath]);
};
methods.isValidClass = function (classString) {
// some.package/some.package.Activity
return new RegExp(/^[a-zA-Z0-9\./_]+$/).exec(classString);
};
methods.forceStop = async function (pkg) {
return await this.shell(['am', 'force-stop', pkg]);
};
methods.clear = async function (pkg) {
return await this.shell(['pm', 'clear', pkg]);
};
methods.stopAndClear = async function (pkg) {
try {
await this.forceStop(pkg);
await this.clear(pkg);
} catch (e) {
log.errorAndThrow(`Cannot stop and clear ${pkg}. Original error: ${e.message}`);
}
};
methods.availableIMEs = async function () {
try {
return getIMEListFromOutput(await this.shell(['ime', 'list', '-a']));
} catch (e) {
log.errorAndThrow(`Error getting available IME's. Original error: ${e.message}`);
}
};
methods.enabledIMEs = async function () {
try {
return getIMEListFromOutput(await this.shell(['ime', 'list']));
} catch (e) {
log.errorAndThrow(`Error getting enabled IME's. Original error: ${e.message}`);
}
};
methods.enableIME = async function (imeId) {
await this.shell(['ime', 'enable', imeId]);
};
methods.disableIME = async function (imeId) {
await this.shell(['ime', 'disable', imeId]);
};
methods.setIME = async function (imeId) {
await this.shell(['ime', 'set', imeId]);
};
methods.defaultIME = async function () {
try {
let engine = await this.shell(['settings', 'get', 'secure', 'default_input_method']);
return engine.trim();
} catch (e) {
log.errorAndThrow(`Error getting default IME. Original error: ${e.message}`);
}
};
methods.keyevent = async function (keycode) {
// keycode must be an int.
let code = parseInt(keycode, 10);
await this.shell(['input', 'keyevent', code]);
};
methods.inputText = async function (text) {
/* jshint ignore:start */
// need to escape whitespace and ( ) < > | ; & * \ ~ " '
text = text
.replace('\\', '\\\\')
.replace('(', '\(')
.replace(')', '\)')
.replace('<', '\<')
.replace('>', '\>')
.replace('|', '\|')
.replace(';', '\;')
.replace('&', '\&')
.replace('*', '\*')
.replace('~', '\~')
.replace('"', '\"')
.replace("'", "\'")
.replace(' ', '%s');
/* jshint ignore:end */
await this.shell(['input', 'text', text]);
};
methods.clearTextField = async function (length = 100) {
// assumes that the EditText field already has focus
log.debug(`Clearing up to ${length} characters`);
if (length === 0) {
return;
}
let args = ['input', 'keyevent'];
for (let i = 0; i < length; i++) {
// we cannot know where the cursor is in the text field, so delete both before
// and after so that we get rid of everything
// https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_DEL
// https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_FORWARD_DEL
args.push('67', '112');
}
await this.shell(args);
};
methods.lock = async function () {
let locked = await this.isScreenLocked();
locked = await this.isScreenLocked();
if (!locked) {
log.debug("Pressing the KEYCODE_POWER button to lock screen");
await this.keyevent(26);
// wait for the screen to lock
await retryInterval(10, 500, async () => {
locked = await this.isScreenLocked();
if (!locked) {
log.errorAndThrow("Waiting for screen to lock.");
}
});
} else {
log.debug("Screen is already locked. Doing nothing.");
}
};
methods.back = async function () {
log.debug("Pressing the BACK button");
await this.keyevent(4);
};
methods.goToHome = async function () {
log.debug("Pressing the HOME button");
await this.keyevent(3);
};
methods.getAdbPath = function () {
return this.executable.path;
};
methods.getScreenOrientation = async function () {
let stdout = await this.shell(['dumpsys', 'input']);
return getSurfaceOrientation(stdout);
};
methods.isScreenLocked = async function () {
let stdout = await this.shell(['dumpsys', 'window']);
if (process.env.APPIUM_LOG_DUMPSYS) {
// optional debugging
// if the method is not working, turn it on and send us the output
let dumpsysFile = path.resolve(process.cwd(), "dumpsys.log");
log.debug(`Writing dumpsys output to ${dumpsysFile}`);
await fs.writeFile(dumpsysFile, stdout);
}
return (isShowingLockscreen(stdout) || isCurrentFocusOnKeyguard(stdout) ||
!isScreenOnFully(stdout));
};
methods.isSoftKeyboardPresent = async function () {
try {
let stdout = await this.shell(['dumpsys', 'input_method']);
let isKeyboardShown = false,
canCloseKeyboard = false,
inputShownMatch = /mInputShown=\w+/gi.exec(stdout);
if (inputShownMatch && inputShownMatch[0]) {
isKeyboardShown = inputShownMatch[0].split('=')[1] === 'true';
let isInputViewShownMatch = /mIsInputViewShown=\w+/gi.exec(stdout);
if (isInputViewShownMatch && isInputViewShownMatch[0]) {
canCloseKeyboard = isInputViewShownMatch[0].split('=')[1] === 'true';
}
}
return {isKeyboardShown, canCloseKeyboard};
} catch (e) {
log.errorAndThrow(`Error finding softkeyboard. Original error: ${e.message}`);
}
};
methods.sendTelnetCommand = async function (command) {
log.debug(`Sending telnet command to device: ${command}`);
let port = await this.getEmulatorPort();
return new Promise((resolve, reject) => {
let conn = net.createConnection(port, 'localhost'),
connected = false,
readyRegex = /^OK$/m,
dataStream = "",
res = null;
conn.on('connect', () => {
log.debug("Socket connection to device created");
});
conn.on('data', (data) => {
data = data.toString('utf8');
if (!connected) {
if (readyRegex.test(data)) {
connected = true;
log.debug("Socket connection to device ready");
conn.write(command + "\n");
}
} else {
dataStream += data;
if (readyRegex.test(data)) {
res = dataStream.replace(readyRegex, "").trim();
res = _.last(res.trim().split('\n'));
log.debug(`Telnet command got response: ${res}`);
conn.write("quit\n");
}
}
});
conn.on('close', () => {
if (res === null) {
reject(new Error("Never got a response from command"));
} else {
resolve(res);
}
});
});
};
methods.isAirplaneModeOn = async function () {
let stdout = await this.shell(['settings', 'get', 'global', 'airplane_mode_on']);
return parseInt(stdout, 10) !== 0;
};
/*
* on: true (to turn on) or false (to turn off)
*/
methods.setAirplaneMode = async function (on) {
await this.shell(['settings', 'put', 'global', 'airplane_mode_on', on ? 1 : 0]);
};
/*
* on: true (to turn on) or false (to turn off)
*/
methods.broadcastAirplaneMode = async function (on) {
let args = ['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE',
'--ez', 'state', on ? 'true' : 'false'];
await this.shell(args);
};
methods.isWifiOn = async function () {
let stdout = await this.shell(['settings', 'get', 'global', 'wifi_on']);
return (parseInt(stdout, 10) !== 0);
};
/*
* on: true (to turn on) or false (to turn off)
*/
methods.setWifiState = async function (on) {
await this.shell(['settings', 'put', 'global', 'wifi_on', on ? 1 : 0]);
};
methods.isDataOn = async function () {
let stdout = await this.shell(['settings', 'get', 'global', 'mobile_data']);
return (parseInt(stdout, 10) !== 0);
};
/*
* on: true (to turn on) or false (to turn off)
*/
methods.setDataState = async function (on) {
await this.shell(['settings', 'put', 'global', 'mobile_data', on ? 1 : 0]);
};
/*
* opts: { wifi: true/false, data true/false } (true to turn on, false to turn off)
*/
methods.setWifiAndData = async function ({wifi, data}) {
let wifiOpts = [],
dataOpts = [];
if (!_.isUndefined(wifi)) {
wifiOpts = ['-e', 'wifi', (wifi ? 'on' : 'off')];
}
if (!_.isUndefined(data)) {
dataOpts = ['-e', 'data', (data ? 'on' : 'off')];
}
let opts = ['am', 'start', '-n', 'io.appium.settings/.Settings'];
await this.shell(opts.concat(wifiOpts, dataOpts));
};
methods.rimraf = async function (path) {
await this.shell(['rm', '-rf', path]);
};
methods.push = async function (localPath, remotePath, opts) {
await this.adbExec(['push', localPath, remotePath], opts);
};
methods.pull = async function (remotePath, localPath) {
// pull folder can take more time, increasing time out to 60 secs
await this.adbExec(['pull', remotePath, localPath], {timeout: 60000});
};
methods.processExists = async function (processName) {
try {
if (!this.isValidClass(processName)) {
throw new Error(`Invalid process name: ${processName}`);
}
let stdout = await this.shell("ps");
for (let line of stdout.split(/\r?\n/)) {
line = line.trim().split(/\s+/);
let pkgColumn = line[line.length - 1];
if (pkgColumn && pkgColumn.indexOf(processName) !== -1) {
return true;
}
}
return false;
} catch (e) {
log.errorAndThrow(`Error finding if process exists. Original error: ${e.message}`);
}
};
methods.forwardPort = async function (systemPort, devicePort) {
log.debug(`Forwarding system: ${systemPort} to device: ${devicePort}`);
await this.adbExec(['forward', `tcp:${systemPort}`, `tcp:${devicePort}`]);
};
methods.removePortForward = async function (systemPort) {
log.debug(`Removing forwarded port socket connection: ${systemPort} `);
await this.adbExec(['forward', `--remove`, `tcp:${systemPort}`]);
};
methods.forwardAbstractPort = async function (systemPort, devicePort) {
log.debug(`Forwarding system: ${systemPort} to abstract device: ${devicePort}`);
await this.adbExec(['forward', `tcp:${systemPort}`, `localabstract:${devicePort}`]);
};
methods.ping = async function () {
let stdout = await this.shell(["echo", "ping"]);
if (stdout.indexOf("ping") === 0) {
return true;
}
throw new Error(`ADB ping failed, returned ${stdout}`);
};
methods.restart = async function () {
try {
await this.stopLogcat();
await this.restartAdb();
await this.waitForDevice(60);
await this.startLogcat();
} catch (e) {
log.errorAndThrow(`Restart failed. Orginial error: ${e.message}`);
}
};
methods.startLogcat = async function () {
if (this.logcat !== null) {
log.errorAndThrow("Trying to start logcat capture but it's already started!");
}
this.logcat = new Logcat({
adb: this.executable
, debug: false
, debugTrace: false
});
await this.logcat.startCapture();
};
methods.stopLogcat = async function () {
if (this.logcat !== null) {
await this.logcat.stopCapture();
this.logcat = null;
}
};
methods.getLogcatLogs = function () {
if (this.logcat === null) {
log.errorAndThrow("Can't get logcat logs since logcat hasn't started");
}
return this.logcat.getLogs();
};
methods.getPIDsByName = async function (name) {
log.debug(`Getting all processes with ${name}`);
try {
// ps <comm> where comm is last 15 characters of package name
if (name.length > 15) {
name = name.substr(name.length - 15);
}
let stdout = (await this.shell(["ps"])).trim();
let pids = [];
for (let line of stdout.split("\n")) {
if (line.indexOf(name) !== -1) {
let match = /[^\t ]+[\t ]+([0-9]+)/.exec(line);
if (match) {
pids.push(parseInt(match[1], 10));
} else {
throw new Error(`Could not extract PID from ps output: ${line}`);
}
}
}
return pids;
} catch (e) {
log.errorAndThrow(`Unable to get pids for ${name}. Orginial error: ${e.message}`);
}
};
methods.killProcessesByName = async function (name) {
try {
log.debug(`Attempting to kill all ${name} processes`);
let pids = await this.getPIDsByName(name);
if (pids.length < 1) {
log.info(`No ${name} process found to kill, continuing...`);
return;
}
for (let pid of pids) {
await this.killProcessByPID(pid);
}
} catch (e) {
log.errorAndThrow(`Unable to kill ${name} processes. Original error: ${e.message}`);
}
};
methods.killProcessByPID = async function (pid) {
log.debug(`Attempting to kill process ${pid}`);
return await this.shell(['kill', pid]);
};
methods.broadcastProcessEnd = async function (intent, processName) {
// start the broadcast without waiting for it to finish.
this.broadcast(intent);
// wait for the process to end
let start = Date.now();
let timeoutMs = 40000;
try {
while ((Date.now() - start) < timeoutMs) {
if (await this.processExists(processName)) {
// cool down
await sleep(400);
continue;
}
return;
}
throw new Error(`Process never died within ${timeoutMs} ms`);
} catch (e) {
log.errorAndThrow(`Unable to broadcast process end. Original error: ${e.message}`);
}
};
methods.broadcast = async function (intent) {
if (!this.isValidClass(intent)) {
log.errorAndThrow(`Invalid intent ${intent}`);
}
log.debug(`Broadcasting: ${intent}`);
await this.shell(['am', 'broadcast', '-a', intent]);
};
methods.endAndroidCoverage = async function () {
if (this.instrumentProc) {
await this.instrumentProc.stop();
}
};
methods.instrument = async function (pkg, activity, instrumentWith) {
if (activity[0] !== ".") {
pkg = "";
}
let pkgActivity = (pkg + activity).replace(/\.+/g, '.'); // Fix pkg..activity error
let stdout = await this.shell(['am', 'instrument', '-e', 'main_activity',
pkgActivity, instrumentWith]);
if (stdout.indexOf("Exception") !== -1) {
log.errorAndThrow(`Unknown exception during instrumentation. ` +
`Original error ${stdout.split("\n")[0]}`);
}
};
methods.androidCoverage = async function (instrumentClass, waitPkg, waitActivity) {
if (!this.isValidClass(instrumentClass)) {
log.errorAndThrow(`Invalid class ${instrumentClass}`);
}
return new Promise(async (resolve, reject) => {
let args = this.executable.defaultArgs
.concat(['shell', 'am', 'instrument', '-e', 'coverage', 'true', '-w'])
.concat([instrumentClass]);
log.debug(`Collecting coverage data with: ${[this.executable.path].concat(args).join(' ')}`);
try {
// am instrument runs for the life of the app process.
this.instrumentProc = new SubProcess(this.executable.path, args);
await this.instrumentProc.start(0);
this.instrumentProc.on('output', (stdout, stderr) => {
if (stderr) {
reject(new Error(`Failed to run instrumentation. Original error: ${stderr}`));
}
});
await this.waitForActivity(waitPkg, waitActivity);
resolve();
} catch (e) {
reject(new Error(`Android coverage failed. Original error: ${e.message}`));
}
});
};
methods.getDeviceProperty = async function (property) {
let stdout = await this.shell(['getprop', property]);
let val = stdout.trim();
log.debug(`Current device property '${property}': ${val}`);
return val;
};
methods.setDeviceProperty = async function (prop, val) {
log.debug(`Setting device property '${prop}' to '${val}'`);
await this.shell(['setprop', prop, val]);
};
methods.getDeviceSysLanguage = async function () {
return await this.getDeviceProperty("persist.sys.language");
};
methods.setDeviceSysLanguage = async function (language) {
return await this.setDeviceProperty("persist.sys.language", language.toLowerCase());
};
methods.getDeviceSysCountry = async function () {
return await this.getDeviceProperty("persist.sys.country");
};
methods.setDeviceSysCountry = async function (country) {
return await this.setDeviceProperty("persist.sys.country", country.toUpperCase());
};
methods.getDeviceSysLocale = async function () {
return await this.getDeviceProperty("persist.sys.locale");
};
methods.setDeviceSysLocale = async function (locale) {
return await this.setDeviceProperty("persist.sys.locale", locale);
};
methods.getDeviceProductLanguage = async function () {
return await this.getDeviceProperty("ro.product.locale.language");
};
methods.getDeviceProductCountry = async function () {
return await this.getDeviceProperty("ro.product.locale.region");
};
methods.getDeviceProductLocale = async function () {
return await this.getDeviceProperty("ro.product.locale");
};
export default methods;