1
0
Fork 0
mirror of https://github.com/openstf/stf synced 2025-10-05 19:42:01 +02:00

Refactor InputAgent into STFService. Support copypasting (only pasting implemented in the UI right now though).

This commit is contained in:
Simo Kinnunen 2014-04-01 11:47:20 +09:00
parent 514fc3554d
commit 09eb8c539d
13 changed files with 379 additions and 50 deletions

View file

@ -347,6 +347,24 @@ module.exports = function(options) {
]) ])
}) })
// Transactions // Transactions
.on('clipboard.paste', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.envelope(new wire.PasteMessage(
data.text
))
])
})
.on('clipboard.copy', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.envelope(new wire.CopyMessage(
data.text
))
])
})
.on('device.identify', function(channel, responseChannel) { .on('device.identify', function(channel, responseChannel) {
push.send([ push.send([
channel channel

View file

@ -18,6 +18,7 @@ module.exports = function(options) {
.dependency(require('./device/plugins/display')) .dependency(require('./device/plugins/display'))
.dependency(require('./device/plugins/http')) .dependency(require('./device/plugins/http'))
.dependency(require('./device/plugins/input')) .dependency(require('./device/plugins/input'))
.dependency(require('./device/plugins/clipboard'))
.dependency(require('./device/plugins/logcat')) .dependency(require('./device/plugins/logcat'))
.dependency(require('./device/plugins/shell')) .dependency(require('./device/plugins/shell'))
.dependency(require('./device/plugins/touch')) .dependency(require('./device/plugins/touch'))

View file

@ -0,0 +1,70 @@
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('./input'))
.define(function(options, router, push, input) {
var log = logger.createLogger('device:plugins:clipboard')
router.on(wire.PasteMessage, function(channel, message) {
log.info('Pasting "%s" to clipboard', message.text)
var seq = 0
input.paste(message.text)
.then(function() {
push.send([
channel
, wireutil.envelope(new wire.TransactionDoneMessage(
options.serial
, seq++
, true
))
])
})
.catch(function(err) {
log.error('Paste failed', err.stack)
push.send([
channel
, wireutil.envelope(new wire.TransactionDoneMessage(
options.serial
, seq++
, false
, err.message
))
])
})
})
router.on(wire.CopyMessage, function(channel) {
log.info('Copying clipboard contents')
var seq = 0
input.copy()
.then(function(content) {
push.send([
channel
, wireutil.envelope(new wire.TransactionDoneMessage(
options.serial
, seq++
, true
, content
))
])
})
.catch(function(err) {
log.error('Copy failed', err.stack)
push.send([
channel
, wireutil.envelope(new wire.TransactionDoneMessage(
options.serial
, seq++
, false
, err.message
))
])
})
})
})

View file

@ -2,7 +2,7 @@ var util = require('util')
var syrup = require('syrup') var syrup = require('syrup')
var split = require('split') var split = require('split')
var ByteBuffer = require('protobufjs/node_modules/bytebuffer') var Promise = require('bluebird')
var wire = require('../../../wire') var wire = require('../../../wire')
var wireutil = require('../../../wire/util') var wireutil = require('../../../wire/util')
@ -10,26 +10,29 @@ var devutil = require('../../../util/devutil')
var keyutil = require('../../../util/keyutil') var keyutil = require('../../../util/keyutil')
var streamutil = require('../../../util/streamutil') var streamutil = require('../../../util/streamutil')
var logger = require('../../../util/logger') var logger = require('../../../util/logger')
var ms = require('../../../wire/messagestream')
module.exports = syrup.serial() module.exports = syrup.serial()
.dependency(require('../support/adb')) .dependency(require('../support/adb'))
.dependency(require('../support/router')) .dependency(require('../support/router'))
.dependency(require('../support/push')) .dependency(require('../support/push'))
.dependency(require('../support/quit')) .dependency(require('../support/quit'))
.dependency(require('../resources/inputagent')) .dependency(require('../resources/service'))
.define(function(options, adb, router, push, quit, apk) { .define(function(options, adb, router, push, quit, apk) {
var log = logger.createLogger('device:plugins:input') var log = logger.createLogger('device:plugins:input')
var serviceQueue = []
var agent = { var agent = {
socket: null socket: null
, writer: null
, port: 1090 , port: 1090
} }
var service = { var service = {
socket: null socket: null
, writer: null
, reader: null
, port: 1100 , port: 1100
, startAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_START'
, stopAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_STOP'
} }
function openAgent() { function openAgent() {
@ -40,8 +43,7 @@ module.exports = syrup.serial()
}) })
.then(function() { .then(function() {
return adb.shell(options.serial, util.format( return adb.shell(options.serial, util.format(
"export CLASSPATH='%s';" + "export CLASSPATH='%s'; exec app_process /system/bin '%s'"
" exec app_process /system/bin '%s'"
, apk.path , apk.path
, apk.main , apk.main
)) ))
@ -65,6 +67,8 @@ module.exports = syrup.serial()
}) })
.then(function(conn) { .then(function(conn) {
agent.socket = conn agent.socket = conn
agent.writer = new ms.DelimitingStream()
agent.writer.pipe(conn)
conn.on('error', function(err) { conn.on('error', function(err) {
log.fatal('InputAgent socket had an error', err.stack) log.fatal('InputAgent socket had an error', err.stack)
quit.fatal() quit.fatal()
@ -137,62 +141,166 @@ module.exports = syrup.serial()
return devutil.waitForPortToFree(adb, options.serial, service.port) return devutil.waitForPortToFree(adb, options.serial, service.port)
}) })
.then(function() { .then(function() {
return callService(util.format("-a '%s'", service.startAction)) return callService(util.format("-a '%s'", apk.startAction))
}) })
.then(function() { .then(function() {
return devutil.waitForPort(adb, options.serial, service.port) return devutil.waitForPort(adb, options.serial, service.port)
}) })
.then(function(conn) { .then(function(conn) {
service.socket = conn service.socket = conn
service.reader = conn.pipe(new ms.DelimitedStream())
service.reader.on('data', function(data) {
if (serviceQueue.length) {
var resolver = serviceQueue.shift()
resolver.resolve(data)
}
else {
log.warn('Unexpected data from service', data)
}
})
service.writer = new ms.DelimitingStream()
service.writer.pipe(conn)
conn.on('error', function(err) { conn.on('error', function(err) {
log.fatal('InputService socket had an error', err.stack) log.fatal('InputService socket had an error', err.stack)
quit.fatal() quit.fatal()
}) })
conn.on('end', function() { .on('end', function() {
log.fatal('InputService socket ended') log.fatal('InputService socket ended')
quit.fatal() quit.fatal()
}) })
}) })
} }
function stopService() { function stopService() {
return callService(util.format("-a '%s'", service.stopAction)) return callService(util.format("-a '%s'", apk.stopAction))
} }
function sendInputEvent(event) { function sendInputEvent(event) {
var lengthBuffer = new ByteBuffer() agent.writer.write(new apk.agentProto.InputEvent(event).encodeNB())
, messageBuffer = new apk.proto.InputEvent(event).encode() }
// Delimiter function version() {
lengthBuffer.writeVarint32(messageBuffer.length) return runServiceCommand(
apk.serviceProto.RequestType.VERSION
agent.socket.write(Buffer.concat([ , new apk.serviceProto.VersionRequest()
lengthBuffer.toBuffer() )
, messageBuffer.toBuffer() .then(function(data) {
])) var response = apk.serviceProto.VersionResponse.decode(data)
if (response.success) {
return response.version
}
throw new Error('Unable to retrieve version')
})
} }
function unlock() { function unlock() {
service.socket.write('unlock\n') return runServiceCommand(
apk.serviceProto.RequestType.SET_KEYGUARD_STATE
, new apk.serviceProto.SetKeyguardStateRequest(false)
)
.then(function(data) {
var response = apk.serviceProto.SetKeyguardStateResponse.decode(data)
if (!response.success) {
throw new Error('Unable to unlock device')
}
})
} }
function lock() { function lock() {
service.socket.write('lock\n') return runServiceCommand(
apk.serviceProto.RequestType.SET_KEYGUARD_STATE
, new apk.serviceProto.SetKeyguardStateRequest(true)
)
.then(function(data) {
var response = apk.serviceProto.SetKeyguardStateResponse.decode(data)
if (!response.success) {
throw new Error('Unable to lock device')
}
})
} }
function acquireWakeLock() { function acquireWakeLock() {
service.socket.write('acquire wake lock\n') return runServiceCommand(
apk.serviceProto.RequestType.SET_WAKE_LOCK
, new apk.serviceProto.SetWakeLockRequest(true)
)
.then(function(data) {
var response = apk.serviceProto.SetWakeLockResponse.decode(data)
if (!response.success) {
throw new Error('Unable to acquire WakeLock')
}
})
} }
function releaseWakeLock() { function releaseWakeLock() {
service.socket.write('release wake lock\n') return runServiceCommand(
apk.serviceProto.RequestType.SET_WAKE_LOCK
, new apk.serviceProto.SetWakeLockRequest(false)
)
.then(function(data) {
var response = apk.serviceProto.SetWakeLockResponse.decode(data)
if (!response.success) {
throw new Error('Unable to release WakeLock')
}
})
} }
function identity() { function identity() {
service.socket.write(util.format( return runServiceCommand(
'show identity %s\n' apk.serviceProto.RequestType.IDENTIFY
, options.serial , new apk.serviceProto.IdentifyRequest(options.serial)
)) )
.then(function(data) {
var response = apk.serviceProto.IdentifyResponse.decode(data)
if (!response.success) {
throw new Error('Unable to identify device')
}
})
}
function setClipboard(text) {
return runServiceCommand(
apk.serviceProto.RequestType.SET_CLIPBOARD
, new apk.serviceProto.SetClipboardRequest(
apk.serviceProto.ClipboardType.TEXT
, text
)
)
.then(function(data) {
var response = apk.serviceProto.SetClipboardResponse.decode(data)
if (!response.success) {
throw new Error('Unable to set clipboard')
}
})
}
function getClipboard() {
return runServiceCommand(
apk.serviceProto.RequestType.GET_CLIPBOARD
, new apk.serviceProto.GetClipboardRequest(
apk.serviceProto.ClipboardType.TEXT
)
)
.then(function(data) {
var response = apk.serviceProto.GetClipboardResponse.decode(data)
if (response.success) {
switch (response.type) {
case apk.serviceProto.ClipboardType.TEXT:
return response.text
}
}
throw new Error('Unable to get clipboard')
})
}
function runServiceCommand(type, cmd) {
var resolver = Promise.defer()
service.writer.write(new apk.serviceProto.RequestEnvelope(
type
, cmd.encodeNB()
).encodeNB())
serviceQueue.push(resolver)
return resolver.promise
} }
return openAgent() return openAgent()
@ -242,6 +350,17 @@ module.exports = syrup.serial()
, acquireWakeLock: acquireWakeLock , acquireWakeLock: acquireWakeLock
, releaseWakeLock: releaseWakeLock , releaseWakeLock: releaseWakeLock
, identity: identity , identity: identity
, paste: function(text) {
return setClipboard(text)
.then(function() {
sendInputEvent({
action: 2
, keyCode: adb.Keycode.KEYCODE_V
, ctrlKey: true
})
})
}
, copy: getClipboard
} }
}) })
}) })

View file

@ -11,16 +11,21 @@ var logger = require('../../../util/logger')
module.exports = syrup.serial() module.exports = syrup.serial()
.dependency(require('../support/adb')) .dependency(require('../support/adb'))
.define(function(options, adb) { .define(function(options, adb) {
var log = logger.createLogger('device:resources:inputagent') var log = logger.createLogger('device:resources:service')
var resource = { var resource = {
requiredVersion: '~0.2.0' requiredVersion: '~0.2.0'
, pkg: 'jp.co.cyberagent.stf.input.agent' , pkg: 'jp.co.cyberagent.stf'
, main: 'jp.co.cyberagent.stf.input.agent.InputAgent' , main: 'jp.co.cyberagent.stf.InputAgent'
, apk: pathutil.vendor('InputAgent/InputAgent.apk') , apk: pathutil.vendor('STFService/STFService.apk')
, proto: ProtoBuf.loadProtoFile( , agentProto: ProtoBuf.loadProtoFile(
pathutil.vendor('InputAgent/proto/agent.proto') pathutil.vendor('STFService/proto/agent.proto')
).build().jp.co.cyberagent.stf.input.agent.proto ).build().jp.co.cyberagent.stf.proto
, serviceProto: ProtoBuf.loadProtoFile(
pathutil.vendor('STFService/proto/service.proto')
).build().jp.co.cyberagent.stf.proto
, startAction: 'jp.co.cyberagent.stf.ACTION_START'
, stopAction: 'jp.co.cyberagent.stf.ACTION_STOP'
} }
function getPath() { function getPath() {
@ -34,7 +39,7 @@ module.exports = syrup.serial()
} }
function install() { function install() {
log.info('Checking whether we need to install InputAgent.apk') log.info('Checking whether we need to install STFService')
return getPath() return getPath()
.then(function(installedPath) { .then(function(installedPath) {
log.info('Running version check') log.info('Running version check')
@ -49,6 +54,10 @@ module.exports = syrup.serial()
.timeout(10000) .timeout(10000)
.then(function(buffer) { .then(function(buffer) {
var version = buffer.toString() var version = buffer.toString()
throw new Error(util.format(
'Incompatible version %s'
, version
))
if (semver.satisfies(version, resource.requiredVersion)) { if (semver.satisfies(version, resource.requiredVersion)) {
return installedPath return installedPath
} }
@ -62,7 +71,7 @@ module.exports = syrup.serial()
}) })
}) })
.catch(function() { .catch(function() {
log.info('Installing InputAgent.apk') log.info('Installing STFService')
return adb.install(options.serial, resource.apk) return adb.install(options.serial, resource.apk)
.then(function() { .then(function() {
return getPath() return getPath()
@ -72,12 +81,8 @@ module.exports = syrup.serial()
return install() return install()
.then(function(path) { .then(function(path) {
log.info('InputAgent.apk up to date') log.info('STFService up to date')
return { resource.path = path
path: path return resource
, pkg: resource.pkg
, main: resource.main
, proto: resource.proto
}
}) })
}) })

View file

@ -9,6 +9,7 @@ module.exports = syrup.serial()
.define(function(options) { .define(function(options) {
var log = logger.createLogger('device:support:adb') var log = logger.createLogger('device:support:adb')
var adb = adbkit.createClient() var adb = adbkit.createClient()
adb.Keycode = adbkit.Keycode
function ensureBootComplete() { function ensureBootComplete() {
return promiseutil.periodicNotify( return promiseutil.periodicNotify(

View file

@ -1,6 +1,7 @@
// Message wrapper // Message wrapper
enum MessageType { enum MessageType {
CopyMessage = 33;
DeviceAbsentMessage = 1; DeviceAbsentMessage = 1;
DeviceHeartbeatMessage = 28; DeviceHeartbeatMessage = 28;
DeviceIdentityMessage = 2; DeviceIdentityMessage = 2;
@ -21,6 +22,7 @@ enum MessageType {
LaunchActivityMessage = 31; LaunchActivityMessage = 31;
LeaveGroupMessage = 15; LeaveGroupMessage = 15;
LogcatApplyFiltersMessage = 16; LogcatApplyFiltersMessage = 16;
PasteMessage = 32;
ProbeMessage = 17; ProbeMessage = 17;
ShellCommandMessage = 18; ShellCommandMessage = 18;
ShellKeepAliveMessage = 19; ShellKeepAliveMessage = 19;
@ -216,6 +218,13 @@ message TypeMessage {
required string text = 1; required string text = 1;
} }
message PasteMessage {
required string text = 1;
}
message CopyMessage {
}
enum KeyCode { enum KeyCode {
HOME = 3; HOME = 3;
BACK = 4; BACK = 4;

View file

@ -78,6 +78,20 @@ module.exports = function ControlServiceFactory(
}) })
} }
this.paste = function(text) {
var tx = TransactionService.create(target)
socket.emit('clipboard.paste', channel, tx.channel, {
text: text
})
return tx
}
this.copy = function() {
var tx = TransactionService.create(target)
socket.emit('clipboard.copy', channel, tx.channel)
return tx
}
this.shell = function(command) { this.shell = function(command) {
var tx = TransactionService.create(target) var tx = TransactionService.create(target)
socket.emit('shell.command', channel, tx.channel, { socket.emit('shell.command', channel, tx.channel, {

View file

@ -86,7 +86,7 @@ module.exports = function DeviceScreenDirective($document, ScalingService) {
function pasteListener(e) { function pasteListener(e) {
e.preventDefault() // no need to change value e.preventDefault() // no need to change value
scope.control.type(e.clipboardData.getData('text/plain')) scope.control.paste(e.clipboardData.getData('text/plain'))
} }
function maybeLoadScreen() { function maybeLoadScreen() {

Binary file not shown.

BIN
vendor/STFService/STFService.apk vendored Normal file

Binary file not shown.

View file

@ -1,4 +1,4 @@
package jp.co.cyberagent.stf.input.agent.proto; package jp.co.cyberagent.stf.proto;
option java_outer_classname = "AgentProto"; option java_outer_classname = "AgentProto";

92
vendor/STFService/proto/service.proto vendored Normal file
View file

@ -0,0 +1,92 @@
package jp.co.cyberagent.stf.proto;
option java_outer_classname = "ServiceProto";
enum RequestType {
VERSION = 0;
SET_KEYGUARD_STATE = 1;
SET_WAKE_LOCK = 2;
SET_CLIPBOARD = 3;
GET_CLIPBOARD = 4;
GET_PROPERTIES = 6;
IDENTIFY = 7;
}
message RequestEnvelope {
required RequestType type = 1;
required bytes request = 2;
}
message ResponseEnvelope {
required bool success = 1;
required bytes response = 2;
}
message VersionRequest {
}
message VersionResponse {
required bool success = 1;
optional string version = 2;
}
message SetKeyguardStateRequest {
required bool enabled = 1;
}
message SetKeyguardStateResponse {
required bool success = 1;
}
message SetWakeLockRequest {
required bool enabled = 1;
}
message SetWakeLockResponse {
required bool success = 1;
}
enum ClipboardType {
TEXT = 1;
}
message SetClipboardRequest {
required ClipboardType type = 1;
optional string text = 2;
}
message SetClipboardResponse {
required bool success = 1;
}
message GetClipboardRequest {
required ClipboardType type = 1;
}
message GetClipboardResponse {
required bool success = 1;
optional ClipboardType type = 2;
optional string text = 3;
}
message Property {
required string name = 1;
required string value = 2;
}
message GetPropertiesRequest {
repeated string properties = 1;
}
message GetPropertiesResponse {
required bool success = 1;
repeated Property properties = 2;
}
message IdentifyRequest {
required string serial = 1;
}
message IdentifyResponse {
required bool success = 1;
}