Rename "roles" to "units". Put units in their own folders.

This commit is contained in:
Simo Kinnunen
2014-08-26 14:34:34 +09:00
parent 7d9d64ddcb
commit 3a9b193f68
63 changed files with 105 additions and 105 deletions

View File

@@ -0,0 +1,324 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('./identity'))
.dependency(require('./touch'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.define(function(options, service, identity, touch, router, push, adb) {
var log = logger.createLogger('device:plugins:account')
function checkAccount(type, account) {
return service.getAccounts({type: type})
.timeout(30000)
.then(function(accounts) {
if(accounts.indexOf(account) >= 0) {
return true
}
throw new Error('The account is not added')
})
}
router.on(wire.AccountCheckMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Checking if account "%s" is added',message.account)
checkAccount(message.type, message.account)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Account check failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountGetMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Getting account(s)')
service.getAccounts(message)
.timeout(30000)
.then(function(accounts) {
push.send([
channel
, reply.okay('success',accounts)
])
})
.catch(function(err) {
log.error('Account get failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountRemoveMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Removing "%s" account(s)', message.type)
service.removeAccount(message)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Account removal failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountAddMenuMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.info('Showing add account menu for Google Account')
service.addAccountMenu()
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Add account menu failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountAddMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
var type = "com.google"
var account = message.user + "@gmail.com";
log.info('Adding Google Account automatedly')
var version = identity.version.substring(0,3)
function automation() {
switch (version) {
case '2.3': // tested: 2.3.3-2.3.6
return service.pressKey('dpad_down').delay(1000)
.then(function() {
return service.pressKey('dpad_down')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.pressKey('dpad_down')
}).delay(2000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.0': // tested: 4.0.3 and 4.0.4
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.1': // tested: 4.1.1 and 4.1.2
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.2': // tested: 4.2.2
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.3': // tested: 4.3
case '4.4': // tested: 4.4.2
default:
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
}
}
// First check if the account is already added so we don't continue
return checkAccount(type, account)
.then(function() {
push.send([
channel
, reply.fail('Add account failed: account was already added')
])
})
.catch(function() {
return adb.clear(options.serial, 'com.google.android.gsf.login')
.catch(function() {
// The package name is different in 2.3, so let's try the old name
// if the new name fails.
return adb.clear(options.serial, 'com.google.android.gsf')
})
.then(function() {
return service.addAccountMenu()
})
.delay(5000)
.then(function() {
// Just in case the add account menu has any button focused
return touch.tap({x:0, y:0.9})
})
.delay(500)
.then(function() {
return automation()
})
.delay(3000)
.then(function () {
return service.pressKey('home')
})
.then(function () {
return checkAccount(type, account)
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Add account failed', err.stack)
push.send([
channel
, reply.fail('Add account failed: ' + err.message)
])
})
})
})
})

View File

@@ -0,0 +1,122 @@
var util = require('util')
var syrup = require('syrup')
var browsers = require('stf-browser-db')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var mapping = (function() {
var list = Object.create(null)
Object.keys(browsers).forEach(function(id) {
var browser = browsers[id]
if (browser.platforms.android) {
list[browser.platforms.android.package] = id
}
})
return list
})()
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.dependency(require('./service'))
.define(function(options, router, push, adb, service) {
var log = logger.createLogger('device:plugins:browser')
function pkg(component) {
return component.split('/', 1)[0]
}
function processApp(app) {
var packageName = pkg(app.component)
var browserId = mapping[packageName]
if (!browserId) {
throw new Error(util.format('Unmapped browser "%s"', packageName))
}
return {
id: app.component
, type: browserId
, name: browsers[browserId].name
, selected: app.selected
, system: app.system
}
}
function updateBrowsers(data) {
log.info('Updating browser list')
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceBrowserMessage(
options.serial
, data.selected
, data.apps.map(function(app) {
return new wire.DeviceBrowserAppMessage(processApp(app))
})
))
])
}
function loadBrowsers() {
log.info('Loading browser list')
return service.getBrowsers()
.then(updateBrowsers)
}
service.on('browserPackageChange', updateBrowsers)
router.on(wire.BrowserOpenMessage, function(channel, message) {
if (message.browser) {
log.info('Opening "%s" in "%s"', message.url, message.browser)
}
else {
log.info('Opening "%s"', message.url)
}
var reply = wireutil.reply(options.serial)
adb.startActivity(options.serial, {
action: 'android.intent.action.VIEW'
, component: message.browser
, data: message.url
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Browser could not be opened', err.stack)
push.send([
channel
, reply.fail()
])
})
})
router.on(wire.BrowserClearMessage, function(channel, message) {
log.info('Clearing "%s"', message.browser)
var reply = wireutil.reply(options.serial)
adb.clear(options.serial, pkg(message.browser))
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Browser could not be cleared', err.stack)
push.send([
channel
, reply.fail()
])
})
})
return loadBrowsers()
})

