var util = require('util') var events = require('events') var syrup = require('syrup') var Promise = require('bluebird') var wire = require('../../../wire') var wireutil = require('../../../wire/util') var devutil = require('../../../util/devutil') var keyutil = require('../../../util/keyutil') var streamutil = require('../../../util/streamutil') var logger = require('../../../util/logger') var ms = require('../../../wire/messagestream') var lifecycle = require('../../../util/lifecycle') function MessageResolver() { this.resolvers = Object.create(null) this.await = function(id, resolver) { this.resolvers[id] = resolver return resolver.promise } this.resolve = function(id, value) { var resolver = this.resolvers[id] delete this.resolvers[id] resolver.resolve(value) return resolver.promise } } module.exports = syrup.serial() .dependency(require('../support/adb')) .dependency(require('../support/router')) .dependency(require('../support/push')) .dependency(require('../resources/service')) .define(function(options, adb, router, push, apk) { var log = logger.createLogger('device:plugins:service') var messageResolver = new MessageResolver() var plugin = new events.EventEmitter() var agent = { socket: null , writer: null , port: 1090 } var service = { socket: null , writer: null , reader: null , port: 1100 } function openAgent() { log.info('Launching agent') return stopAgent() .timeout(15000) .then(function() { return devutil.ensureUnusedPort(adb, options.serial, agent.port) .timeout(10000) }) .then(function() { return adb.shell(options.serial, util.format( "export CLASSPATH='%s'; exec app_process /system/bin '%s'" , apk.path , apk.main )) .timeout(10000) }) .then(function(out) { lifecycle.share('Agent shell', out) streamutil.talk(log, 'Agent says: "%s"', out) }) .then(function() { return devutil.waitForPort(adb, options.serial, agent.port) .timeout(10000) }) .then(function(conn) { agent.socket = conn agent.writer = new ms.DelimitingStream() agent.writer.pipe(conn) lifecycle.share('Agent connection', conn) }) } function stopAgent() { return devutil.killProcsByComm( adb , options.serial , 'stf.agent' , 'stf.agent' ) } function callService(intent) { return adb.shell(options.serial, util.format( 'am startservice --user 0 %s' , intent )) .timeout(15000) .then(function(out) { return streamutil.findLine(out, /^Error/) .finally(function() { out.end() }) .timeout(10000) .then(function(line) { if (line.indexOf('--user') !== -1) { return adb.shell(options.serial, util.format( 'am startservice %s' , intent )) .timeout(15000) .then(function() { return streamutil.findLine(out, /^Error/) .finally(function() { out.end() }) .timeout(10000) .then(function(line) { throw new Error(util.format( 'Service had an error: "%s"' , line )) }) .catch(streamutil.NoSuchLineError, function() { return true }) }) } else { throw new Error(util.format( 'Service had an error: "%s"' , line )) } }) .catch(streamutil.NoSuchLineError, function() { return true }) }) } // The APK should be up to date at this point. If it was reinstalled, the // service should have been automatically stopped while it was happening. // So, we should be good to go. function openService() { log.info('Launching service') return callService(util.format( "-a '%s' -n '%s'" , apk.startIntent.action , apk.startIntent.component )) .then(function() { return devutil.waitForPort(adb, options.serial, service.port) .timeout(15000) }) .then(function(conn) { service.socket = conn service.reader = conn.pipe(new ms.DelimitedStream()) service.reader.on('data', handleEnvelope) service.writer = new ms.DelimitingStream() service.writer.pipe(conn) lifecycle.share('Service connection', conn) }) } function handleEnvelope(data) { var envelope = apk.wire.Envelope.decode(data) , message if (envelope.id !== null) { messageResolver.resolve(envelope.id, envelope.message) } else { switch (envelope.type) { case apk.wire.MessageType.EVENT_AIRPLANE_MODE: message = apk.wire.AirplaneModeEvent.decode(envelope.message) push.send([ wireutil.global , wireutil.envelope(new wire.AirplaneModeEvent( options.serial , message.enabled )) ]) plugin.emit('airplaneModeChange', message) break case apk.wire.MessageType.EVENT_BATTERY: message = apk.wire.BatteryEvent.decode(envelope.message) push.send([ wireutil.global , wireutil.envelope(new wire.BatteryEvent( options.serial , message.status , message.health , message.source , message.level , message.scale , message.temp , message.voltage )) ]) plugin.emit('batteryChange', message) break case apk.wire.MessageType.EVENT_BROWSER_PACKAGE: message = apk.wire.BrowserPackageEvent.decode(envelope.message) plugin.emit('browserPackageChange', message) break case apk.wire.MessageType.EVENT_CONNECTIVITY: message = apk.wire.ConnectivityEvent.decode(envelope.message) push.send([ wireutil.global , wireutil.envelope(new wire.ConnectivityEvent( options.serial , message.connected , message.type , message.subtype , message.failover , message.roaming )) ]) plugin.emit('connectivityChange', message) break case apk.wire.MessageType.EVENT_PHONE_STATE: message = apk.wire.PhoneStateEvent.decode(envelope.message) push.send([ wireutil.global , wireutil.envelope(new wire.PhoneStateEvent( options.serial , message.state , message.manual , message.operator )) ]) plugin.emit('phoneStateChange', message) break case apk.wire.MessageType.EVENT_ROTATION: message = apk.wire.RotationEvent.decode(envelope.message) push.send([ wireutil.global , wireutil.envelope(new wire.RotationEvent( options.serial , message.rotation )) ]) plugin.emit('rotationChange', message) break } } } function keyEvent(data) { return runAgentCommand( apk.wire.MessageType.DO_KEYEVENT , new apk.wire.KeyEventRequest(data) ) } plugin.type = function(text) { return runAgentCommand( apk.wire.MessageType.DO_TYPE , new apk.wire.DoTypeRequest(text) ) } plugin.paste = function(text) { return plugin.setClipboard(text) .then(function() { keyEvent({ event: apk.wire.KeyEvent.PRESS , keyCode: adb.Keycode.KEYCODE_V , ctrlKey: true }) }) } plugin.copy = function() { // @TODO Not sure how to force the device to copy the current selection // yet. return plugin.getClipboard() } plugin.getDisplay = function(id) { return runServiceCommand( apk.wire.MessageType.GET_DISPLAY , new apk.wire.GetDisplayRequest(id) ) .timeout(10000) .then(function(data) { var response = apk.wire.GetDisplayResponse.decode(data) if (response.success) { return { id: id , width: response.width , height: response.height , xdpi: response.xdpi , ydpi: response.ydpi , fps: response.fps , density: response.density , rotation: response.rotation , secure: response.secure } } throw new Error('Unable to retrieve display information') }) } plugin.wake = function() { return runAgentCommand( apk.wire.MessageType.DO_WAKE , new apk.wire.DoWakeRequest() ) } plugin.rotate = function(rotation) { return runAgentCommand( apk.wire.MessageType.SET_ROTATION , new apk.wire.SetRotationRequest(rotation, false) ) } plugin.freezeRotation = function(rotation) { return runAgentCommand( apk.wire.MessageType.SET_ROTATION , new apk.wire.SetRotationRequest(rotation, true) ) } plugin.thawRotation = function() { return runAgentCommand( apk.wire.MessageType.SET_ROTATION , new apk.wire.SetRotationRequest(0, false) ) } plugin.version = function() { return runServiceCommand( apk.wire.MessageType.GET_VERSION , new apk.wire.GetVersionRequest() ) .timeout(10000) .then(function(data) { var response = apk.wire.GetVersionResponse.decode(data) if (response.success) { return response.version } throw new Error('Unable to retrieve version') }) } plugin.unlock = function() { return runServiceCommand( apk.wire.MessageType.SET_KEYGUARD_STATE , new apk.wire.SetKeyguardStateRequest(false) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetKeyguardStateResponse.decode(data) if (!response.success) { throw new Error('Unable to unlock device') } }) } plugin.lock = function() { return runServiceCommand( apk.wire.MessageType.SET_KEYGUARD_STATE , new apk.wire.SetKeyguardStateRequest(true) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetKeyguardStateResponse.decode(data) if (!response.success) { throw new Error('Unable to lock device') } }) } plugin.acquireWakeLock = function() { return runServiceCommand( apk.wire.MessageType.SET_WAKE_LOCK , new apk.wire.SetWakeLockRequest(true) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetWakeLockResponse.decode(data) if (!response.success) { throw new Error('Unable to acquire WakeLock') } }) } plugin.releaseWakeLock = function() { return runServiceCommand( apk.wire.MessageType.SET_WAKE_LOCK , new apk.wire.SetWakeLockRequest(false) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetWakeLockResponse.decode(data) if (!response.success) { throw new Error('Unable to release WakeLock') } }) } plugin.identity = function() { return runServiceCommand( apk.wire.MessageType.DO_IDENTIFY , new apk.wire.DoIdentifyRequest(options.serial) ) .timeout(10000) .then(function(data) { var response = apk.wire.DoIdentifyResponse.decode(data) if (!response.success) { throw new Error('Unable to identify device') } }) } plugin.setClipboard = function(text) { return runServiceCommand( apk.wire.MessageType.SET_CLIPBOARD , new apk.wire.SetClipboardRequest( apk.wire.ClipboardType.TEXT , text ) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetClipboardResponse.decode(data) if (!response.success) { throw new Error('Unable to set clipboard') } }) } plugin.getClipboard = function() { return runServiceCommand( apk.wire.MessageType.GET_CLIPBOARD , new apk.wire.GetClipboardRequest( apk.wire.ClipboardType.TEXT ) ) .timeout(10000) .then(function(data) { var response = apk.wire.GetClipboardResponse.decode(data) if (response.success) { switch (response.type) { case apk.wire.ClipboardType.TEXT: return response.text } } throw new Error('Unable to get clipboard') }) } plugin.getBrowsers = function() { return runServiceCommand( apk.wire.MessageType.GET_BROWSERS , new apk.wire.GetBrowsersRequest() ) .timeout(15000) .then(function(data) { var response = apk.wire.GetBrowsersResponse.decode(data) if (response.success) { delete response.success return response } throw new Error('Unable to get browser list') }) } plugin.getProperties = function(properties) { return runServiceCommand( apk.wire.MessageType.GET_PROPERTIES , new apk.wire.GetPropertiesRequest(properties) ) .timeout(15000) .then(function(data) { var response = apk.wire.GetPropertiesResponse.decode(data) if (response.success) { var mapped = Object.create(null) response.properties.forEach(function(property) { mapped[property.name] = property.value }) return mapped } throw new Error('Unable to get properties') }) } plugin.getAccounts = function(data) { return runServiceCommand( apk.wire.MessageType.GET_ACCOUNTS , new apk.wire.GetAccountsRequest({type: data.type}) ) .timeout(15000) .then(function(data) { var response = apk.wire.GetAccountsResponse.decode(data) if (response.success) { return response.accounts } throw new Error('No accounts returned') }) } plugin.removeAccount = function(data) { return runServiceCommand( apk.wire.MessageType.DO_REMOVE_ACCOUNT , new apk.wire.DoRemoveAccountRequest({ type: data.type , account: data.account }) ) .timeout(15000) .then(function(data) { var response = apk.wire.DoRemoveAccountResponse.decode(data) if (response.success) { return true } throw new Error('Unable to remove account') }) } plugin.addAccountMenu = function() { return runServiceCommand( apk.wire.MessageType.DO_ADD_ACCOUNT_MENU , new apk.wire.DoAddAccountMenuRequest() ) .timeout(15000) .then(function(data) { var response = apk.wire.DoAddAccountMenuResponse.decode(data) if (response.success) { return true } throw new Error('Unable to show add account menu') }) } plugin.setRingerMode = function(mode) { return runServiceCommand( apk.wire.MessageType.SET_RINGER_MODE , new apk.wire.SetRingerModeRequest(mode) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetRingerModeResponse.decode(data) if (!response.success) { throw new Error('Unable to set ringer mode') } }) } plugin.getRingerMode = function() { return runServiceCommand( apk.wire.MessageType.GET_RINGER_MODE , new apk.wire.GetRingerModeRequest() ) .timeout(10000) .then(function(data) { var response = apk.wire.GetRingerModeResponse.decode(data) // Reflection to decode enums to their string values, otherwise // we only get an integer var ringerMode = apk.builder.lookup('jp.co.cyberagent.stf.proto.RingerMode') .children[response.mode].name if (response.success) { return ringerMode } throw new Error('Unable to get ringer mode') }) } plugin.setWifiEnabled = function(enabled) { return runServiceCommand( apk.wire.MessageType.SET_WIFI_ENABLED , new apk.wire.SetWifiEnabledRequest(enabled) ) .timeout(10000) .then(function(data) { var response = apk.wire.SetWifiEnabledResponse.decode(data) if (!response.success) { throw new Error('Unable to set Wifi') } }) } plugin.getWifiStatus = function() { return runServiceCommand( apk.wire.MessageType.GET_WIFI_STATUS , new apk.wire.GetWifiStatusRequest() ) .timeout(10000) .then(function(data) { var response = apk.wire.GetWifiStatusResponse.decode(data) if (response.success) { return response.status } throw new Error('Unable to get Wifi status') }) } plugin.getSdStatus = function () { return runServiceCommand( apk.wire.MessageType.GET_SD_STATUS , new apk.wire.GetSdStatusRequest() ) .timeout(10000) .then(function(data) { var response = apk.wire.GetSdStatusResponse.decode(data) if (response.success) { return response.mounted } throw new Error('Unable to get SD card status') }) } plugin.pressKey = function(key) { keyEvent({event: apk.wire.KeyEvent.PRESS, keyCode: keyutil.namedKey(key)}) return Promise.resolve(true) } function runServiceCommand(type, cmd) { var resolver = Promise.defer() var id = Math.floor(Math.random() * 0xFFFFFF) service.writer.write(new apk.wire.Envelope( id , type , cmd.encodeNB() ).encodeNB()) return messageResolver.await(id, resolver) } function runAgentCommand(type, cmd) { agent.writer.write(new apk.wire.Envelope( null , type , cmd.encodeNB() ).encodeNB()) } return openAgent() .then(openService) .then(function() { router .on(wire.PhysicalIdentifyMessage, function(channel) { var reply = wireutil.reply(options.serial) plugin.identity() push.send([ channel , reply.okay() ]) }) .on(wire.KeyDownMessage, function(channel, message) { try { keyEvent({ event: apk.wire.KeyEvent.DOWN , keyCode: keyutil.namedKey(message.key) }) } catch(e) { log.warn(e.message) } }) .on(wire.KeyUpMessage, function(channel, message) { try { keyEvent({ event: apk.wire.KeyEvent.UP , keyCode: keyutil.namedKey(message.key) }) } catch(e) { log.warn(e.message) } }) .on(wire.KeyPressMessage, function(channel, message) { try { keyEvent({ event: apk.wire.KeyEvent.PRESS , keyCode: keyutil.namedKey(message.key) }) } catch(e) { log.warn(e.message) } }) .on(wire.TypeMessage, function(channel, message) { plugin.type(message.text) }) .on(wire.RotateMessage, function(channel, message) { plugin.rotate(message.rotation) }) }) .return(plugin) })