diff --git a/lib/devices/android/adb.js b/lib/devices/android/adb.js index 27b58fcbb..748ba3b99 100644 --- a/lib/devices/android/adb.js +++ b/lib/devices/android/adb.js @@ -18,8 +18,7 @@ var spawn = require('win-spawn') , rimraf = require('rimraf') , Logcat = require('./logcat.js') , isWindows = helpers.isWindows() - , helperJarPath = path.resolve(__dirname, 'helpers') - , deviceState = require('./device-state.js'); + , helperJarPath = path.resolve(__dirname, 'helpers'); var ADB = function(opts) { @@ -53,7 +52,6 @@ ADB.prototype.debug = function(msg) { } }; - ADB.prototype.checkSdkBinaryPresent = function(binary, cb) { logger.info("Checking whether " + binary + " is present"); var binaryLoc = null; @@ -79,9 +77,9 @@ ADB.prototype.checkSdkBinaryPresent = function(binary, cb) { }); 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); + 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?")); return; } this.debug("Using " + binary + " from " + binaryLoc); @@ -96,8 +94,7 @@ ADB.prototype.checkSdkBinaryPresent = function(binary, cb) { } 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); + "added to your PATH?")); } }.bind(this)); } @@ -160,7 +157,7 @@ ADB.prototype.compileManifest = function(manifest, manifestPackage, targetPackag return cb("error compiling manifest"); } logger.debug("Compiled manifest"); - cb(null); + cb(); }); }; @@ -175,7 +172,7 @@ ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) { logger.debug(stderr); return cb(err); } - cb(null); + cb(); }); }; @@ -195,7 +192,8 @@ ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) { java = isWindows ? '"' + java + '.exe"' : '"' + java + '"'; var moveManifestCmd = '"' + path.resolve(helperJarPath, 'move_manifest.jar') + '"'; - moveManifestCmd = [java, '-jar', moveManifestCmd, '"' + dstApk + '"', '"' + manifest + '"'].join(' '); + moveManifestCmd = [java, '-jar', moveManifestCmd, '"' + dstApk + '"', '"' + + manifest + '"'].join(' '); logger.debug("Moving manifest with: " + moveManifestCmd); exec(moveManifestCmd, { maxBuffer: 524288 }, function(err) { @@ -218,7 +216,7 @@ ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) { return cb(err); } logger.debug("Inserted manifest."); - cb(null); + cb(); }); } }; @@ -231,10 +229,10 @@ ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) { ], cb); }; -// apks is an array of strings. -ADB.prototype.signDefault = function(apks, cb) { +ADB.prototype.signWithDefaultCert = function(apks, cb) { var signPath = path.resolve(helperJarPath, 'sign.jar'); - var resign = 'java -jar "' + signPath + '" "' + apks.join('" "') + '" --override'; + 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) { @@ -247,27 +245,28 @@ ADB.prototype.signDefault = function(apks, cb) { }); }; -// apk is a single apk path -ADB.prototype.signCustom = function(apk, cb) { +ADB.prototype.signWithCustomCert = 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(helperJarPath, '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(' '); + 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) { + if (err || stderr) { logger.warn(stderr); return cb(new Error("Could not unsign apk. Are you sure " + "the file path is correct: " + @@ -275,7 +274,7 @@ ADB.prototype.signCustom = function(apk, cb) { } logger.debug("Signing apk with: " + sign); exec(sign, { maxBuffer: 524288 }, function(err, stdout, stderr) { - if (stderr) { + if (err || stderr) { logger.warn(stderr); return cb(new Error("Could not sign apk. Are you sure " + "the file path is correct: " + @@ -286,14 +285,13 @@ ADB.prototype.signCustom = function(apk, cb) { }); }; -// apks is an array of strings. ADB.prototype.sign = function(apks, cb) { if (this.useKeystore) { - async.each(apks, this.signCustom.bind(this), function(err) { + async.each(apks, this.signWithCustomCert.bind(this), function(err) { cb(err); }); } else { - this.signDefault(apks, cb); + this.signWithDefaultCert(apks, cb); } }; @@ -305,78 +303,7 @@ ADB.prototype.checkApkCert = function(apk, pkg, cb) { } 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(), pkg, '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(null, match); }); - - // exit checkApkCert - return; + return this.checkCustomApkCert(apk, pkg, cb); } var verifyPath = path.resolve(helperJarPath, 'verify.jar'); @@ -392,6 +319,78 @@ ADB.prototype.checkApkCert = function(apk, pkg, cb) { }); }; +ADB.prototype.checkCustomApkCert = function(apk, pkg, cb) { + 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 + '"'; + + this.getKeystoreMd5(keytool, md5, function(err, keystoreHash) { + if (err) return cb(err); + this.checkApkKeystoreMatch(keytool, md5, keystoreHash, apk, pkg, cb); + }.bind(this)); +}; + +ADB.prototype.getKeystoreMd5 = function(keytool, md5re, cb) { + var keystoreHash; + 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) { + if (err) return cb(err); + keystoreHash = md5re.exec(stdout); + keystoreHash = keystoreHash ? keystoreHash[1] : null; + logger.debug('Keystore MD5: ' + keystoreHash); + cb(null, keystoreHash); + }); +}; + +ADB.prototype.checkApkKeystoreMatch = function(keytool, md5re, keystoreHash, + pkg, apk, cb) { + 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 cb(null, false); // no more entries + entry = entry.entryName; + if (!rsa.test(entry)) return next(); + logger.debug("Entry: " + entry); + var entryPath = path.join(getTempPath(), pkg, '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 = md5re.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) { + return cb(null, true); + } else { + next(); + } + }); + }.bind(this); + + next(); +}; + ADB.prototype.getDevicesWithRetry = function(timeoutMs, cb) { if (typeof timeoutMs === "function") { cb = timeoutMs; @@ -472,7 +471,8 @@ ADB.prototype.killAllEmulators = function(cb) { "/usr/bin/killall -m emulator*"; exec(killallCmd, { maxBuffer: 524288 }, function(err) { if (err) { - logger.info("Could not kill emulator. It was probably not running. : " + err.message); + logger.info("Could not kill emulator. It was probably not running.: " + + err.message); } cb(); }); @@ -487,8 +487,12 @@ ADB.prototype.launchAVD = function(avdName, cb) { avdName = "@" + avdName; } - spawn(emulatorBinaryPath.substr(1, emulatorBinaryPath.length - 2), - [avdName]); + try { + spawn(emulatorBinaryPath.substr(1, emulatorBinaryPath.length - 2), + [avdName]); + } catch (err) { + return cb(err); + } this.getDeviceWithRetry(120000, cb); }.bind(this)); }; @@ -594,9 +598,7 @@ ADB.prototype.waitForDevice = function(cb) { }; 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) { + this.exec("kill-server", function(err) { if (err) { logger.error("Error killing ADB server, going to see if it's online " + "anyway"); @@ -684,7 +686,7 @@ ADB.prototype.waitForActivityOrNot = function(pkg, activity, not, waitMs = 20000; } - logger.info("Waiting for app's activity to not be focused"); + logger.info("Waiting for activity to " + (not ? "not" : "") + " be focused"); var intMs = 750 , endAt = Date.now() + waitMs; @@ -843,7 +845,6 @@ ADB.prototype.isAppInstalled = function(pkg, 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() { @@ -852,67 +853,45 @@ ADB.prototype.back = 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); + this.keyevent(3, 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(); - }); + var cmd = 'input keyevent ' + code; + this.shell(cmd, cb); }; -ADB.prototype.unlockScreen = function(cb) { - this.requireDeviceId(); - deviceState.isScreenLocked(this.adbCmd, function(err, isLocked) { - if (err) { - cb(err); - } else { - if (isLocked) { - this.debug("Attempting to unlock screen by starting Unlock app..."); - var cmd = this.adbCmd + " shell am start -n io.appium.unlock/.Unlock"; - exec(cmd, { maxBuffer: 524288 }, function(err, stdout, stderr) { - if (err) { - cb(err); - } else { - var intervalRepeat = 200; - var interval = function() { - deviceState.isScreenLocked(this.adbCmd, function(err, isLocked) { - if (err) { - cb(err); - } else { - if (isLocked) { - setTimeout(interval, intervalRepeat); - } else { - cb(); - } - } - }); - }.bind(this); - setTimeout(interval, intervalRepeat); - } - }.bind(this)); +ADB.prototype.isScreenLocked = function(cb) { + var cmd = "dumpsys window"; + this.exec(cmd, function(err, stdout) { + if (err) return cb(err); + + var screenLocked = /mShowingLockscreen=\w+/gi.exec(stdout); + var samsungNoteUnlocked = /mScreenOnFully=\w+/gi.exec(stdout); + var gbScreenLocked = /mCurrentFocus.+Keyguard/gi.exec(stdout); + + if (screenLocked && screenLocked[0]) { + if (screenLocked[0].split('=')[1] == 'false') { + cb(null, false); } else { - this.debug('Screen already unlocked, continuing.'); - cb(); + cb(null, true); } + } else if (samsungNoteUnlocked && samsungNoteUnlocked[0]) { + if (samsungNoteUnlocked[0].split('=')[1] == 'true') { + cb(null, false); + } else { + cb(null, true); + } + } else if (gbScreenLocked && gbScreenLocked[0]) { + cb(null, true); + } else { + cb(null, false); } - }.bind(this)); + + }); }; ADB.prototype.sendTelnetCommand = function(command, cb) { @@ -957,5 +936,4 @@ ADB.prototype.sendTelnetCommand = function(command, cb) { }); }; - module.exports = ADB; diff --git a/lib/devices/android/android-common.js b/lib/devices/android/android-common.js index c7dd946c7..ea64d0670 100644 --- a/lib/devices/android/android-common.js +++ b/lib/devices/android/android-common.js @@ -187,5 +187,31 @@ androidCommon.pushUnlock = function(cb) { }.bind(this)); }; +androidCommon.unlockScreen = function(cb) { + this.adb.isScreenLocked(function(err, isLocked) { + if (err) return cb(err); + if (isLocked) { + this.adb.startApp("io.appium.unlock", ".Unlock", function(err) { + if (err) return cb(err); + var intervalRepeat = 500; + var interval = function() { + this.adb.isScreenLocked(function(err, isLocked) { + if (err) return cb(err); + if (isLocked) { + setTimeout(interval, intervalRepeat); + } else { + cb(); + } + }); + }.bind(this); + setTimeout(interval, intervalRepeat); + }.bind(this)); + } else { + logger.debug('Screen already unlocked, continuing.'); + cb(); + } + }.bind(this)); +}; + module.exports = androidCommon; diff --git a/lib/devices/android/android-controller.js b/lib/devices/android/android-controller.js index eefdd8a38..dd7f4693d 100644 --- a/lib/devices/android/android-controller.js +++ b/lib/devices/android/android-controller.js @@ -636,6 +636,14 @@ androidController.getCurrentActivity = function(cb) { }); }; +androidController.fastReset = function(cb) { + async.series([ + function(cb) { this.adb.stopAndClear(this.appPackage, cb); }.bind(this), + this.adb.waitForNotActivity.bind(this), + this.adb.startApp.bind(this) + ], cb); +}; + androidController.isAppInstalled = function(appPackage, cb) { var isInstalledCommand = null; if (this.udid) { diff --git a/lib/devices/android/android.js b/lib/devices/android/android.js index 1edd8811f..775d615d8 100644 --- a/lib/devices/android/android.js +++ b/lib/devices/android/android.js @@ -23,6 +23,7 @@ var Android = function(opts) { Android.prototype.initialize = function(opts) { this.compressXml = opts.compressXml; + this.skipUninstall = opts.skipUninstall; this.rest = opts.rest; this.webSocket = opts.webSocket; opts.systemPort = opts.systemPort || 4724; @@ -61,15 +62,6 @@ Android.prototype.initialize = function(opts) { }; }; -// Clear data, close app, then start app. -Android.prototype.fastReset = function(cb) { - async.series([ - function(cb) { this.adb.stopAndClear(this.appPackage, cb); }.bind(this), - function(cb) { this.adb.waitForNotActivity(cb); }.bind(this), - function(cb) { this.adb.startApp(cb); }.bind(this), - ], cb); -}; - Android.prototype.start = function(cb, onDie) { if (typeof onDie === "function") { this.onStop = onDie; @@ -154,7 +146,6 @@ Android.prototype.start = function(cb, onDie) { } }; -// XXX convert to use new adb Android.prototype.startAppium = function(onLaunch, onExit) { logger.info("Starting android appium"); this.uiautomator.onExit = onExit; @@ -171,8 +162,8 @@ Android.prototype.startAppium = function(onLaunch, onExit) { this.pushAppium.bind(this), this.pushUnlock.bind(this), this.uiautomator.start.bind(this.uiautomator), - this.adb.wakeUp.bind(this.adb), - this.adb.unlockScreen.bind(this.adb), + this.wakeUp.bind(this), + this.unlockScreen.bind(this), this.startApp.bind(this) ], function(err) { onLaunch(err); @@ -362,6 +353,12 @@ Android.prototype.push = function(elem) { next(); }; +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.waitForCondition = deviceCommon.waitForCondition; Android.prototype.setCommandTimeout = function(secs, cb) { diff --git a/lib/devices/android/device-state.js b/lib/devices/android/device-state.js deleted file mode 100644 index 75bdb89e9..000000000 --- a/lib/devices/android/device-state.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; - -var exec = require('child_process').exec - , logger = require('../../server/logger.js').get('appium'); - -function log(msg) { - logger.info("[ADB] " + msg); -} - -module.exports = { - isScreenLocked: function(adbCmd, cb) { - var cmd = adbCmd + " shell dumpsys window"; - log("Checking if screen is unlocked via `dumpsys window`..."); - exec(cmd, {maxBuffer: 524288}, function(err, stdout) { - if (err) { - cb(err); - } else { - var screenLocked = /mShowingLockscreen=\w+/gi.exec(stdout); - var samsungNoteUnlocked = /mScreenOnFully=\w+/gi.exec(stdout); - var gbScreenLocked = /mCurrentFocus.+Keyguard/gi.exec(stdout); - if (screenLocked && screenLocked[0]) { - if (screenLocked[0].split('=')[1] == 'false') { - cb(null, false); - } else { - cb(null, true); - } - } else if (samsungNoteUnlocked && samsungNoteUnlocked[0]) { - if (samsungNoteUnlocked[0].split('=')[1] == 'true') { - cb(null, false); - } else { - cb(null, true); - } - } else if (gbScreenLocked && gbScreenLocked[0]) { - cb(null, true); - } else { - cb(null, false); - } - } - }); - } -}; diff --git a/lib/devices/android/selendroid.js b/lib/devices/android/selendroid.js index 6d65f2ba6..fb250f47b 100644 --- a/lib/devices/android/selendroid.js +++ b/lib/devices/android/selendroid.js @@ -113,9 +113,8 @@ ADB.prototype.startSelendroid = function(serverPath, onReady) { }; Selendroid.prototype.pushSelendroid = function(cb) { - var instrumentWith = this.appPackage + - ".selendroid/io.selendroid.ServerInstrumentation"; - + var instrumentWith = this.appPackage + ".selendroid/" + + "io.selendroid.ServerInstrumentation"; this.adb.instrument(this.appPackage, this.appActivity, instrumentWith, cb); };