blob: dee0df23e71e68260665d89ff76f547352941cea [file] [log] [blame]
var util = require('util')
var events = require('events')
var syrup = require('stf-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 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
})
})
}
function prepareForServiceDeath(conn) {
function endListener() {
var startTime = Date.now()
log.important('Service connection ended, attempting to relaunch')
/* eslint no-use-before-define: 0 */
openService()
.timeout(5000)
.then(function() {
log.important('Service relaunched in %dms', Date.now() - startTime)
})
.catch(function(err) {
log.fatal('Service connection could not be relaunched', err.stack)
lifecycle.fatal()
})
}
conn.once('end', endListener)
conn.on('error', function(err) {
log.fatal('Service connection had an error', err.stack)
lifecycle.fatal()
})
}
function handleEnvelope(data) {
var envelope = apk.wire.Envelope.decode(data)
var 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
}
}
}
// 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)
return prepareForServiceDeath(conn)
})
}
function prepareForAgentDeath(conn) {
function endListener() {
var startTime = Date.now()
log.important('Agent connection ended, attempting to relaunch')
openService()
.timeout(5000)
.then(function() {
log.important('Agent relaunched in %dms', Date.now() - startTime)
})
.catch(function(err) {
log.fatal('Agent connection could not be relaunched', err.stack)
lifecycle.fatal()
})
}
conn.once('end', endListener)
conn.on('error', function(err) {
log.fatal('Agent connection had an error', err.stack)
lifecycle.fatal()
})
}
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) {
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)
return prepareForAgentDeath(conn)
})
}
function runAgentCommand(type, cmd) {
agent.writer.write(new apk.wire.Envelope(
null
, type
, cmd.encodeNB()
).encodeNB())
}
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)
.delay(500) // Give it a little bit of time to settle.
.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()
}
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)
}
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
, size: Math.sqrt(
Math.pow(response.width / response.xdpi, 2) +
Math.pow(response.height / response.ydpi, 2)
)
}
}
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, options.lockRotation || 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)
}
plugin.setMasterMute = function(mode) {
return runServiceCommand(
apk.wire.MessageType.SET_MASTER_MUTE
, new apk.wire.SetMasterMuteRequest(mode)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetMasterMuteResponse.decode(data)
if (!response.success) {
throw new Error('Unable to set master mute')
}
})
}
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)
})