Files
appium/android/adb.js
T
bootstraponline beb2202be7 Android packages may have underscores
var str = "mFocusedApp=AppWindowToken{41ac2030 token=Token{41797308
ActivityRecord{419b9290 u0 com.example.webviewapp_18_api/.MainActivity}}}";

// undefined
/mFocusedApp.+ ([a-zA-Z0-9\.]+)\/(\.?[^\}]+)\}/.exec(str)
2013-09-13 08:34:42 -04:00

1592 lines
51 KiB
JavaScript

"use strict";
var spawn = require('win-spawn')
, exec = require('child_process').exec
, path = require('path')
, fs = require('fs')
, net = require('net')
, logger = require('../logger').get('appium')
, status = require('../app/uiauto/lib/status')
, unzipFile = require('../app/helpers').unzipFile
, testZipArchive = require('../app/helpers').testZipArchive
, async = require('async')
, ncp = require('ncp')
, mkdirp = require('mkdirp')
, _ = require('underscore')
, helpers = require('../app/helpers')
, AdmZip = require('adm-zip')
, getTempPath = helpers.getTempPath
, rimraf = require('rimraf')
, Logcat = require('./logcat')
, isWindows = helpers.isWindows();
var noop = function() {};
var ADB = function(opts, android) {
if (!opts) {
opts = {};
}
if (typeof opts.sdkRoot === "undefined") {
opts.sdkRoot = process.env.ANDROID_HOME || '';
}
this.compressXml = opts.compressXml;
this.sdkRoot = opts.sdkRoot;
this.udid = opts.udid;
this.webSocket = opts.webSocket;
// Don't uninstall if using fast reset.
// Uninstall if reset is set and fast reset isn't.
this.skipUninstall = opts.fastReset || !opts.reset || false;
this.fastReset = opts.fastReset;
this.cleanApp = opts.cleanApp || this.fastReset;
this.systemPort = opts.systemPort || 4724;
this.internalDevicePort = opts.devicePort || 4724;
this.avdName = opts.avdName;
this.appPackage = opts.appPackage;
this.appActivity = opts.appActivity;
this.appWaitActivity = opts.appWaitActivity;
this.appDeviceReadyTimeout = opts.appDeviceReadyTimeout;
this.apkPath = opts.apkPath;
this.useKeystore = opts.useKeystore;
this.keystorePath = opts.keystorePath;
this.keystorePassword = opts.keystorePassword;
this.keyAlias = opts.keyAlias;
this.keyPassword = opts.keyPassword;
this.adb = "adb";
this.adbCmd = this.adb;
this.curDeviceId = null;
this.socketClient = null;
this.proc = null;
this.onSocketReady = noop;
this.onExit = noop;
this.alreadyExited = false;
this.portForwarded = false;
this.emulatorPort = null;
this.debugMode = true;
this.logcat = null;
this.cleanAPK = path.resolve(helpers.getTempPath(), this.appPackage + '.clean.apk');
// This is set to true when the bootstrap jar crashes.
this.restartBootstrap = false;
// The android ref is used to resend the command that
// detected the crash.
this.android = android;
this.cmdCb = null;
this.binaries = {};
this.resendLastCommand = function() {};
};
ADB.prototype.checkSdkBinaryPresent = function(binary, cb) {
logger.info("Checking whether " + binary + " is present");
var binaryLoc = null;
var binaryName = binary;
if (isWindows) {
if (binaryName === "android") {
binaryName += ".bat";
} else {
if (binaryName.indexOf(".exe", binaryName.length - 4) === -1) {
binaryName += ".exe";
}
}
}
if (this.sdkRoot) {
var binaryLocs = [ path.resolve(this.sdkRoot, "platform-tools", binaryName)
, path.resolve(this.sdkRoot, "tools", binaryName)
, path.resolve(this.sdkRoot, "build-tools", "17.0.0", binaryName)
, path.resolve(this.sdkRoot, "build-tools", "android-4.2.2", binaryName)
, path.resolve(this.sdkRoot, "build-tools", "18.0.1", binaryName)
, path.resolve(this.sdkRoot, "build-tools", "android-4.3", binaryName)];
_.each(binaryLocs, function(loc) {
if (fs.existsSync(loc)) binaryLoc = loc;
});
if (binaryLoc === null) {
cb(new Error("Could not find " + binary + " in tools, platform-tools, or build-tools under \"" + this.sdkRoot + "\"; " +
"do you have android SDK installed into this location?"),
null);
return;
}
this.debug("Using " + binary + " from " + binaryLoc);
binaryLoc = '"' + binaryLoc.trim() + '"';
this.binaries[binary] = binaryLoc;
cb(null, binaryLoc);
} else {
exec("which " + binary, { maxBuffer: 524288 }, function(err, stdout) {
if (stdout) {
this.debug("Using " + binary + " from " + stdout);
cb(null, stdout);
} else {
cb(new Error("Could not find " + binary + "; do you have the Android " +
"SDK installed and the tools + platform-tools folders " +
"added to your PATH?"),
null);
}
}.bind(this));
}
};
ADB.prototype.checkAdbPresent = function(cb) {
this.checkSdkBinaryPresent("adb", function(err, binaryLoc) {
if (err) return cb(err);
this.adb = binaryLoc.trim();
cb(null);
}.bind(this));
};
ADB.prototype.checkAppPresent = function(cb) {
if (this.apkPath === null) {
logger.info("Not checking whether app is present since we are assuming " +
"it's already on the device");
cb(null);
} else {
logger.info("Checking whether app is actually present");
fs.stat(this.apkPath, function(err) {
if (err) {
logger.error("Could not find app apk at " + this.apkPath);
cb(new Error("Error locating the app apk, supposedly it's at " +
this.apkPath + " but we can't stat it. Filesystem error " +
"is " + err));
} else {
cb(null);
}
}.bind(this));
}
};
// Fast reset
ADB.prototype.buildFastReset = function(skipAppSign, cb) {
logger.info("Building fast reset");
// Create manifest
var targetAPK = this.apkPath
, cleanAPKSrc = path.resolve(__dirname, '..', 'app', 'android', 'Clean.apk')
, newPackage = this.appPackage + '.clean'
, srcManifest = path.resolve(__dirname, '..', 'app', 'android',
'AndroidManifest.xml.src')
, dstManifest = path.resolve(getTempPath(), 'AndroidManifest.xml');
fs.writeFileSync(dstManifest, fs.readFileSync(srcManifest, "utf8"), "utf8");
var resignApks = function(cb) {
// Resign clean apk and target apk
var apks = [ this.cleanAPK ];
if (!skipAppSign) {
logger.debug("Signing app and clean apk.");
apks.push(targetAPK);
} else {
logger.debug("Skip app sign. Sign clean apk.");
}
this.sign(apks, cb);
}.bind(this);
async.series([
function(cb) { this.checkSdkBinaryPresent("aapt", cb); }.bind(this),
function(cb) { this.compileManifest(dstManifest, newPackage, this.appPackage, cb); }.bind(this),
function(cb) { this.insertManifest(dstManifest, cleanAPKSrc, this.cleanAPK, cb); }.bind(this),
function(cb) { resignApks(cb); }.bind(this)
], cb);
};
ADB.prototype.insertSelendroidManifest = function(serverPath, cb) {
logger.info("Inserting selendroid manifest");
var newServerPath = this.selendroidServerPath
, newPackage = this.appPackage + '.selendroid'
, srcManifest = path.resolve(__dirname, '..', 'build', 'selendroid',
'AndroidManifest.xml')
, dstDir = path.resolve(getTempPath(), this.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.checkSdkBinaryPresent("aapt", cb); }.bind(this),
function(cb) { this.compileManifest(dstManifest, newPackage, this.appPackage, cb); }.bind(this),
function(cb) { this.insertManifest(dstManifest, serverPath, newServerPath,
cb); }.bind(this)
], cb);
};
ADB.prototype.compileManifest = function(manifest, manifestPackage, targetPackage, cb) {
logger.info("Compiling manifest " + manifest);
var platform = helpers.getAndroidPlatform();
if (!platform || !platform[1]) {
return cb(new Error("Required platform doesn't exist (API level >= 17)"));
}
// Compile manifest into manifest.xml.apk
var compileManifest = [this.binaries.aapt + ' package -M "', manifest + '"',
' --rename-manifest-package "', manifestPackage + '"',
' --rename-instrumentation-target-package "', targetPackage + '"',
' -I "', path.resolve(platform[1], 'android.jar') +'" -F "',
manifest, '.apk" -f'].join('');
logger.debug(compileManifest);
exec(compileManifest, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (err) {
logger.debug(stderr);
return cb("error compiling manifest");
}
logger.debug("Compiled manifest");
cb(null);
});
};
ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) {
logger.info("Inserting manifest, src: " + srcApk + ", dst: " + dstApk);
var extractManifest = function(cb) {
logger.debug("Extracting manifest");
// Extract compiled manifest from manifest.xml.apk
unzipFile(manifest + '.apk', function(err, stderr) {
if (err) {
logger.info("Error unzipping manifest apk, here's stderr:");
logger.debug(stderr);
return cb(err);
}
cb(null);
});
};
var createTmpApk = function(cb) {
logger.debug("Writing tmp apk. " + srcApk + ' to ' + dstApk);
ncp(srcApk, dstApk, cb);
};
var testDstApk = function(cb) {
logger.debug("Testing new tmp apk.");
testZipArchive(dstApk, cb);
};
var moveManifest = function(cb) {
if (isWindows) {
var java = path.resolve(process.env.JAVA_HOME, 'bin', 'java');
java = isWindows ? '"' + java + '.exe"' : '"' + java + '"';
var moveManifestCmd = '"' + path.resolve(__dirname, '..', 'app', 'android', 'move_manifest.jar') + '"';
moveManifestCmd = [java, '-jar', moveManifestCmd, '"' + dstApk + '"', '"' + manifest + '"'].join(' ');
logger.debug("Moving manifest with: " + moveManifestCmd);
exec(moveManifestCmd, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.info("Got error moving manifest: " + err);
return cb(err);
}
logger.debug("Inserted manifest.");
cb(null);
});
} else {
// Insert compiled manifest into /tmp/appPackage.clean.apk
// -j = keep only the file, not the dirs
// -m = move manifest into target apk.
var replaceCmd = 'zip -j -m "' + dstApk + '" "' + manifest + '"';
logger.debug("Moving manifest with: " + replaceCmd);
exec(replaceCmd, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.info("Got error moving manifest: " + err);
return cb(err);
}
logger.debug("Inserted manifest.");
cb(null);
});
}
};
async.series([
function(cb) { extractManifest(cb); },
function(cb) { createTmpApk(cb); },
function(cb) { testDstApk(cb); },
function(cb) { moveManifest(cb); }
], cb);
};
// apks is an array of strings.
ADB.prototype.signDefault = function(apks, cb) {
var signPath = path.resolve(__dirname, '..', 'app', 'android', 'sign.jar');
var resign = 'java -jar "' + signPath + '" "' + apks.join('" "') + '" --override';
logger.debug("Resigning apks with: " + resign);
exec(resign, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (stderr.indexOf("Input is not an existing file") !== -1) {
logger.warn("Could not resign apk(s), got non-existing file error");
return cb(new Error("Could not sign one or more apks. Are you sure " +
"the file paths are correct: " +
JSON.stringify(apks)));
}
cb(err);
});
};
// apk is a single apk path
ADB.prototype.signCustom = function(apk, cb) {
var jarsigner = path.resolve(process.env.JAVA_HOME, 'bin', 'jarsigner');
jarsigner = isWindows ? '"' + jarsigner + '.exe"' : '"' + jarsigner + '"';
var java = path.resolve(process.env.JAVA_HOME, 'bin', 'java');
java = isWindows ? '"' + java + '.exe"' : '"' + java + '"';
var unsign = '"' + path.resolve(__dirname, '..', 'app', 'android', 'unsign.jar') + '"';
unsign = [java, '-jar', unsign, '"' + apk + '"'].join(' ');
// "jarsigner" "blank.apk" -sigalg MD5withRSA -digestalg SHA1
// -keystore "./key.keystore" -storepass "android"
// -keypass "android" "androiddebugkey"
if (!fs.existsSync(this.keystorePath)) {
return cb(new Error("Keystore doesn't exist. " + this.keystorePath));
}
var sign = [jarsigner, '"' + apk + '"', '-sigalg MD5withRSA', '-digestalg SHA1',
'-keystore "' + this.keystorePath + '"', '-storepass "' + this.keystorePassword + '"',
'-keypass "' + this.keyPassword + '"', '"' + this.keyAlias + '"'].join(' ');
logger.debug("Unsigning apk with: " + unsign);
exec(unsign, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (stderr) {
logger.warn(stderr);
return cb(new Error("Could not unsign apk. Are you sure " +
"the file path is correct: " +
JSON.stringify(apk)));
}
logger.debug("Signing apk with: " + sign);
exec(sign, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (stderr) {
logger.warn(stderr);
return cb(new Error("Could not sign apk. Are you sure " +
"the file path is correct: " +
JSON.stringify(apk)));
}
cb(err);
});
});
};
// apks is an array of strings.
ADB.prototype.sign = function(apks, cb) {
if (this.useKeystore) {
async.each(apks, this.signCustom.bind(this), function(err) {
cb(err);
});
} else {
this.signDefault(apks, cb);
}
};
// returns true when already signed, false otherwise.
ADB.prototype.checkApkCert = function(apk, cb) {
if (!fs.existsSync(apk)) {
logger.debug("APK doesn't exist. " + apk);
return cb(false);
}
if (this.useKeystore) {
var h = "a-fA-F0-9";
var md5Str = ['.*MD5.*((?:[', h, ']{2}:){15}[', h, ']{2})'].join('');
var md5 = new RegExp(md5Str, 'mi');
var keytool = path.resolve(process.env.JAVA_HOME, 'bin', 'keytool');
keytool = isWindows ? '"' + keytool + '.exe"' : '"' + keytool + '"';
var keystoreHash = null;
var checkKeystoreMD5 = function(innerCb) {
logger.debug("checkKeystoreMD5");
// get keystore md5
var keystore = [keytool, '-v', '-list', '-alias "' + this.keyAlias + '"',
'-keystore "' + this.keystorePath + '"', '-storepass "' + this.keystorePassword + '"'].join(' ');
logger.debug("Printing keystore md5: " + keystore);
exec(keystore, { maxBuffer: 524288 }, function(err, stdout) {
keystoreHash = md5.exec(stdout);
keystoreHash = keystoreHash ? keystoreHash[1] : null;
logger.debug(' Keystore MD5: ' + keystoreHash);
innerCb();
});
};
var match = false;
var checkApkMD5 = function(innerCb) {
logger.debug("checkApkMD5");
var entryHash = null;
var zip = new AdmZip(apk);
var rsa = /^META-INF\/.*\.[rR][sS][aA]$/;
var entries = zip.getEntries();
var next = function() {
var entry = entries.pop(); // meta-inf tends to be at the end
if (!entry) return innerCb(); // no more entries
entry = entry.entryName;
if (!rsa.test(entry)) return next();
logger.debug("Entry: " + entry);
var entryPath = path.join(getTempPath(), this.appPackage, 'cert');
logger.debug("entryPath: " + entryPath);
var entryFile = path.join(entryPath, entry);
logger.debug("entryFile: " + entryFile);
// ensure /tmp/pkg/cert/ doesn't exist or extract will fail.
rimraf.sync(entryPath);
// META-INF/CERT.RSA
zip.extractEntryTo(entry, entryPath, true); // overwrite = true
logger.debug("extracted!");
// check for match
var md5Entry = [keytool, '-v', '-printcert', '-file', entryFile].join(' ');
logger.debug("Printing apk md5: " + md5Entry);
exec(md5Entry, { maxBuffer: 524288 }, function(err, stdout) {
entryHash = md5.exec(stdout);
entryHash = entryHash ? entryHash[1] : null;
logger.debug('entryHash MD5: ' + entryHash);
logger.debug(' keystore MD5: ' + keystoreHash);
var matchesKeystore = entryHash && entryHash === keystoreHash;
logger.debug('Matches keystore? ' + matchesKeystore);
if (matchesKeystore) {
match = true;
return innerCb();
} else {
next();
}
});
}.bind(this);
next();
};
async.series([
function(cb) { checkKeystoreMD5(cb); },
function(cb) { checkApkMD5(cb); }
], function() { logger.debug("checkApkCert match? " + match);
cb(match); });
// exit checkApkCert
return;
}
var verifyPath = path.resolve(__dirname, '..', 'app', 'android',
'verify.jar');
var resign = 'java -jar "' + verifyPath + '" "' + apk + '"';
logger.debug("Checking app cert for " + apk + ": " + resign);
exec(resign, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.debug("App not signed with debug cert.");
return cb(false);
}
logger.debug("App already signed.");
cb(true);
});
};
ADB.prototype.checkFastReset = function(cb) {
logger.info("Checking whether we need to run fast reset");
// NOP if fast reset is not true.
if (!this.fastReset) {
logger.info("User doesn't want fast reset, doing nothing");
return cb(null);
}
if (this.apkPath === null) {
logger.info("Can't run fast reset on an app that's already on the device " +
"so doing nothing");
return cb(null);
}
if (!this.appPackage) return cb(new Error("appPackage must be set."));
this.checkApkCert(this.cleanAPK, function(cleanSigned){
this.checkApkCert(this.apkPath, function(appSigned){
logger.debug("App signed? " + appSigned + " " + this.apkPath);
// Only build & resign clean.apk if it doesn't exist or isn't signed.
if (!fs.existsSync(this.cleanAPK) || !cleanSigned) {
this.buildFastReset(appSigned, function(err){ if (err) return cb(err); cb(null); });
} else {
if (!appSigned) {
// Resign app apk because it's not signed.
this.sign([this.apkPath], cb);
} else {
// App and clean are already existing and signed.
cb(null);
}
}
}.bind(this));
}.bind(this));
};
ADB.prototype.getDeviceWithRetry = function(cb, count) {
logger.info("Trying to find a connected android device");
var error = new Error("Could not find a connected Android device.");
var getDevices = function(innerCb) {
this.getConnectedDevices(function(err, devices) {
if (typeof devices === "undefined" || devices.length === 0 || err) {
return innerCb(error);
}
innerCb(null);
});
}.bind(this);
getDevices(function(err) {
count = count || 1;
if (err) {
if (count < 10) {
logger.info("Could not find devices, restarting adb server...");
this.restartAdb(function() {
this.getDeviceWithRetry(cb, count + 1);
}.bind(this));
} else {
logger.info("Looked for devices " + count + " times. Giving up");
cb(error);
}
} else {
logger.info("Found device, no need to retry");
cb(null);
}
}.bind(this));
};
ADB.prototype.prepareDevice = function(onReady) {
logger.info("Preparing device for session");
async.series([
function(cb) { this.checkAppPresent(cb); }.bind(this),
function(cb) { this.checkAdbPresent(cb); }.bind(this),
function(cb) { this.prepareEmulator(cb); }.bind(this),
function(cb) { this.getDeviceWithRetry(cb);}.bind(this),
function(cb) { this.waitForDevice(cb); }.bind(this),
function(cb) { this.startLogcat(cb); }.bind(this),
function(cb) { this.checkFastReset(cb); }.bind(this)
], onReady);
};
ADB.prototype.requestXmlCompression = function(cb) {
if (this.compressXml) {
this.android.proxy(["enableCompressedLayoutHeirarchy"], cb);
} else {
cb(null);
}
};
ADB.prototype.pushStrings = function(cb) {
var remotePath = '/data/local/tmp';
var stringsJson = 'strings.json';
if (!fs.existsSync(this.apkPath)) {
// apk doesn't exist locally so remove old strings.json
var pushCmd = this.adbCmd + ' shell rm ' + remotePath + '/' + stringsJson;
logger.debug("Apk doesn't exist. Removing old strings.json " + pushCmd);
exec(pushCmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
cb(null);
});
} else {
var stringsFromApkJarPath = path.resolve(__dirname, '..', 'app', 'android',
'strings_from_apk.jar');
var outputPath = path.resolve(getTempPath(), this.appPackage);
var makeStrings = ['java -jar "', stringsFromApkJarPath,
'" "', this.apkPath, '" "', outputPath, '"'].join('');
logger.debug(makeStrings);
exec(makeStrings, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (err) {
logger.debug(stderr);
return cb("error making strings");
}
var jsonFile = path.resolve(outputPath, stringsJson);
var pushCmd = this.adbCmd + ' push "' + jsonFile + '" ' + remotePath;
exec(pushCmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
cb(null);
});
}.bind(this));
}
};
ADB.prototype.startAppium = function(onReady, onExit) {
logger.info("Starting android appium");
this.onExit = onExit;
logger.debug("Using fast reset? " + this.fastReset);
async.series([
function(cb) { this.prepareDevice(cb); }.bind(this),
function(cb) { this.pushStrings(cb); }.bind(this),
function(cb) { this.requestXmlCompression(cb); }.bind(this),
function(cb) { this.uninstallApp(cb); }.bind(this),
function(cb) { this.installApp(cb); }.bind(this),
function(cb) { this.forwardPort(cb); }.bind(this),
function(cb) { this.pushAppium(cb); }.bind(this),
function(cb) { this.runBootstrap(cb, onExit); }.bind(this),
function(cb) { this.wakeUp(cb); }.bind(this),
function(cb) { this.unlockScreen(cb); }.bind(this),
function(cb) { this.startApp(cb); }.bind(this)
], function(err, seriesInfo) {
onReady(err);
});
};
ADB.prototype.startChrome = function(onReady) {
logger.info("Starting Chrome");
logger.debug("Using fast reset? " + this.fastReset);
async.series([
function(cb) { this.prepareDevice(cb); }.bind(this),
function(cb) { this.installApp(cb); }.bind(this),
function(cb) { this.startApp(cb); }.bind(this)
], onReady);
};
ADB.prototype.startSelendroid = function(serverPath, onReady) {
logger.info("Starting selendroid");
var modServerExists = false
, modAppPkg = this.appPackage + '.selendroid';
this.selendroidServerPath = path.resolve(getTempPath(),
'selendroid.' + this.appPackage + '.apk');
var checkModServerExists = function(cb) {
fs.stat(this.selendroidServerPath, function(err) {
modServerExists = !err;
cb();
});
}.bind(this);
var conditionalUninstallSelendroid = function(cb) {
if (!modServerExists) {
// assume we're going to rebuild selendroid and therefore
// need to uninstall it if it's already on device
logger.info("Rebuilt selendroid apk does not exist, uninstalling " +
"any instances of it on device to make way for new one");
this.uninstallApk(modAppPkg, cb);
} else {
logger.info("Rebuilt selendroid apk exists, doing nothing");
cb();
}
}.bind(this);
var conditionalInsertManifest = function(cb) {
if (!modServerExists) {
logger.info("Rebuilt selendroid server does not exist, inserting " +
"modified manifest");
this.insertSelendroidManifest(serverPath, cb);
} else {
logger.info("Rebuilt selendroid server already exists, no need to " +
"rebuild it with a new manifest");
cb();
}
}.bind(this);
var conditionalInstallSelendroid = function(cb) {
this.checkAppInstallStatus(modAppPkg, function(e, installed) {
if (!installed) {
logger.info("Rebuilt selendroid is not installed, installing it");
this.installApk(this.selendroidServerPath, cb);
} else {
logger.info("Rebuilt selendroid is already installed");
cb();
}
}.bind(this));
}.bind(this);
async.series([
function(cb) { this.prepareDevice(cb); }.bind(this),
function(cb) { checkModServerExists(cb); },
function(cb) { conditionalUninstallSelendroid(cb); },
function(cb) { conditionalInsertManifest(cb); },
function(cb) { this.checkSelendroidCerts(this.selendroidServerPath, cb); }.bind(this),
function(cb) { conditionalInstallSelendroid(cb); },
function(cb) { this.installApp(cb); }.bind(this),
function(cb) { this.forwardPort(cb); }.bind(this),
function(cb) { this.unlockScreen(cb); }.bind(this),
function(cb) { this.pushSelendroid(cb); }.bind(this),
function(cb) { logger.info("Selendroid server is launching"); cb(); }
], onReady);
};
ADB.prototype.pushSelendroid = function(cb) {
var activityString = this.appActivity;
if (activityString.indexOf(this.appPackage) === 0) {
activityString = activityString.substring(this.appPackage.length);
}
var cmd = this.adbCmd + " shell am instrument -e main_activity '" +
this.appPackage + activityString + "' " + this.appPackage +
".selendroid/io.selendroid.ServerInstrumentation";
cmd = cmd.replace(/\.+/g,'.'); // Fix pkg..activity error
logger.info("Starting instrumentation process for selendroid with cmd: " +
cmd);
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
if (err) return cb(err);
if (stdout.indexOf("Exception") !== -1) {
logger.error(stdout);
var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid";
return cb(new Error(msg));
}
cb();
});
};
ADB.prototype.checkSelendroidCerts = function(serverPath, cb) {
var alreadyReturned = false
, checks = 0;
var onDoneSigning = function() {
checks++;
if (checks === 2 && !alreadyReturned) {
cb();
}
};
// these run in parallel
var apks = [serverPath, this.apkPath];
_.each(apks, function(apk) {
logger.info("Checking signed status of " + apk);
this.checkApkCert(apk, function(isSigned) {
if (isSigned) return onDoneSigning();
this.sign([apk], function(err) {
if (err && !alreadyReturned) {
alreadyReturned = true;
return cb(err);
}
onDoneSigning();
});
}.bind(this));
}, this);
};
ADB.prototype.getEmulatorPort = function(cb) {
logger.info("Getting running emulator port");
if (this.emulatorPort !== null) {
return cb(null, this.emulatorPort);
}
this.getConnectedDevices(function(err, devices) {
if (err || devices.length < 1) {
cb(new Error("No devices connected"));
} else {
// pick first device
var port = this.getPortFromEmulatorString(devices[0]);
if (port) {
cb(null, port);
} else {
cb(new Error("Emulator port not found"));
}
}
}.bind(this));
};
ADB.prototype.getPortFromEmulatorString = function(emStr) {
var portPattern = /emulator-(\d+)/;
if (portPattern.test(emStr)) {
return parseInt(portPattern.exec(emStr)[1], 10);
}
return false;
};
ADB.prototype.getRunningAVDName = function(cb) {
logger.info("Getting running AVD name");
this.sendTelnetCommand("avd name", cb);
};
ADB.prototype.prepareEmulator = function(cb) {
if (this.avdName !== null) {
this.getRunningAVDName(function(err, runningAVDName) {
if (!err && this.avdName.replace('@','') === runningAVDName) {
logger.info("Did not launch AVD because it was already running.");
cb(null);
} else {
logger.info("Launching Emulator with AVD " + this.avdName);
var killallCmd = isWindows ? "TASKKILL /IM emulator.exe" : "/usr/bin/killall -m emulator*";
exec(killallCmd, { maxBuffer: 524288 }, function(err, stdout) {
if (err) {
logger.info("Could not kill emulator. It was probably not running. : " + err.message);
}
this.checkSdkBinaryPresent("emulator", function(err, emulatorBinaryPath) {
if (err) {
return cb(err);
}
if (this.avdName[0] !== "@") {
this.avdName = "@" + this.avdName;
}
var emulatorProc = spawn(emulatorBinaryPath.substr(1, emulatorBinaryPath.length - 2), [this.avdName]);
var timeoutMs = 120000;
var now = Date.now();
var checkEmulatorAlive = function() {
this.restartAdb(function() {
this.getConnectedDevices(function(err, devices) {
if (!err && devices.length) {
cb(null, true);
} else if (Date.now() < (now + timeoutMs)) {
setTimeout(checkEmulatorAlive, 2000);
} else {
cb(new Error("Emulator didn't come up in " + timeoutMs + "ms"));
}
}.bind(this));
}.bind(this));
}.bind(this);
checkEmulatorAlive();
}.bind(this));
}.bind(this));
}
}.bind(this));
} else {
cb();
}
};
ADB.prototype.getConnectedDevices = function(cb) {
this.debug("Getting connected devices...");
exec(this.adb + " devices", { maxBuffer: 524288 }, function(err, stdout) {
if (err) {
logger.error(err);
cb(err);
} else if (stdout.toLowerCase().indexOf("error") !== -1) {
logger.error(stdout);
cb(new Error(stdout));
} else {
var devices = [];
_.each(stdout.split("\n"), function(line) {
if (line.trim() !== "" && line.indexOf("List of devices") === -1 && line.indexOf("* daemon") === -1 && line.indexOf("offline") == -1) {
var lineInfo = line.split("\t");
devices.push({udid: lineInfo[0], state: lineInfo[1]}); // state is either "device" or "offline", afaict
}
});
this.debug(devices.length + " device(s) connected");
if (devices.length) {
this.debug("Setting device id to " + (this.udid || devices[0].udid));
this.emulatorPort = null;
var emPort = this.getPortFromEmulatorString(devices[0].udid);
this.setDeviceId(this.udid || devices[0].udid);
if (emPort && !this.udid) {
this.emulatorPort = emPort;
}
}
cb(null, devices);
}
}.bind(this));
};
ADB.prototype.forwardPort = function(cb) {
this.requireDeviceId();
this.debug("Forwarding system:" + this.systemPort + " to device:" +
this.internalDevicePort);
var arg = "tcp:" + this.systemPort + " tcp:" + this.internalDevicePort;
exec(this.adbCmd + " forward " + arg, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.error(err);
cb(err);
} else {
this.portForwarded = true;
cb(null);
}
}.bind(this));
};
ADB.prototype.runBootstrap = function(readyCb, exitCb) {
logger.info("Running bootstrap");
this.requireDeviceId();
var args = ["-s", this.curDeviceId, "shell", "uiautomator", "runtest",
"AppiumBootstrap.jar", "-c", "io.appium.android.bootstrap.Bootstrap"];
logger.info(this.adb + " " + args.join(" "));
this.proc = spawn(this.adb.substr(1, this.adb.length - 2), args);
this.onSocketReady = readyCb;
this.proc.stdout.on('data', function(data) {
this.outputStreamHandler(data);
}.bind(this));
this.proc.stderr.on('data', function(data) {
this.errorStreamHandler(data);
}.bind(this));
this.proc.on('exit', function(code) {
this.cmdCb = null;
if (this.socketClient) {
this.socketClient.end();
this.socketClient.destroy();
this.socketClient = null;
}
if (this.restartBootstrap === true) {
// The bootstrap jar has crashed so it must be restarted.
this.restartBootstrap = false;
this.runBootstrap(function() {
// Resend last command because the client is still waiting for the
// response.
this.resendLastCommand();
}.bind(this), exitCb);
return;
}
if (!this.alreadyExited) {
this.alreadyExited = true;
exitCb(code);
}
}.bind(this));
};
ADB.prototype.checkForSocketReady = function(output) {
if (/Appium Socket Server Ready/.test(output)) {
this.requirePortForwarded();
this.debug("Connecting to server on device on port " + this.systemPort + "...");
this.socketClient = net.connect(this.systemPort, function() {
this.debug("Connected!");
this.onSocketReady(null);
}.bind(this));
this.socketClient.setEncoding('utf8');
var oldData = '';
this.socketClient.on('data', function(data) {
this.debug("Received command result from bootstrap");
try {
data = JSON.parse(oldData + data);
oldData = '';
} catch (e) {
logger.info("Stream still not complete, waiting");
oldData += data;
return;
}
if (this.cmdCb) {
var next = this.cmdCb;
this.cmdCb = null;
next(data);
} else {
this.debug("Got data when we weren't expecting it, ignoring:");
this.debug(JSON.stringify(data));
}
}.bind(this));
}
};
ADB.prototype.sendAutomatorCommand = function(action, params, cb) {
if (typeof params === "function") {
cb = params;
params = {};
}
var extra = {action: action, params: params};
this.sendCommand('action', extra, cb);
};
ADB.prototype.sendCommand = function(type, extra, cb) {
if (this.cmdCb !== null) {
logger.warn("Trying to run a command when one is already in progress. " +
"Will spin a bit and try again");
var start = Date.now();
var timeoutMs = 10000;
var intMs = 200;
var waitForCmdCbNull = function() {
if (this.cmdCb === null) {
this.sendCommand(type, extra, cb);
} else if ((Date.now() - start) < timeoutMs) {
setTimeout(waitForCmdCbNull, intMs);
} else {
cb(new Error("Never became able to push strings since a command " +
"was in process"));
}
}.bind(this);
waitForCmdCbNull();
} else if (this.socketClient) {
this.resendLastCommand = function() {
this.sendCommand(type, extra, cb);
}.bind(this);
if (typeof extra === "undefined" || extra === null) {
extra = {};
}
var cmd = {cmd: type};
cmd = _.extend(cmd, extra);
var cmdJson = JSON.stringify(cmd) + "\n";
this.cmdCb = cb;
var logCmd = cmdJson.trim();
if (logCmd.length > 1000) {
logCmd = logCmd.substr(0, 1000) + "...";
}
this.debug("Sending command to android: " + logCmd);
this.socketClient.write(cmdJson);
} else {
cb({
status: status.codes.UnknownError.code
, value: "Tried to send command to non-existent Android socket, " +
"maybe it's shutting down?"
});
}
};
ADB.prototype.sendShutdownCommand = function(cb) {
setTimeout(function() {
if (!this.alreadyExited) {
logger.warn("Android did not shut down fast enough, calling it gone");
this.alreadyExited = true;
this.onExit(1);
}
}.bind(this), 7000);
this.sendCommand('shutdown', null, cb);
if (this.logcat !== null) {
this.logcat.stopCapture();
}
};
ADB.prototype.outputStreamHandler = function(output) {
this.checkForSocketReady(output);
this.handleBootstrapOutput(output);
};
ADB.prototype.handleBootstrapOutput = function(output) {
// for now, assume all intentional logging takes place on one line
// and that we don't get half-lines from the stream.
// probably bad assumptions
output = output.toString().trim();
var lines = output.split("\n");
var re = /^\[APPIUM-UIAUTO\] (.+)\[\/APPIUM-UIAUTO\]$/;
var match;
_.each(lines, function(line) {
line = line.trim();
if (line !== '') {
match = re.exec(line);
if (match) {
logger.info("[ANDROID] " + match[1]);
var alertRe = /Emitting system alert message/;
if (alertRe.test(line)) {
logger.info("Emiting alert message...");
this.webSocket.sockets.emit('alert', {message: line});
}
} else {
// The dump command will always disconnect UiAutomation.
// Detect the crash then restart UiAutomation.
if (line.indexOf("UiAutomationService not connected") !== -1) {
this.restartBootstrap = true;
}
logger.info(("[ADB STDOUT] " + line).grey);
}
}
}, this);
};
ADB.prototype.errorStreamHandler = function(output) {
var lines = output.split("\n");
_.each(lines, function(line) {
logger.info(("[ADB STDERR] " + line).yellow);
});
};
ADB.prototype.debug = function(msg) {
if (this.debugMode) {
logger.info("[ADB] " + msg);
}
};
ADB.prototype.isDeviceConnected = function(cb) {
this.getConnectedDevices(function(err, devices) {
if (err) {
cb(err);
} else {
cb(null, devices.length > 0);
}
});
};
ADB.prototype.setDeviceId = function(deviceId) {
this.curDeviceId = deviceId;
this.adbCmd = this.adb + " -s " + deviceId;
};
ADB.prototype.requireDeviceId = function() {
if (!this.curDeviceId) {
throw new Error("This method requires that a device ID is set. " +
"Call getConnectedDevices or setDeviceId");
}
};
ADB.prototype.requirePortForwarded = function() {
if (!this.portForwarded) {
throw new Error("This method requires the port be forwarded on the " +
"device. Make sure to call forwardPort()!");
}
};
ADB.prototype.requireApp = function() {
if (!this.appPackage || !this.appActivity) {
throw new Error("This method requires that appPackage and appActivity " +
"be sent in with options");
}
};
ADB.prototype.requireApk = function() {
if (!this.apkPath) {
throw new Error("This method requires that apkPath be sent in as option");
}
};
ADB.prototype.waitForDevice = function(cb) {
var doWait = function(innerCb) {
this.debug("Waiting for device " + this.curDeviceId + " to be ready " +
"and to respond to shell commands (timeout = " +
this.appDeviceReadyTimeout + ")");
var movedOn = false
, cmd = this.adbCmd + " wait-for-device"
, timeoutSecs = parseInt(this.appDeviceReadyTimeout, 10);
setTimeout(function() {
if (!movedOn) {
movedOn = true;
innerCb("Device did not become ready in " + timeoutSecs + " secs; " +
"are you sure it's powered on?");
}
}.bind(this), timeoutSecs * 1000);
exec(cmd, { maxBuffer: 524288 }, function(err) {
if (!movedOn) {
if (err) {
logger.error("Error running wait-for-device");
movedOn = true;
innerCb(err);
} else {
exec(this.adbCmd + " shell echo 'ready'", function(err) {
if (!movedOn) {
movedOn = true;
if (err) {
logger.error("Error running shell echo: " + err);
innerCb(err);
} else {
innerCb(null);
}
}
}.bind(this));
}
}
}.bind(this));
}.bind(this);
doWait(function(err) {
if (err) {
this.restartAdb(function() {
this.getConnectedDevices(function() {
doWait(cb);
});
}.bind(this));
} else {
cb(null);
}
}.bind(this));
};
ADB.prototype.restartAdb = function(cb) {
logger.info("Killing ADB server so it will come back online");
var cmd = this.adb + " kill-server";
exec(cmd, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.error("Error killing ADB server, going to see if it's online " +
"anyway");
}
cb();
});
};
ADB.prototype.startLogcat = function(cb) {
if (this.logcat !== null) {
cb(new Error("Trying to start logcat capture but it's already started!"));
return;
}
var adb = this.adb.replace(/"/g, '');
this.logcat = new Logcat({
adbCmd: adb
, debug: false
, debugTrace: false
});
this.logcat.startCapture(cb);
};
ADB.prototype.getLogcatLogs = function() {
if (this.logcat === null) {
throw new Error("Can't get logcat logs since logcat hasn't started");
}
return this.logcat.getLogs();
};
ADB.prototype.pushAppium = function(cb) {
this.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 {
var remotePath = "/data/local/tmp";
var cmd = this.adbCmd + ' push "' + binPath + '" ' + remotePath;
exec(cmd, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.error(err);
cb(err);
} else {
cb(null);
}
}.bind(this));
}
}.bind(this));
};
ADB.prototype.startApp = function(cb) {
logger.info("Starting app");
this.requireDeviceId();
this.requireApp();
var activityString = this.appActivity;
var cmd = this.adbCmd + " shell am start -n " + this.appPackage + "/" +
activityString;
this.debug("Starting app\n" + cmd);
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
if(err) {
logger.error(err);
cb(err);
} else {
if (stdout.indexOf("Error: Activity class") !== -1 &&
stdout.indexOf("does not exist") !== -1) {
if (this.appActivity[0] !== ".") {
logger.info("We tried to start an activity that doesn't exist, " +
"retrying with . prepended to activity");
this.appActivity = "." + this.appActivity;
return this.startApp(cb);
} else {
var msg = "Activity used to start app doesn't exist! Make sure " +
"it exists";
logger.error(msg);
return cb(new Error(msg));
}
}
this.waitForActivity(cb);
}
}.bind(this));
};
ADB.prototype.stopApp = function(cb) {
logger.info("Killing app");
this.requireDeviceId();
this.requireApp();
var cmd = this.adbCmd + " shell am force-stop " + this.appPackage;
exec(cmd, { maxBuffer: 524288 }, function(err) {
if (err) {
logger.error(err);
return cb(err);
}
cb();
});
};
ADB.prototype.getFocusedPackageAndActivity = function(cb) {
logger.info("Getting focused package and activity");
this.requireDeviceId();
var cmd = this.adbCmd + " shell dumpsys window windows"
, searchRe = new RegExp(/mFocusedApp.+ ([a-zA-Z0-9\._]+)\/(\.?[^\}]+)\}/);
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
if (err) {
logger.error(err);
cb(err);
} else {
var foundMatch = false;
_.each(stdout.split("\n"), function(line) {
var match = searchRe.exec(line);
if (match) {
foundMatch = match;
}
});
if (foundMatch) {
cb(null, foundMatch[1].trim(), foundMatch[2].trim());
} else {
cb(new Error("Could not parse activity from dumpsys"));
}
}
}.bind(this));
};
ADB.prototype.waitForNotActivity = function(cb) {
this.requireApp();
logger.info("Waiting for app's activity to not be focused");
var waitMs = 20000
, intMs = 750
, endAt = Date.now() + waitMs
, targetActivity = this.appWaitActivity || this.appActivity;
if (targetActivity.indexOf(this.appPackage) === 0) {
targetActivity = targetActivity.substring(this.appPackage.length);
}
var getFocusedApp = function() {
this.getFocusedPackageAndActivity(function(err, foundPackage,
foundActivity) {
var notFoundAct = true;
_.each(targetActivity.split(','), function(act) {
act = act.trim();
if (act === foundActivity || "." + act === foundActivity) {
notFoundAct = false;
}
});
if (foundPackage !== this.appPackage && notFoundAct) {
cb(null);
} else if (Date.now() < endAt) {
if (err) logger.info(err);
setTimeout(getFocusedApp, intMs);
} else {
var msg = "App never closed. appActivity: " +
foundActivity + " != " + targetActivity;
logger.error(msg);
cb(new Error(msg));
}
}.bind(this));
}.bind(this);
getFocusedApp();
};
ADB.prototype.waitForActivity = function(cb, waitMsOverride) {
this.requireApp();
logger.info("Waiting for app's activity to become focused");
var waitMs = waitMsOverride || 20000
, intMs = 750
, endAt = Date.now() + waitMs
, targetActivity = this.appWaitActivity || this.appActivity;
if (targetActivity.indexOf(this.appPackage) === 0) {
targetActivity = targetActivity.substring(this.appPackage.length);
}
var getFocusedApp = function() {
this.getFocusedPackageAndActivity(function(err, foundPackage,
foundActivity) {
var foundAct = false;
_.each(targetActivity.split(','), function(act) {
act = act.trim();
if (act === foundActivity || "." + act === foundActivity) {
foundAct = true;
}
});
if (foundPackage === this.appPackage && foundAct) {
cb(null);
} else if (Date.now() < endAt) {
if (err) logger.info(err);
setTimeout(getFocusedApp, intMs);
} else {
var msg = "App never showed up as active. appActivity: " +
foundActivity + " != " + targetActivity;
logger.error(msg);
cb(new Error(msg));
}
}.bind(this));
}.bind(this);
getFocusedApp();
};
ADB.prototype.uninstallApk = function(pkg, cb) {
logger.info("Uninstalling " + pkg);
var cmd = this.adbCmd + " uninstall " + pkg;
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
if (err) {
logger.error(err);
cb(err);
} else {
stdout = stdout.trim();
if (stdout === "Success") {
logger.debug("App was uninstalled");
} else {
logger.debug("App was not uninstalled, maybe it wasn't on device?");
}
cb(null);
}
});
};
ADB.prototype.installApk = function(apk, cb) {
var cmd = this.adbCmd + ' install -r "' + apk + '"';
logger.info("Installing " + apk);
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
if (err) {
logger.error(err);
cb(err);
} else {
// Useful for debugging.
logger.debug(stdout);
cb(null);
}
});
};
ADB.prototype.uninstallApp = function(cb) {
var next = function() {
this.requireDeviceId();
this.requireApp();
this.debug("Uninstalling app " + this.appPackage);
this.uninstallApk(this.appPackage, function(err) {
if (this.fastReset) {
var cleanPkg = this.appPackage + '.clean';
this.debug("Uninstalling app " + cleanPkg);
this.uninstallApk(cleanPkg, function(err) {
if (err) return cb(err);
cb(null);
});
} else {
if (err) return cb(err);
cb(null);
}
}.bind(this));
}.bind(this);
if (this.skipUninstall) {
this.debug("Not uninstalling app since server started with --full-reset " +
"or --no-reset");
cb();
} else {
next();
}
};
ADB.prototype.runFastReset = function(cb) {
// list instruments with: adb shell pm list instrumentation
// targetPackage + '.clean' / clean.apk.Clear
var clearCmd = this.adbCmd + ' shell am instrument ' + this.appPackage + '.clean/clean.apk.Clean';
logger.debug("Running fast reset clean: " + clearCmd);
exec(clearCmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
if (err) {
logger.warn(stderr);
cb(err);
} else {
cb(null);
}
});
};
ADB.prototype.checkAppInstallStatus = function(pkg, cb) {
var installed = false
, cleanInstalled = false;
this.requireDeviceId();
logger.debug("Getting install/clean status for " + pkg);
var listPkgCmd = this.adbCmd + " shell pm list packages -3 " + pkg;
exec(listPkgCmd, { maxBuffer: 524288 }, function(err, stdout) {
var apkInstalledRgx = new RegExp('^package:' +
pkg.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
installed = apkInstalledRgx.test(stdout);
var cleanInstalledRgx = new RegExp('^package:' +
(pkg + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
cleanInstalled = cleanInstalledRgx.test(stdout);
cb(null, installed, cleanInstalled);
});
};
ADB.prototype.installApp = function(cb) {
var installApp = false
, installClean = false;
this.requireDeviceId();
if (this.apkPath === null) {
logger.info("Not installing app since we launched with a package instead " +
"of an app path");
return cb(null);
}
this.requireApk();
var determineInstallAndCleanStatus = function(cb) {
logger.info("Determining app install/clean status");
this.checkAppInstallStatus(this.appPackage, function(err, installed, cleaned) {
installApp = !installed;
installClean = !cleaned;
cb();
});
}.bind(this);
var doInstall = function(cb) {
if (installApp) {
this.debug("Installing app apk");
this.installApk(this.apkPath, cb);
} else { cb(null); }
}.bind(this);
var doClean = function(cb) {
if (installClean && this.cleanApp) {
this.debug("Installing clean apk");
this.installApk(this.cleanAPK, cb);
} else { cb(null); }
}.bind(this);
var doFastReset = function(cb) {
// App is already installed so reset it.
if (!installApp && this.fastReset) {
this.runFastReset(cb);
} else { cb(null); }
}.bind(this);
async.series([
function(cb) { determineInstallAndCleanStatus(cb); },
function(cb) { doInstall(cb); },
function(cb) { doClean(cb); },
function(cb) { doFastReset(cb); }
], cb);
};
ADB.prototype.back = function(cb) {
this.requireDeviceId();
this.debug("Pressing the BACK button");
var cmd = this.adbCmd + " shell input keyevent 4";
exec(cmd, { maxBuffer: 524288 }, function() {
cb();
});
};
ADB.prototype.goToHome = function(cb) {
this.requireDeviceId();
this.debug("Pressing the HOME button");
var cmd = this.adbCmd + " shell input keyevent 3";
exec(cmd, { maxBuffer: 524288 }, function() {
cb();
});
};
ADB.prototype.wakeUp = function(cb) {
// requires an appium bootstrap connection loaded
this.debug("Waking up device if it's not alive");
this.android.proxy(["wake", {}], cb);
};
ADB.prototype.keyevent = function(keycode, cb) {
this.requireDeviceId();
var code = parseInt(keycode, 10);
// keycode must be an int.
var cmd = this.adbCmd + ' shell input keyevent ' + code;
this.debug("Sending keyevent " + code);
exec(cmd, { maxBuffer: 524288 }, function() {
cb();
});
};
ADB.prototype.unlockScreen = function(cb) {
this.requireDeviceId();
this.debug("Attempting to unlock screen");
var cmd = this.adbCmd + " shell input keyevent 82";
exec(cmd, { maxBuffer: 524288 }, function() {
cb();
});
};
ADB.prototype.sendTelnetCommand = function(command, cb) {
logger.info("Sending telnet command to device: " + command);
this.getEmulatorPort(function(err, port) {
if (err) return cb(err);
var conn = net.createConnection(port, 'localhost');
var connected = false;
var readyRegex = /^OK$/m;
var dataStream = "";
var res = null;
var onReady = function() {
logger.info("Socket connection to device ready");
conn.write(command + "\n");
};
conn.on('connect', function() {
logger.info("Socket connection to device created");
});
conn.on('data', function(data) {
data = data.toString('utf8');
if (!connected) {
if (readyRegex.test(data)) {
connected = true;
onReady();
}
} else {
dataStream += data;
if (readyRegex.test(data)) {
res = dataStream.replace(readyRegex, "").trim();
logger.info("Telnet command got response: " + res);
conn.write("quit\n");
}
}
});
conn.on('close', function() {
if (res === null) {
cb(new Error("Never got a response from command"));
} else {
cb(null, res);
}
});
});
};
module.exports = function(opts, android) {
return new ADB(opts, android);
};