diff --git a/lib/roles/device.js b/lib/roles/device.js index 1bc35f5f..56ea36ff 100644 --- a/lib/roles/device.js +++ b/lib/roles/device.js @@ -239,7 +239,7 @@ module.exports = function(options) { .then(function(port) { var log = logger.createLogger('device:inputAgent') return promiseutil.periodicNotify( - inputAgent.open(adb, options.serial) + inputAgent.openAgent(adb, options.serial) , 1000 ) .progressed(function() { @@ -264,8 +264,34 @@ module.exports = function(options) { ) }) }) + .then(function(apk) { + log.info('Launching InputService') + return inputAgent.stopService(adb, options.serial) + .then(function() { + return devutil.waitForPortToFree(adb, options.serial, 1100) + }) + .then(function(port) { + var log = logger.createLogger('device:inputService') + return inputAgent.openService(adb, options.serial) + .then(function() { + return promiseutil.periodicNotify( + devutil.waitForPort(adb, options.serial, port) + , 1000 + ) + .progressed(function() { + log.info('Waiting for InputService') + }) + }) + }) + .then(function(conn) { + services.inputServiceSocket = vitals.register( + 'device:inputService:socket' + , conn + ) + }) + }) .then(function() { - log.info('Launching input service') + log.info('Launching TouchService') return devutil.ensureUnusedPort(adb, options.serial, 2820) .then(function(port) { var log = logger.createLogger('device:remote:input') @@ -290,7 +316,7 @@ module.exports = function(options) { return monkey.connectStream(conn) }) .then(function(monkey) { - services.input = vitals.register( + services.touch = vitals.register( 'device:remote:input:monkey' , Promise.promisifyAll(monkey) ) @@ -404,25 +430,25 @@ module.exports = function(options) { } }) .on(wire.TouchDownMessage, function(channel, message) { - services.input.touchDownAsync(message.x, message.y) + services.touch.touchDownAsync(message.x, message.y) .catch(function(err) { log.error('touchDown failed', err.stack) }) }) .on(wire.TouchMoveMessage, function(channel, message) { - services.input.touchMoveAsync(message.x, message.y) + services.touch.touchMoveAsync(message.x, message.y) .catch(function(err) { log.error('touchMove failed', err.stack) }) }) .on(wire.TouchUpMessage, function(channel, message) { - services.input.touchUpAsync(message.x, message.y) + services.touch.touchUpAsync(message.x, message.y) .catch(function(err) { log.error('touchUp failed', err.stack) }) }) .on(wire.TapMessage, function(channel, message) { - services.input.tapAsync(message.x, message.y) + services.touch.tapAsync(message.x, message.y) .catch(function(err) { log.error('tap failed', err.stack) }) @@ -593,6 +619,8 @@ module.exports = function(options) { )) ]) services.logcat.on('entry', logcatListener) + inputAgent.acquireWakeLock(services.inputServiceSocket) + inputAgent.unlock(services.inputServiceSocket) owner = newOwner } @@ -609,6 +637,8 @@ module.exports = function(options) { )) ]) services.logcat.removeListener('entry', logcatListener) + inputAgent.releaseWakeLock(services.inputServiceSocket) + inputAgent.lock(services.inputServiceSocket) owner = null } diff --git a/lib/services/inputagent.js b/lib/services/inputagent.js index b6a36128..15ff9f72 100644 --- a/lib/services/inputagent.js +++ b/lib/services/inputagent.js @@ -3,38 +3,106 @@ var util = require('util') var Promise = require('bluebird') var ProtoBuf = require('protobufjs') var ByteBuffer = require('protobufjs/node_modules/bytebuffer') -var split = require('split') +var semver = require('semver') var pathutil = require('../util/pathutil') var streamutil = require('../util/streamutil') +var SUPPORTED_VERSION = '~0.1.2'; + +var pkg = 'jp.co.cyberagent.stf.input.agent' +var apk = pathutil.vendor('InputAgent/InputAgent.apk') var proto = ProtoBuf.loadProtoFile( - pathutil.vendor('InputAgent/inputAgentProtocol.proto') -).build().jp.co.cyberagent.stf.input.agent + pathutil.vendor('InputAgent/proto/agent.proto') +).build().jp.co.cyberagent.stf.input.agent.proto var inputAgent = module.exports = Object.create(null) -inputAgent.open = function(adb, serial) { - return adb.installAsync(serial, pathutil.vendor('InputAgent/InputAgent.apk')) - .then(function() { - return adb.shellAsync(serial, 'pm path jp.co.cyberagent.stf.input.agent') - }) +function IncompatibleVersionError(version) { + Error.call(this, util.format('Incompatible version %s', version)) + this.name = 'IncompatibleVersionError' + this.version = version + Error.captureStackTrace(this, IncompatibleVersionError) +} + +util.inherits(IncompatibleVersionError, Error) + +inputAgent.IncompatibleVersionError = IncompatibleVersionError + +inputAgent.getInstalledPath = function(adb, serial) { + return adb.shellAsync(serial, util.format("pm path '%s'", pkg)) .then(function(out) { return streamutil.findLine(out, (/^package:/)) .then(function(line) { return line.substr(8) }) }) - .then(function(apk) { +} + +inputAgent.ensureInstalled = function(adb, serial) { + return inputAgent.getInstalledPath(adb, serial) + .then(function(installedPath) { + return adb.shellAsync(serial, util.format( + "export CLASSPATH='%s';" + + ' exec app_process /system/bin' + + ' jp.co.cyberagent.stf.input.agent.InputAgent --version' + , installedPath + )) + .then(function(out) { + return streamutil.readAll(out) + .then(function(buffer) { + var version = buffer.toString() + if (semver.satisfies(version, SUPPORTED_VERSION)) { + return installedPath + } + else { + return Promise.reject(new IncompatibleVersionError(version)) + } + }) + }) + }) + .catch(function() { + return adb.installAsync(serial, apk) + .then(function() { + return inputAgent.getInstalledPath(adb, serial) + }) + }) +} + +inputAgent.openAgent = function(adb, serial) { + return inputAgent.ensureInstalled(adb, serial) + .then(function(installedPath) { return adb.shellAsync(serial, util.format( "export CLASSPATH='%s';" + ' exec app_process /system/bin' + ' jp.co.cyberagent.stf.input.agent.InputAgent' - , apk + , installedPath )) }) } +inputAgent.openService = function(adb, serial) { + return inputAgent.ensureInstalled(adb, serial) + .then(function() { + var intent = + '-a jp.co.cyberagent.stf.input.agent.InputService.ACTION_START' + return adb.shellAsync(serial, util.format( + 'am startservice --user 0 %s || am startservice %s' + , intent + , intent + )) + }) +} + +inputAgent.stopService = function(adb, serial) { + var intent = '-a jp.co.cyberagent.stf.input.agent.InputService.ACTION_STOP' + return adb.shellAsync(serial, util.format( + 'am startservice --user 0 %s || am startservice %s' + , intent + , intent + )) +} + inputAgent.sendInputEvent = function(agent, event) { var lengthBuffer = new ByteBuffer() , messageBuffer = new proto.InputEvent(event).encode() @@ -46,3 +114,23 @@ inputAgent.sendInputEvent = function(agent, event) { , messageBuffer.toBuffer() ])) } + +inputAgent.unlock = function(service) { + service.write('unlock\n'); +} + +inputAgent.lock = function(service) { + service.write('lock\n'); +} + +inputAgent.acquireWakeLock = function(service) { + service.write('acquire wake lock\n'); +} + +inputAgent.releaseWakeLock = function(service) { + service.write('release wake lock\n'); +} + +inputAgent.identity = function(service, serial) { + service.write(util.format('show identity %s\n', serial)); +} diff --git a/lib/util/devutil.js b/lib/util/devutil.js index 879c0096..db3ee45d 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -91,6 +91,36 @@ devutil.waitForPort = function(adb, serial, port) { }) } +devutil.waitForPortToFree = function(adb, serial, port) { + function closedError(err) { + return err.message === 'closed' + } + return adb.openTcpAsync(serial, port) + .then(function(conn) { + var resolver = Promise.defer() + + function endListener() { + resolver.resolve(port) + } + + function errorListener(err) { + resolver.reject(err) + } + + conn.on('end', endListener) + conn.on('error', errorListener) + + return resolver.promise.finally(function() { + conn.removeListener('end', endListener) + conn.removeListener('error', errorListener) + conn.end() + }) + }) + .catch(closedError, function(err) { + return port + }) +} + devutil.listPidsByComm = function(adb, serial, comm, bin) { var users = { shell: true diff --git a/lib/util/streamutil.js b/lib/util/streamutil.js index afc282d5..b3ca3358 100644 --- a/lib/util/streamutil.js +++ b/lib/util/streamutil.js @@ -1,6 +1,18 @@ +var util = require('util') + var Promise = require('bluebird') var split = require('split') +function NoSuchLineError(message) { + Error.call(this, message) + this.name = 'NoSuchLineError' + Error.captureStackTrace(this, NoSuchLineError) +} + +util.inherits(NoSuchLineError, Error) + +module.exports.NoSuchLineError = NoSuchLineError + module.exports.readAll = function(stream) { var resolver = Promise.defer() , collected = new Buffer(0) @@ -41,7 +53,7 @@ module.exports.findLine = function(stream, re) { } function endListener() { - resolver.reject(new Error('No matching line found')) + resolver.reject(new NoSuchLineError()) } function lineListener(line) { diff --git a/vendor/InputAgent/InputAgent.apk b/vendor/InputAgent/InputAgent.apk index 05933e65..8dab5573 100644 Binary files a/vendor/InputAgent/InputAgent.apk and b/vendor/InputAgent/InputAgent.apk differ diff --git a/vendor/InputAgent/inputAgentProtocol.proto b/vendor/InputAgent/proto/agent.proto similarity index 76% rename from vendor/InputAgent/inputAgentProtocol.proto rename to vendor/InputAgent/proto/agent.proto index 300f39f4..aeadd9ae 100644 --- a/vendor/InputAgent/inputAgentProtocol.proto +++ b/vendor/InputAgent/proto/agent.proto @@ -1,15 +1,18 @@ -package jp.co.cyberagent.stf.input.agent; +package jp.co.cyberagent.stf.input.agent.proto; + +option java_outer_classname = "AgentProto"; enum InputAction { KEYDOWN = 0; KEYUP = 1; KEYPRESS = 2; TYPE = 3; + WAKE = 4; } message InputEvent { required InputAction action = 1; - required int32 keyCode = 2; + optional int32 keyCode = 2; optional bool shiftKey = 3; optional bool ctrlKey = 4; optional bool altKey = 5;