View File

@@ -0,0 +1,51 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./service'))
.define(function(options, router, push, service) {
var log = logger.createLogger('device:plugins:clipboard')
router.on(wire.PasteMessage, function(channel, message) {
log.info('Pasting "%s" to clipboard', message.text)
var reply = wireutil.reply(options.serial)
service.paste(message.text)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Paste failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.CopyMessage, function(channel) {
log.info('Copying clipboard contents')
var reply = wireutil.reply(options.serial)
service.copy()
.then(function(content) {
push.send([
channel
, reply.okay(content)
])
})
.catch(function(err) {
log.error('Copy failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,111 @@
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./group'))
.define(function(options, adb, router, push, group) {
var log = logger.createLogger('device:plugins:connect')
, plugin = Object.create(null)
, activeServer = null
plugin.port = options.ports.pop()
plugin.url = util.format('%s:%s', options.publicIp, plugin.port)
plugin.start = function() {
return new Promise(function(resolve, reject) {
if (plugin.isRunning()) {
return resolve(plugin.url)
}
var server = adb.createTcpUsbBridge(options.serial)
server.on('listening', function() {
resolve(plugin.url)
})
server.on('connection', function(conn) {
log.info('New remote ADB connection from %s', conn.remoteAddress)
conn.on('userActivity', function() {
group.keepalive()
})
})
server.on('error', reject)
log.info(util.format('Listening on port %d', plugin.port))
server.listen(plugin.port)
activeServer = server
lifecycle.share('Remote ADB', activeServer)
})
}
plugin.stop = Promise.method(function() {
if (plugin.isRunning()) {
activeServer.close()
activeServer.end()
}
})
plugin.end = Promise.method(function() {
if (plugin.isRunning()) {
activeServer.end()
}
})
plugin.isRunning = function() {
return !!activeServer
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.end)
router
.on(wire.ConnectStartMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.start()
.then(function(url) {
push.send([
channel
, reply.okay(url)
])
})
.catch(function(err) {
log.error('Unable to start remote connect service', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
.on(wire.ConnectStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.end()
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Failed to stop connect service', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
return plugin.start()
.return(plugin)
})

View File

@@ -0,0 +1,20 @@
var syrup = require('syrup')
var deviceData = require('stf-device-db')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./identity'))
.define(function(options, identity) {
var log = logger.createLogger('device:plugins:data')
function find() {
var data = deviceData.find(identity)
if (!data) {
log.warn('Unable to find device data')
}
return data
}
return find()
})

View File

@@ -0,0 +1,25 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('./http'))
.define(function(options, service, http) {
var log = logger.createLogger('device:plugins:display')
function fetch() {
log.info('Fetching display info')
return service.getDisplay(0)
.catch(function() {
log.info('Falling back to HTTP API')
return http.getDisplay(0)
})
.then(function(display) {
display.url = http.getDisplayUrl(display.id)
return display
})
}
return fetch()
})

View File

@@ -0,0 +1,237 @@
var net = require('net')
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var split = require('split')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var streamutil = require('../../../util/streamutil')
var lifecycle = require('../../../util/lifecycle')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../resources/remote'))
.define(function(options, adb, router, push, remote) {
var log = logger.createLogger('device:plugins:forward')
var service = {
port: 2810
, privatePorts: (function() {
var ports = []
for (var i = 2520; i <= 2540; ++i) {
ports.push(i)
}
return ports
})()
, forwards: Object.create(null)
}
function openService() {
log.info('Launching reverse port forwarding service')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-forward', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Forward shell', out)
streamutil.talk(log, 'Forward shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(10000)
})
.then(function(conn) {
conn.end()
})
})
}
function createForward(data) {
log.info(
'Reverse port forwarding port %d to %s:%d'
, data.devicePort
, data.targetHost
, data.targetPort
)
var forward = service.forwards[data.devicePort]
if (forward) {
if (forward.targetHost === data.targetHost &&
forward.targetPort === data.targetPort) {
return Promise.resolve()
}
else if (forward.system) {
return Promise.reject(new Error('Cannot rebind system port'))
}
else {
removeForward(forward)
}
}
return adb.openTcp(options.serial, service.port)
.timeout(10000)
.then(function(conn) {
var resolver = Promise.defer()
var forward = {
devicePort: data.devicePort
, targetHost: data.targetHost
, targetPort: data.targetPort
, system: !!data.system
, privatePort: service.privatePorts.pop()
, connection: conn
}
var parser = conn.pipe(split())
parser.on('data', function(chunk) {
var cmd = chunk.toString().trim()
switch (cmd) {
case 'OKAY':
resolver.resolve(forward)
break
case 'FAIL':
resolver.reject(new Error('Remote replied with FAIL'))
break
case 'CNCT':
adb.openTcp(options.serial, forward.privatePort)
.done(function(dstream) {
return tryConnect(forward)
.then(function(ustream) {
ustream.pipe(dstream)
dstream.pipe(ustream)
})
})
break
}
})
// Keep this around
function endListener() {
removeForward(forward)
}
conn.on('end', endListener)
conn.write(util.format(
'FRWD %d %d\n'
, forward.devicePort
, forward.privatePort
))
return resolver.promise
})
}
function removeForward(data) {
log.info('Removing reverse port forwarding on port %d', data.devicePort)
var forward = service.forwards[data.devicePort]
if (forward) {
forward.connection.end()
delete service.forwards[data.devicePort]
}
}
function tryConnect(data) {
var resolver = Promise.defer()
var conn = net.connect({
host: data.targetHost
, port: data.targetPort
})
function connectListener() {
resolver.resolve(conn)
}
function errorListener(err) {
resolver.reject(err)
}
conn.on('connect', connectListener)
conn.on('error', errorListener)
return resolver.promise.finally(function() {
conn.removeListener('connect', connectListener)
conn.removeListener('error', errorListener)
})
}
function resetForwards() {
Object.keys(service.forwards).forEach(function(privatePort) {
service.forwards[privatePort].connection.end()
delete service.forwards[privatePort]
})
}
function listForwards() {
return Object.keys(service.forwards).map(function(privatePort) {
var forward = service.forwards[privatePort]
return {
devicePort: forward.devicePort
, targetHost: forward.targetHost
, targetPort: forward.targetPort
, system: !!forward.system
}
})
}
return openService()
.then(function() {
router
.on(wire.ForwardTestMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
tryConnect(message)
.then(function(conn) {
conn.end()
push.send([
channel
, reply.okay('success')
])
})
.catch(function() {
push.send([
channel
, reply.fail('fail_connect')
])
})
})
.on(wire.ForwardCreateMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
createForward(message)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Reverse port forwarding failed', err.stack)
push.send([
channel
, reply.fail('fail_forward')
])
})
})
.on(wire.ForwardRemoveMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
removeForward(message)
push.send([
channel
, reply.okay('success')
])
})
})
})

View File

@@ -0,0 +1,170 @@
var events = require('events')
var Promise = require('bluebird')
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var grouputil = require('../../../util/grouputil')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('./solo'))
.dependency(require('./identity'))
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/sub'))
.dependency(require('../support/channels'))
.define(function(options, solo, ident, service, router, push, sub, channels) {
var log = logger.createLogger('device:plugins:group')
, currentGroup = null
, plugin = new events.EventEmitter()
plugin.get = Promise.method(function() {
if (!currentGroup) {
throw new grouputil.NoGroupError()
}
return currentGroup
})
plugin.join = function(newGroup, timeout) {
return plugin.get()
.then(function() {
if (currentGroup.group !== newGroup.group) {
throw new grouputil.AlreadyGroupedError()
}
return currentGroup
})
.catch(grouputil.NoGroupError, function() {
currentGroup = newGroup
log.important('Now owned by "%s"', currentGroup.email)
log.info('Subscribing to group channel "%s"', currentGroup.group)
channels.register(currentGroup.group, {
timeout: timeout || options.groupTimeout
, alias: solo.channel
})
sub.subscribe(currentGroup.group)
push.send([
wireutil.global
, wireutil.envelope(new wire.JoinGroupMessage(
options.serial
, currentGroup
))
])
plugin.emit('join', currentGroup)
return currentGroup
})
}
plugin.keepalive = function() {
if (currentGroup) {
channels.keepalive(currentGroup.group)
}
}
plugin.leave = function(reason) {
return plugin.get()
.then(function(group) {
log.important('No longer owned by "%s"', group.email)
log.info('Unsubscribing from group channel "%s"', group.group)
channels.unregister(group.group)
sub.unsubscribe(group.group)
push.send([
wireutil.global
, wireutil.envelope(new wire.LeaveGroupMessage(
options.serial
, group
, reason
))
])
currentGroup = null
plugin.emit('leave', group)
return group
})
}
plugin.on('join', function() {
service.acquireWakeLock()
service.unlock()
})
plugin.on('leave', function() {
service.releaseWakeLock()
service.lock()
})
router
.on(wire.GroupMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
grouputil.match(ident, message.requirements)
.then(function() {
return plugin.join(message.owner, message.timeout)
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(grouputil.RequirementMismatchError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
.catch(grouputil.AlreadyGroupedError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
})
.on(wire.UngroupMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
grouputil.match(ident, message.requirements)
.then(function() {
return plugin.leave('ungroup_request')
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(grouputil.NoGroupError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
})
channels.on('timeout', function(channel) {
if (currentGroup && channel === currentGroup.group) {
plugin.leave('automatic_timeout')
}
})
lifecycle.observe(function() {
return plugin.leave('device_absent')
.catch(grouputil.NoGroupError, function() {
return true
})
})
return plugin
})

View File

@@ -0,0 +1,143 @@
var util = require('util')
var assert = require('assert')
var http = require('http')
var Promise = require('bluebird')
var syrup = require('syrup')
var request = Promise.promisifyAll(require('request'))
var httpProxy = require('http-proxy')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../resources/remote'))
.define(function(options, adb, remote) {
var log = logger.createLogger('device:plugins:http')
var service = {
port: 2870
, privateUrl: null
, publicUrl: null
}
function openService() {
log.info('Launching HTTP API')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-http', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Remote shell', out)
streamutil.talk(log, 'Remote shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(20000)
})
.then(function(conn) {
var ours = options.ports.pop()
, everyones = options.ports.pop()
, url = util.format('http://127.0.0.1:%d', ours)
// Don't need the connection
conn.end()
log.info('Opening device HTTP API forwarder on "%s"', url)
service.privateUrl = url
service.publicUrl = util.format(
'http://%s:%s'
, options.publicIp
, everyones
)
return adb.forward(
options.serial
, util.format('tcp:%d', ours)
, util.format('tcp:%d', service.port)
)
.timeout(10000)
.then(function() {
log.info(
'Opening HTTP API proxy on "http://%s:%s"'
, options.publicIp
, everyones
)
var resolver = Promise.defer()
function resolve() {
lifecycle.share('Proxy server', proxyServer, {
end: false
})
resolver.resolve()
}
function reject(err) {
resolver.reject(err)
}
var proxy = httpProxy.createProxyServer({
target: url
, ws: false
, xfwd: false
})
var proxyServer = http.createServer(proxy.web)
.listen(everyones)
proxyServer.on('listening', resolve)
proxyServer.on('error', reject)
return resolver.promise.finally(function() {
proxyServer.removeListener('listening', resolve)
proxyServer.removeListener('error', reject)
})
})
})
})
}
return openService()
.then(function() {
return {
getDisplay: function(id) {
return request.getAsync({
url: util.format(
'%s/api/v1/displays/%d'
, service.privateUrl
, id
)
, json: true
})
.timeout(10000)
.then(function(args) {
var display = args[1]
assert.ok('id' in display, 'Invalid response from HTTP API')
// Fix rotation's old name
if ('orientation' in display) {
display.rotation = display.orientation
delete display.orientation
}
return display
})
}
, getDisplayUrl: function(id) {
return util.format(
'%s/api/v1/displays/%d/screenshot.jpg'
, service.publicUrl
, id
)
}
}
})
})

View File

@@ -0,0 +1,22 @@
var syrup = require('syrup')
var devutil = require('../../../util/devutil')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('../support/properties'))
.dependency(require('./display'))
.dependency(require('./phone'))
.define(function(options, properties, display, phone) {
var log = logger.createLogger('device:plugins:identity')
function solve() {
log.info('Solving identity')
var identity = devutil.makeIdentity(options.serial, properties)
identity.display = display
identity.phone = phone
return identity
}
return solve()
})

View File

@@ -0,0 +1,189 @@
var stream = require('stream')
var url = require('url')
var util = require('util')
var syrup = require('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')
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)
, 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
, end = 90
, guesstimate = start
sendProgress('installing_app', guesstimate)
return promiseutil.periodicNotify(
adb.installRemote(options.serial, apk)
.timeout(60000)
.catch(function(err) {
switch (err.code) {
case 'INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES':
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)
})
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 launchActivity = {
action: 'android.intent.action.MAIN'
, component: util.format(
'%s/%s'
, pkg
, manifest.application.launcherActivities[0].name
)
, 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(15000)
}
}
})
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Installation of package "%s" failed', pkg, err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
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')
])
})
})
})

