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:
parent
514fc3554d
commit
09eb8c539d
13 changed files with 379 additions and 50 deletions
|
@ -347,6 +347,24 @@ module.exports = function(options) {
|
|||
])
|
||||
})
|
||||
// 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) {
|
||||
push.send([
|
||||
channel
|
||||
|
|
|
@ -18,6 +18,7 @@ module.exports = function(options) {
|
|||
.dependency(require('./device/plugins/display'))
|
||||
.dependency(require('./device/plugins/http'))
|
||||
.dependency(require('./device/plugins/input'))
|
||||
.dependency(require('./device/plugins/clipboard'))
|
||||
.dependency(require('./device/plugins/logcat'))
|
||||
.dependency(require('./device/plugins/shell'))
|
||||
.dependency(require('./device/plugins/touch'))
|
||||
|
|
70
lib/roles/device/plugins/clipboard.js
Normal file
70
lib/roles/device/plugins/clipboard.js
Normal 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
|
||||
))
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -2,7 +2,7 @@ var util = require('util')
|
|||
|
||||
var syrup = require('syrup')
|
||||
var split = require('split')
|
||||
var ByteBuffer = require('protobufjs/node_modules/bytebuffer')
|
||||
var Promise = require('bluebird')
|
||||
|
||||
var wire = require('../../../wire')
|
||||
var wireutil = require('../../../wire/util')
|
||||
|
@ -10,26 +10,29 @@ 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')
|
||||
|
||||
module.exports = syrup.serial()
|
||||
.dependency(require('../support/adb'))
|
||||
.dependency(require('../support/router'))
|
||||
.dependency(require('../support/push'))
|
||||
.dependency(require('../support/quit'))
|
||||
.dependency(require('../resources/inputagent'))
|
||||
.dependency(require('../resources/service'))
|
||||
.define(function(options, adb, router, push, quit, apk) {
|
||||
var log = logger.createLogger('device:plugins:input')
|
||||
var serviceQueue = []
|
||||
|
||||
var agent = {
|
||||
socket: null
|
||||
, writer: null
|
||||
, port: 1090
|
||||
}
|
||||
|
||||
var service = {
|
||||
socket: null
|
||||
, writer: null
|
||||
, reader: null
|
||||
, port: 1100
|
||||
, startAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_START'
|
||||
, stopAction: 'jp.co.cyberagent.stf.input.agent.InputService.ACTION_STOP'
|
||||
}
|
||||
|
||||
function openAgent() {
|
||||
|
@ -40,8 +43,7 @@ module.exports = syrup.serial()
|
|||
})
|
||||
.then(function() {
|
||||
return adb.shell(options.serial, util.format(
|
||||
"export CLASSPATH='%s';" +
|
||||
" exec app_process /system/bin '%s'"
|
||||
"export CLASSPATH='%s'; exec app_process /system/bin '%s'"
|
||||
, apk.path
|
||||
, apk.main
|
||||
))
|
||||
|
@ -65,6 +67,8 @@ module.exports = syrup.serial()
|
|||
})
|
||||
.then(function(conn) {
|
||||
agent.socket = conn
|
||||
agent.writer = new ms.DelimitingStream()
|
||||
agent.writer.pipe(conn)
|
||||
conn.on('error', function(err) {
|
||||
log.fatal('InputAgent socket had an error', err.stack)
|
||||
quit.fatal()
|
||||
|
@ -137,18 +141,30 @@ module.exports = syrup.serial()
|
|||
return devutil.waitForPortToFree(adb, options.serial, service.port)
|
||||
})
|
||||
.then(function() {
|
||||
return callService(util.format("-a '%s'", service.startAction))
|
||||
return callService(util.format("-a '%s'", apk.startAction))
|
||||
})
|
||||
.then(function() {
|
||||
return devutil.waitForPort(adb, options.serial, service.port)
|
||||
})
|
||||
.then(function(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) {
|
||||
log.fatal('InputService socket had an error', err.stack)
|
||||
quit.fatal()
|
||||
})
|
||||
conn.on('end', function() {
|
||||
.on('end', function() {
|
||||
log.fatal('InputService socket ended')
|
||||
quit.fatal()
|
||||
})
|
||||
|
@ -156,43 +172,135 @@ module.exports = syrup.serial()
|
|||
}
|
||||
|
||||
function stopService() {
|
||||
return callService(util.format("-a '%s'", service.stopAction))
|
||||
return callService(util.format("-a '%s'", apk.stopAction))
|
||||
}
|
||||
|
||||
function sendInputEvent(event) {
|
||||
var lengthBuffer = new ByteBuffer()
|
||||
, messageBuffer = new apk.proto.InputEvent(event).encode()
|
||||
agent.writer.write(new apk.agentProto.InputEvent(event).encodeNB())
|
||||
}
|
||||
|
||||
// Delimiter
|
||||
lengthBuffer.writeVarint32(messageBuffer.length)
|
||||
|
||||
agent.socket.write(Buffer.concat([
|
||||
lengthBuffer.toBuffer()
|
||||
, messageBuffer.toBuffer()
|
||||
]))
|
||||
function version() {
|
||||
return runServiceCommand(
|
||||
apk.serviceProto.RequestType.VERSION
|
||||
, new apk.serviceProto.VersionRequest()
|
||||
)
|
||||
.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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
service.socket.write(util.format(
|
||||
'show identity %s\n'
|
||||
, options.serial
|
||||
))
|
||||
return runServiceCommand(
|
||||
apk.serviceProto.RequestType.IDENTIFY
|
||||
, 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()
|
||||
|
@ -242,6 +350,17 @@ module.exports = syrup.serial()
|
|||
, acquireWakeLock: acquireWakeLock
|
||||
, releaseWakeLock: releaseWakeLock
|
||||
, identity: identity
|
||||
, paste: function(text) {
|
||||
return setClipboard(text)
|
||||
.then(function() {
|
||||
sendInputEvent({
|
||||
action: 2
|
||||
, keyCode: adb.Keycode.KEYCODE_V
|
||||
, ctrlKey: true
|
||||
})
|
||||
})
|
||||
}
|
||||
, copy: getClipboard
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -11,16 +11,21 @@ var logger = require('../../../util/logger')
|
|||
module.exports = syrup.serial()
|
||||
.dependency(require('../support/adb'))
|
||||
.define(function(options, adb) {
|
||||
var log = logger.createLogger('device:resources:inputagent')
|
||||
var log = logger.createLogger('device:resources:service')
|
||||
|
||||
var resource = {
|
||||
requiredVersion: '~0.2.0'
|
||||
, pkg: 'jp.co.cyberagent.stf.input.agent'
|
||||
, main: 'jp.co.cyberagent.stf.input.agent.InputAgent'
|
||||
, apk: pathutil.vendor('InputAgent/InputAgent.apk')
|
||||
, proto: ProtoBuf.loadProtoFile(
|
||||
pathutil.vendor('InputAgent/proto/agent.proto')
|
||||
).build().jp.co.cyberagent.stf.input.agent.proto
|
||||
, pkg: 'jp.co.cyberagent.stf'
|
||||
, main: 'jp.co.cyberagent.stf.InputAgent'
|
||||
, apk: pathutil.vendor('STFService/STFService.apk')
|
||||
, agentProto: ProtoBuf.loadProtoFile(
|
||||
pathutil.vendor('STFService/proto/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() {
|
||||
|
@ -34,7 +39,7 @@ module.exports = syrup.serial()
|
|||
}
|
||||
|
||||
function install() {
|
||||
log.info('Checking whether we need to install InputAgent.apk')
|
||||
log.info('Checking whether we need to install STFService')
|
||||
return getPath()
|
||||
.then(function(installedPath) {
|
||||
log.info('Running version check')
|
||||
|
@ -49,6 +54,10 @@ module.exports = syrup.serial()
|
|||
.timeout(10000)
|
||||
.then(function(buffer) {
|
||||
var version = buffer.toString()
|
||||
throw new Error(util.format(
|
||||
'Incompatible version %s'
|
||||
, version
|
||||
))
|
||||
if (semver.satisfies(version, resource.requiredVersion)) {
|
||||
return installedPath
|
||||
}
|
||||
|
@ -62,7 +71,7 @@ module.exports = syrup.serial()
|
|||
})
|
||||
})
|
||||
.catch(function() {
|
||||
log.info('Installing InputAgent.apk')
|
||||
log.info('Installing STFService')
|
||||
return adb.install(options.serial, resource.apk)
|
||||
.then(function() {
|
||||
return getPath()
|
||||
|
@ -72,12 +81,8 @@ module.exports = syrup.serial()
|
|||
|
||||
return install()
|
||||
.then(function(path) {
|
||||
log.info('InputAgent.apk up to date')
|
||||
return {
|
||||
path: path
|
||||
, pkg: resource.pkg
|
||||
, main: resource.main
|
||||
, proto: resource.proto
|
||||
}
|
||||
log.info('STFService up to date')
|
||||
resource.path = path
|
||||
return resource
|
||||
})
|
||||
})
|
|
@ -9,6 +9,7 @@ module.exports = syrup.serial()
|
|||
.define(function(options) {
|
||||
var log = logger.createLogger('device:support:adb')
|
||||
var adb = adbkit.createClient()
|
||||
adb.Keycode = adbkit.Keycode
|
||||
|
||||
function ensureBootComplete() {
|
||||
return promiseutil.periodicNotify(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Message wrapper
|
||||
|
||||
enum MessageType {
|
||||
CopyMessage = 33;
|
||||
DeviceAbsentMessage = 1;
|
||||
DeviceHeartbeatMessage = 28;
|
||||
DeviceIdentityMessage = 2;
|
||||
|
@ -21,6 +22,7 @@ enum MessageType {
|
|||
LaunchActivityMessage = 31;
|
||||
LeaveGroupMessage = 15;
|
||||
LogcatApplyFiltersMessage = 16;
|
||||
PasteMessage = 32;
|
||||
ProbeMessage = 17;
|
||||
ShellCommandMessage = 18;
|
||||
ShellKeepAliveMessage = 19;
|
||||
|
@ -216,6 +218,13 @@ message TypeMessage {
|
|||
required string text = 1;
|
||||
}
|
||||
|
||||
message PasteMessage {
|
||||
required string text = 1;
|
||||
}
|
||||
|
||||
message CopyMessage {
|
||||
}
|
||||
|
||||
enum KeyCode {
|
||||
HOME = 3;
|
||||
BACK = 4;
|
||||
|
|
|
@ -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) {
|
||||
var tx = TransactionService.create(target)
|
||||
socket.emit('shell.command', channel, tx.channel, {
|
||||
|
|
|
@ -86,7 +86,7 @@ module.exports = function DeviceScreenDirective($document, ScalingService) {
|
|||
|
||||
function pasteListener(e) {
|
||||
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() {
|
||||
|
|
BIN
vendor/InputAgent/InputAgent.apk
vendored
BIN
vendor/InputAgent/InputAgent.apk
vendored
Binary file not shown.
BIN
vendor/STFService/STFService.apk
vendored
Normal file
BIN
vendor/STFService/STFService.apk
vendored
Normal file
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
package jp.co.cyberagent.stf.input.agent.proto;
|
||||
package jp.co.cyberagent.stf.proto;
|
||||
|
||||
option java_outer_classname = "AgentProto";
|
||||
|
92
vendor/STFService/proto/service.proto
vendored
Normal file
92
vendor/STFService/proto/service.proto
vendored
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue