diff --git a/bin/templates/cordova/lib/Adb.js b/bin/templates/cordova/lib/Adb.js index 042a0c2a..a78a6ab4 100644 --- a/bin/templates/cordova/lib/Adb.js +++ b/bin/templates/cordova/lib/Adb.js @@ -53,13 +53,17 @@ Adb.devices = function (opts) { }); }; -Adb.install = function (target, packagePath, opts) { +Adb.install = function (target, packagePath, { replace = false, execOptions = {} } = {}) { events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...'); + var args = ['-s', target, 'install']; - if (opts && opts.replace) args.push('-r'); - return execa('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(({ stdout: output }) => { - // 'adb install' seems to always returns no error, even if installation fails - // so we catching output to detect installation failure + if (replace) args.push('-r'); + + const opts = { cwd: os.tmpdir(), ...execOptions }; + + return execa('adb', args.concat(packagePath), opts).then(({ stdout: output }) => { + // adb does not return an error code even if installation fails. Instead it puts a specific + // message to stdout, so we have to use RegExp matching to detect installation failure. if (output.match(/Failure/)) { if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) { output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' + @@ -69,7 +73,7 @@ Adb.install = function (target, packagePath, opts) { '\nEither uninstall an app or increment the versionCode.'; } - return Promise.reject(new CordovaError('Failed to install apk to device: ' + output)); + throw new CordovaError('Failed to install apk to target: ' + output); } }); }; diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index fb0a5b07..e7b24919 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -30,7 +30,6 @@ var CordovaError = require('cordova-common').CordovaError; var android_sdk = require('./android_sdk'); var check_reqs = require('./check_reqs'); var which = require('which'); -var os = require('os'); // constants const ONE_SECOND = 1000; // in milliseconds @@ -406,11 +405,6 @@ module.exports.install = function (givenTarget, buildResults) { // or the app doesn't installed at all, so no error catching needed. return Promise.resolve().then(function () { var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); - var execOptions = { - cwd: os.tmpdir(), - timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds - killSignal: EXEC_KILL_SIGNAL - }; events.emit('log', 'Using apk: ' + apk_path); events.emit('log', 'Package name: ' + pkgName); @@ -419,29 +413,18 @@ module.exports.install = function (givenTarget, buildResults) { // A special function to call adb install in specific environment w/ specific options. // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119 // to workaround sporadic emulator hangs - function adbInstallWithOptions (target, apk, opts) { - events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...'); - - const args = ['-s', target, 'install', '-r', apk]; - return execa('adb', args, opts).then(({ stdout }) => { - // adb does not return an error code even if installation fails. Instead it puts a specific - // message to stdout, so we have to use RegExp matching to detect installation failure. - if (/Failure/.test(stdout)) { - if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) { - stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' + - ' or sign and deploy the unsigned apk manually using Android tools.'; - } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) { - stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' + - '\nEither uninstall an app or increment the versionCode.'; - } - - throw new CordovaError('Failed to install apk to emulator: ' + stdout); + function adbInstallWithOptions (...args) { + return Adb.install(...args, { + replace: true, + execOptions: { + timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds + killSignal: EXEC_KILL_SIGNAL } }); } function installPromise () { - return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) { + return adbInstallWithOptions(target.target, apk_path).catch(function (error) { // CB-9557 CB-10157 only uninstall and reinstall app if the one that // is already installed on device was signed w/different certificate if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; } @@ -452,7 +435,7 @@ module.exports.install = function (givenTarget, buildResults) { // This promise is always resolved, even if 'adb uninstall' fails to uninstall app // or the app doesn't installed at all, so no error catching needed. return Adb.uninstall(target.target, pkgName).then(function () { - return adbInstallWithOptions(target.target, apk_path, execOptions); + return adbInstallWithOptions(target.target, apk_path); }); }); } diff --git a/spec/unit/emulator.spec.js b/spec/unit/emulator.spec.js index 239e93de..70ae5c2f 100644 --- a/spec/unit/emulator.spec.js +++ b/spec/unit/emulator.spec.js @@ -582,7 +582,6 @@ describe('emulator', () => { let AndroidManifestGetActivitySpy; let AdbSpy; let buildSpy; - let execaSpy; let target; beforeEach(() => { @@ -597,14 +596,12 @@ describe('emulator', () => { AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns); emu.__set__('AndroidManifest', AndroidManifestSpy); - AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'uninstall']); + AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'install', 'uninstall']); AdbSpy.shell.and.returnValue(Promise.resolve()); AdbSpy.start.and.returnValue(Promise.resolve()); + AdbSpy.install.and.returnValue(Promise.resolve()); AdbSpy.uninstall.and.returnValue(Promise.resolve()); emu.__set__('Adb', AdbSpy); - - execaSpy = jasmine.createSpy('execa').and.resolveTo({}); - emu.__set__('execa', execaSpy); }); it('should get the full target object if only id is specified', () => { @@ -618,8 +615,7 @@ describe('emulator', () => { it('should install to the passed target', () => { return emu.install(target, {}).then(() => { - const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); - expect(execCmd).toContain(`-s ${target.target} install`); + expect(AdbSpy.install.calls.argsFor(0)[0]).toBe(target.target); }); }); @@ -636,26 +632,25 @@ describe('emulator', () => { return emu.install(target, buildResults).then(() => { expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch); - const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); - expect(execCmd).toContain(`install -r ${apkPath}`); + expect(AdbSpy.install.calls.argsFor(0)[1]).toBe(apkPath); }); }); it('should uninstall and reinstall app if failure is due to different certificates', () => { - execaSpy.and.returnValues( - ...['Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES', ''] - .map(out => Promise.resolve({ stdout: out })) + AdbSpy.install.and.returnValues( + Promise.reject('Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES'), + Promise.resolve() ); return emu.install(target, {}).then(() => { - expect(execaSpy).toHaveBeenCalledTimes(2); + expect(AdbSpy.install).toHaveBeenCalledTimes(2); expect(AdbSpy.uninstall).toHaveBeenCalled(); }); }); it('should throw any error not caused by different certificates', () => { const errorMsg = 'Failure: Failed to install'; - execaSpy.and.resolveTo({ stdout: errorMsg }); + AdbSpy.install.and.rejectWith(new CordovaError(errorMsg)); return emu.install(target, {}).then( () => fail('Unexpectedly resolved'),