View File

@@ -0,0 +1,141 @@
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./group'))
.define(function(options, adb, router, push, group) {
var log = logger.createLogger('device:plugins:logcat')
, plugin = Object.create(null)
, activeLogcat = null
plugin.start = function(filters) {
return group.get()
.then(function(group) {
return plugin.stop()
.then(function() {
log.info('Starting logcat')
return adb.openLogcat(options.serial, {
clear: true
})
})
.timeout(10000)
.then(function(logcat) {
activeLogcat = logcat
function entryListener(entry) {
push.send([
group.group
, wireutil.envelope(new wire.DeviceLogcatEntryMessage(
options.serial
, entry.date.getTime() / 1000
, entry.pid
, entry.tid
, entry.priority
, entry.tag
, entry.message
))
])
}
logcat.on('entry', entryListener)
return plugin.reset(filters)
})
})
}
plugin.stop = Promise.method(function() {
if (plugin.isRunning()) {
log.info('Stopping logcat')
activeLogcat.end()
activeLogcat = null
}
})
plugin.reset = Promise.method(function(filters) {
if (plugin.isRunning()) {
activeLogcat
.resetFilters()
if (filters.length) {
activeLogcat.excludeAll()
filters.forEach(function(filter) {
activeLogcat.include(filter.tag, filter.priority)
})
}
}
else {
throw new Error('Logcat is not running')
}
})
plugin.isRunning = function() {
return !!activeLogcat
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.stop)
router
.on(wire.LogcatStartMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
plugin.start(message.filters)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Unable to open logcat', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
.on(wire.LogcatApplyFiltersMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
plugin.reset(message.filters)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Failed to apply logcat filters', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
.on(wire.LogcatStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.stop()
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Failed to stop logcat', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
return plugin
})

View File

@@ -0,0 +1,27 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/push'))
.define(function(options, push) {
// Forward all logs
logger.on('entry', function(entry) {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceLogMessage(
options.serial
, entry.timestamp / 1000
, entry.priority
, entry.tag
, entry.pid
, entry.message
, entry.identifier
))
])
})
return logger
})

View File

@@ -0,0 +1,21 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./service'))
.define(function(options, service) {
var log = logger.createLogger('device:plugins:phone')
function fetch() {
log.info('Fetching phone info')
return service.getProperties([
'imei'
, 'phoneNumber'
, 'iccid'
, 'network'
])
}
return fetch()
})

View File

@@ -0,0 +1,35 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
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:reboot')
router.on(wire.RebootMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.important('Rebooting')
adb.reboot(options.serial)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.error(function(err) {
log.error('Reboot failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,57 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:ringer')
router.on(wire.RingerSetMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Setting ringer mode to mode "%s"', message.mode)
service.setRingerMode(message.mode)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Setting ringer mode failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.RingerGetMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.info('Getting ringer mode')
service.getRingerMode()
.timeout(30000)
.then(function(mode) {
push.send([
channel
, reply.okay('success', mode)
])
})
.catch(function(err) {
log.error('Getting ringer mode failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,66 @@
var http = require('http')
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/storage'))
.dependency(require('./display'))
.define(function(options, router, push, storage, display) {
var log = logger.createLogger('device:plugins:screenshot')
var plugin = Object.create(null)
plugin.capture = function() {
log.info('Capturing screenshot from %s', display.url)
return new Promise(function(resolve, reject) {
var req = http.get(display.url)
function responseListener(res) {
if (res.statusCode !== 200) {
reject(new Error(util.format(
'Screenshot capture failed: HTTP %d'
, res.statusCode
)))
}
else {
resolve(storage.store('image', res, {
filename: util.format('%s.jpg', options.serial)
, contentType: 'image/jpeg'
, knownLength: +res.headers['content-length']
}))
}
}
req.on('response', responseListener)
req.on('error', reject)
})
}
router.on(wire.ScreenCaptureMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.capture()
.then(function(file) {
push.send([
channel
, reply.okay('success', file)
])
})
.catch(function(err) {
log.error('Screen capture failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
return plugin
})

View File

@@ -0,0 +1,33 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:sd')
router.on(wire.SdStatusMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Getting SD card status')
service.getSdStatus(message)
.timeout(30000)
.then(function(mounted) {
push.send([
channel
, reply.okay(mounted ? 'sd_mounted' : 'sd_unmounted')
])
})
.catch(function(err) {
log.error('Getting SD card Status', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,695 @@
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)
})

View File

@@ -0,0 +1,86 @@
var Promise = require('bluebird')
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/sub'))
.define(function(options, adb, router, push, sub) {
var log = logger.createLogger('device:plugins:shell')
router.on(wire.ShellCommandMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Running shell command "%s"', message.command)
adb.shell(options.serial, message.command)
.timeout(10000)
.then(function(stream) {
var resolver = Promise.defer()
, timer
function keepAliveListener(channel, message) {
clearTimeout(timer)
timer = setTimeout(forceStop, message.timeout)
}
function readableListener() {
var chunk
while ((chunk = stream.read())) {
push.send([
channel
, reply.progress(chunk)
])
}
}
function endListener() {
push.send([
channel
, reply.okay(null)
])
resolver.resolve()
}
function errorListener(err) {
resolver.reject(err)
}
function forceStop() {
stream.end()
}
stream.setEncoding('utf8')
stream.on('readable', readableListener)
stream.on('end', endListener)
stream.on('error', errorListener)
sub.subscribe(channel)
router.on(wire.ShellKeepAliveMessage, keepAliveListener)
timer = setTimeout(forceStop, message.timeout)
return resolver.promise.finally(function() {
stream.removeListener('readable', readableListener)
stream.removeListener('end', endListener)
stream.removeListener('error', errorListener)
sub.unsubscribe(channel)
router.removeListener(wire.ShellKeepAliveMessage, keepAliveListener)
clearTimeout(timer)
})
})
.error(function(err) {
log.error('Shell command "%s" failed', message.command, err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,50 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/sub'))
.dependency(require('../support/push'))
.dependency(require('../support/router'))
.dependency(require('./identity'))
.define(function(options, sub, push, router, identity) {
var log = logger.createLogger('device:plugins:solo')
var channel = wireutil.makePrivateChannel()
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
router.on(wire.ProbeMessage, function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceIdentityMessage(
options.serial
, identity.platform
, identity.manufacturer
, identity.operator
, identity.model
, identity.version
, identity.abi
, identity.sdk
, new wire.DeviceDisplayMessage(identity.display)
, new wire.DevicePhoneMessage(identity.phone)
, identity.product
))
])
})
return {
channel: channel
, poke: function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DevicePokeMessage(
options.serial
, channel
))
])
}
}
})

View File

@@ -0,0 +1,46 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../resources/remote'))
.define(function(options, adb, remote) {
var log = logger.createLogger('device:plugins:stats')
var service = {
port: 2830
}
function openService() {
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-stats', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Stats shell', out)
streamutil.talk(log, 'Stats shell says: "%s"', out)
})
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(15000)
})
.then(function(conn) {
return lifecycle.share('Stats connection', conn)
})
}
return openService()
.then(function() {
return {}
})
})

View File

@@ -0,0 +1,40 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.define(function(options, router, push, adb) {
var log = logger.createLogger('device:plugins:store')
router.on(wire.StoreOpenMessage, function(channel) {
log.info('Opening Play Store')
var reply = wireutil.reply(options.serial)
adb.startActivity(options.serial, {
action: 'android.intent.action.MAIN'
, component: 'com.android.vending/.AssetBrowserActivity'
// FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
// FLAG_ACTIVITY_BROUGHT_TO_FRONT
// FLAG_ACTIVITY_NEW_TASK
, flags: 0x10600000
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Play Store could not be opened', err.stack)
push.send([
channel
, reply.fail()
])
})
})
})

View File

@@ -0,0 +1,142 @@
var Promise = require('bluebird')
var syrup = require('syrup')
var monkey = require('adbkit-monkey')
var wire = require('../../../wire')
var devutil = require('../../../util/devutil')
var logger = require('../../../util/logger')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
var SeqQueue = require('../../../wire/seqqueue')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../resources/remote'))
.dependency(require('./display'))
.dependency(require('./data'))
.define(function(options, adb, router, remote, display, data) {
var log = logger.createLogger('device:plugins:touch')
var plugin = Object.create(null)
var service = {
port: 2820
}
function openService() {
log.info('Launching touch service')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-input', service.port
])
.timeout(10000)
})
.then(function(out) {
lifecycle.share('Touch shell', out)
streamutil.talk(log, 'Touch shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(15000)
})
.then(function(conn) {
return Promise.promisifyAll(monkey.connectStream(conn))
})
.then(function(monkey) {
return lifecycle.share('Touch monkey', monkey)
})
}
function modifyCoords(message) {
message.x = Math.floor(message.x * display.width)
message.y = Math.floor(message.y * display.height)
}
return openService()
.then(function(monkey) {
var queue = new SeqQueue()
, pressure = (data && data.touch && data.touch.defaultPressure) || 50
log.info('Setting default pressure to %d', pressure)
plugin.touchDown = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch down'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchDown failed', err.stack)
})
}
plugin.touchMove = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch move'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchMove failed', err.stack)
})
}
plugin.touchUp = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch up'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchUp failed', err.stack)
})
}
plugin.tap = function(point) {
modifyCoords(point)
monkey.sendAsync([
'tap'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('tap failed', err.stack)
})
}
router
.on(wire.TouchDownMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchDown(message)
})
})
.on(wire.TouchMoveMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchMove(message)
})
})
.on(wire.TouchUpMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchUp(message)
})
// Reset queue
queue = new SeqQueue()
})
.on(wire.TapMessage, function(channel, message) {
plugin.tap(message)
})
})
.return(plugin)
})

View File

@@ -0,0 +1,53 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:wifi')
router.on(wire.WifiSetEnabledMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Setting Wifi "%s"', message.enabled)
service.setWifiEnabled(message.enabled)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Setting Wifi enabled failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.WifiGetStatusMessage, function(channel){
var reply = wireutil.reply(options.serial)
log.info('Getting Wifi status')
service.getWifiStatus()
.timeout(30000)
.then(function(enabled) {
push.send([
channel
, reply.okay(enabled ? 'wifi_enabled' : 'wifi_disabled')
])
})
.catch(function(err) {
log.error('Getting Wifi status failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})