Skip to content

Commit 1e0a01e

Browse files
committed
Add simulated input support
Signed-off-by: Liam McLoughlin <lmcloughlin@fitbit.com>
1 parent 0938332 commit 1e0a01e

8 files changed

Lines changed: 296 additions & 1 deletion

File tree

packages/fdb-debugger/src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,16 +449,55 @@ export class RemoteHost extends EventEmitter {
449449
FDBTypes.AppDebugEvalResult,
450450
);
451451

452+
private sendButtonInput = this.bindMethod(
453+
'input.button',
454+
FDBTypes.ButtonInput,
455+
t.null,
456+
);
457+
458+
private sendTouchInput = this.bindMethod(
459+
'input.touch',
460+
FDBTypes.TouchInput,
461+
t.null,
462+
);
463+
452464
hasEvalSupport() {
453465
return this.hasCapability('appHost.debug.app.evalToString.supported') &&
454466
this.info.capabilities.appHost!.debug!.app!.evalToString!.supported &&
455467
!FBOS3_EVAL_QUIRK.test(this.info.device);
456468
}
457469

470+
hasTouchInputSupport() {
471+
return this.hasCapability('appHost.input.touch') &&
472+
this.info.capabilities.appHost!.input!.touch!;
473+
}
474+
475+
hasButtonInputSupport() {
476+
return this.hasCapability('appHost.input.buttons') &&
477+
this.info.capabilities.appHost!.input!.buttons! &&
478+
this.info.capabilities.appHost!.input!.buttons!.length > 0;
479+
}
480+
481+
buttons() {
482+
if (!this.hasButtonInputSupport) return [];
483+
return this.info.capabilities.appHost!.input!.buttons!;
484+
}
485+
458486
eval(cmd: string) {
459487
return this.sendEvalCmd({ cmd });
460488
}
461489

490+
simulateButtonPress(button: FDBTypes.Button) {
491+
return this.sendButtonInput({ button });
492+
}
493+
494+
simulateTouch(location: FDBTypes.Point, state: FDBTypes.TouchState) {
495+
return this.sendTouchInput({
496+
location,
497+
state,
498+
});
499+
}
500+
462501
supportsPartialAppInstall() {
463502
return this.hasCapability('appHost.install.partialBundle') &&
464503
this.info.capabilities.appHost!.install!.partialBundle!;

packages/fdb-protocol/src/FDBTypes/Initialize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IOCapabilities } from './BulkData';
55
import { ConsoleDebuggerCapabilities } from './Console';
66
import { EvalToStringCapability } from './Eval';
77
import { HeapSnapshotCapability } from './HeapSnapshot';
8+
import { InputCapabilities } from './Input';
89
import { LaunchCapabilities } from './Launch';
910
import { ProtocolCapabilities } from './Meta';
1011
import { ScreenshotCapabilities } from './Screenshot';
@@ -84,6 +85,7 @@ export const ApplicationHostCapabilities = t.partial(
8485
launch: LaunchCapabilities,
8586
screenshot: ScreenshotCapabilities,
8687
debug: DebugCapabilities,
88+
input: InputCapabilities
8789
},
8890
'ApplicationHostCapabilities',
8991
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as t from "io-ts";
2+
3+
import { Point } from "./Structures";
4+
5+
// Runtime types are variables which are used like types, which is
6+
// reflected in their PascalCase naming scheme.
7+
/* tslint:disable:variable-name */
8+
9+
export const Button = t.union(
10+
[t.literal("up"), t.literal("down"), t.literal("back")],
11+
"Button"
12+
);
13+
export type Button = t.TypeOf<typeof Button>;
14+
15+
export const ButtonInput = t.interface(
16+
{
17+
/**
18+
* Which button is being pressed.
19+
*/
20+
button: Button
21+
},
22+
"ButtonInput"
23+
);
24+
export type ButtonInput = t.TypeOf<typeof ButtonInput>;
25+
26+
export const TouchState = t.union(
27+
[t.literal("up"), t.literal("down"), t.literal("move")],
28+
"TouchState"
29+
);
30+
export type TouchState = t.TypeOf<typeof TouchState>;
31+
32+
export const TouchInput = t.interface(
33+
{
34+
/**
35+
* Status of simulated touch.
36+
* 'move' must only be sent in the period between a 'down' input and its corresponding 'up'.
37+
*/
38+
state: TouchState,
39+
40+
/**
41+
* Location of touch event.
42+
*/
43+
location: Point
44+
},
45+
"TouchInput"
46+
);
47+
export type TouchInput = t.TypeOf<typeof TouchInput>;
48+
49+
/**
50+
* Capabilities specific to inputs.
51+
*/
52+
export const InputCapabilities = t.partial(
53+
{
54+
/**
55+
* The Host supports sending simulated button presses.
56+
*/
57+
buttons: t.array(Button),
58+
59+
/**
60+
* The Host supports sending simulated touch screen presses.
61+
*/
62+
touch: t.boolean
63+
},
64+
"InputCapabilities"
65+
);
66+
export type InputCapabilities = t.TypeOf<typeof InputCapabilities>;

