var stream = require('stream') var url = require('url') var util = require('util') var syrup = require('stf-syrup') var request = require('request') var Promise = require('bluebird') var logger = require('../../../util/logger') var wire = require('../../../wire') var wireutil = require('../../../wire/util') var promiseutil = require('../../../util/promiseutil') // The error codes are available at https://github.com/android/ // platform_frameworks_base/blob/master/core/java/android/content/ // pm/PackageManager.java function InstallationError(err) { return err.code && /^INSTALL_/.test(err.code) } module.exports = syrup.serial() .dependency(require('../support/adb')) .dependency(require('../support/router')) .dependency(require('../support/push')) .define(function(options, adb, router, push) { var log = logger.createLogger('device:plugins:install') router.on(wire.InstallMessage, function(channel, message) { var manifest = JSON.parse(message.manifest) var pkg = manifest.package log.info('Installing package "%s" from "%s"', pkg, message.href) var reply = wireutil.reply(options.serial) function sendProgress(data, progress) { push.send([ channel , reply.progress(data, progress) ]) } function pushApp() { var req = request({ url: url.resolve(options.storageUrl, message.href) }) // We need to catch the Content-Length on the fly or we risk // losing some of the initial chunks. var contentLength = null req.on('response', function(res) { contentLength = parseInt(res.headers['content-length'], 10) }) var source = new stream.Readable().wrap(req) var target = '/data/local/tmp/_app.apk' return adb.push(options.serial, source, target) .timeout(10000) .then(function(transfer) { var resolver = Promise.defer() function progressListener(stats) { if (contentLength) { // Progress 0% to 70% sendProgress( 'pushing_app' , 50 * Math.max(0, Math.min( 50 , stats.bytesTransferred / contentLength )) ) } } function errorListener(err) { resolver.reject(err) } function endListener() { resolver.resolve(target) } transfer.on('progress', progressListener) transfer.on('error', errorListener) transfer.on('end', endListener) return resolver.promise.finally(function() { transfer.removeListener('progress', progressListener) transfer.removeListener('error', errorListener) transfer.removeListener('end', endListener) }) }) } // Progress 0% sendProgress('pushing_app', 0) pushApp() .then(function(apk) { var start = 50 var end = 90 var guesstimate = start sendProgress('installing_app', guesstimate) return promiseutil.periodicNotify( adb.installRemote(options.serial, apk) .timeout(60000 * 5) .catch(function(err) { switch (err.code) { case 'INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES': case 'INSTALL_FAILED_VERSION_DOWNGRADE': log.info( 'Uninstalling "%s" first due to inconsistent certificates' , pkg ) return adb.uninstall(options.serial, pkg) .timeout(15000) .then(function() { return adb.installRemote(options.serial, apk) .timeout(60000 * 5) }) default: return Promise.reject(err) } }) , 250 ) .progressed(function() { guesstimate = Math.min( end , guesstimate + 1.5 * (end - guesstimate) / (end - start) ) sendProgress('installing_app', guesstimate) }) }) .then(function() { if (message.launch) { if (manifest.application.launcherActivities.length) { var activityName = manifest.application.launcherActivities[0].name // According to the AndroidManifest.xml documentation the dot is // required, but actually it isn't. if (activityName.indexOf('.') === -1) { activityName = util.format('.%s', activityName) } var launchActivity = { action: 'android.intent.action.MAIN' , component: util.format( '%s/%s' , pkg , activityName ) , category: ['android.intent.category.LAUNCHER'] , flags: 0x10200000 } log.info( 'Launching activity with action "%s" on component "%s"' , launchActivity.action , launchActivity.component ) // Progress 90% sendProgress('launching_app', 90) return adb.startActivity(options.serial, launchActivity) .timeout(30000) } } }) .then(function() { push.send([ channel , reply.okay('INSTALL_SUCCEEDED') ]) }) .catch(Promise.TimeoutError, function(err) { log.error('Installation of package "%s" failed', pkg, err.stack) push.send([ channel , reply.fail('INSTALL_ERROR_TIMEOUT') ]) }) .catch(InstallationError, function(err) { log.important( 'Tried to install package "%s", got "%s"' , pkg , err.code ) push.send([ channel , reply.fail(err.code) ]) }) .catch(function(err) { log.error('Installation of package "%s" failed', pkg, err.stack) push.send([ channel , reply.fail('INSTALL_ERROR_UNKNOWN') ]) }) }) router.on(wire.UninstallMessage, function(channel, message) { log.info('Uninstalling "%s"', message.packageName) var reply = wireutil.reply(options.serial) adb.uninstall(options.serial, message.packageName) .then(function() { push.send([ channel , reply.okay('success') ]) }) .catch(function(err) { log.error('Uninstallation failed', err.stack) push.send([ channel , reply.fail('fail') ]) }) }) })