packages/fdb-protocol/src/FDBTypes/Structures.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,15 @@ export const ComponentBundleKind = t.union([
182182
t.literal('companion'),
183183
]);
184184
export type ComponentBundleKind = t.TypeOf<typeof ComponentBundleKind>;
185+
186+
/**
187+
* Describes a point on the simulated device's screen, relative to the top-left corner at (0,0).
188+
*/
189+
export const Point = t.interface(
190+
{
191+
x: NonNegativeInteger,
192+
y: NonNegativeInteger,
193+
},
194+
'Point',
195+
);
196+
export type Point = t.TypeOf<typeof Point>;

packages/fdb-protocol/src/FDBTypes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './ContentsList';
66
export * from './Eval';
77
export * from './HeapSnapshot';
88
export * from './Initialize';
9+
export * from './Input';
910
export * from './Launch';
1011
export * from './Meta';
1112
export * from './Screenshot';

packages/sdk-cli/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import buildAndInstall from './commands/buildAndInstall';
2020
import connect from './commands/connect';
2121
import heapSnapshot from './commands/heapSnapshot';
2222
import hosts from './commands/hosts';
23+
import input from './commands/input';
2324
import install from './commands/install';
2425
import logout from './commands/logout';
2526
import mockHost from './commands/mockHost';
@@ -38,6 +39,7 @@ cli.use(build);
3839
cli.use(buildAndInstall({ hostConnections, appContext }));
3940
cli.use(connect({ hostConnections }));
4041
cli.use(heapSnapshot({ hostConnections }));
42+
cli.use(input({ hostConnections }));
4143
cli.use(install({ hostConnections, appContext }));
4244
cli.use(screenshot({ hostConnections }));
4345
cli.use(setAppPackage({ appContext }));
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { FDBTypes } from '@fitbit/fdb-protocol';
2+
import { isRight } from 'fp-ts/lib/Either';
3+
import vorpal from 'vorpal';
4+
5+
import HostConnections from '../models/HostConnections';
6+
7+
function isSupportedButton(supportedButtons: FDBTypes.Button[], button: string): button is FDBTypes.Button {
8+
return supportedButtons.includes(button as FDBTypes.Button);
9+
}
10+
11+
function isValidTouchState(state: string): state is FDBTypes.TouchState {
12+
return isRight(FDBTypes.TouchState.decode(state));
13+
}
14+
15+
const wait = (durationMs: number) => new Promise(resolve => setTimeout(resolve, durationMs));
16+
17+
export default function input(
18+
stores: {
19+
hostConnections: HostConnections,
20+
},
21+
) {
22+
return (cli: vorpal) => {
23+
cli.command('input button <button>', 'Simulate a button press on device')
24+
.hidden()
25+
.action(async (args: vorpal.Args & { button?: string }) => {
26+
const { appHost } = stores.hostConnections;
27+
if (!appHost) {
28+
cli.activeCommand.log('Not connected to a device');
29+
return false;
30+
}
31+
32+
if (!appHost.host.hasButtonInputSupport()) {
33+
cli.activeCommand.log('Connected device does not support simulated button presses');
34+
return false;
35+
}
36+
37+
cli.activeCommand.log(args.button);
38+
if (!isSupportedButton(appHost.host.buttons(), args.button!)) {
39+
cli.activeCommand.log(`Connected device does not support requested button type. Supported buttons: ${appHost.host.buttons().join(', ')}`);
40+
return false;
41+
}
42+
43+
return appHost.host.simulateButtonPress(args.button);
44+
});
45+
46+
cli.command('input touch <state> <x> <y>', 'Simualate a touch event on device')
47+
.hidden()
48+
.action(async (args: vorpal.Args & { state?: string, x?: number, y?: number }) => {
49+
const { appHost } = stores.hostConnections;
50+
if (!appHost) {
51+
cli.activeCommand.log('Not connected to a device');
52+
return false;
53+
}
54+
55+
if (!appHost.host.hasTouchInputSupport()) {
56+
cli.activeCommand.log('Connected device does not support simulated touch events');
57+
return false;
58+
}
59+
60+
if (args.state === 'tap') {
61+
await appHost.host.simulateTouch({ x: args.x!, y: args.y! }, 'down');
62+
await wait(250);
63+
await appHost.host.simulateTouch({ x: args.x!, y: args.y! }, 'up');
64+
} else {
65+
if (!isValidTouchState(args.state!)) {
66+
cli.activeCommand.log('Touch state provided was not valid');
67+
return false;
68+
}
69+
70+
return appHost.host.simulateTouch({ x: args.x!, y: args.y! }, args.state)
71+
}
72+
});
73+
};
74+
}

0 commit comments

Comments
 (0)