FBSimulatorControl/HID/FBSimulatorHIDEvent.m (378 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBSimulatorHIDEvent.h"
#import <FBControlCore/FBControlCore.h>
#import "FBSimulatorError.h"
#import "FBSimulatorHID.h"
#import "FBSimulator.h"
static NSString *const KeyEventClass = @"class";
static NSString *const KeyDirection = @"direction";
static NSString *const EventClassStringComposite = @"composite";
static NSString *const EventClassStringTouch = @"touch";
static NSString *const EventClassStringButton = @"button";
static NSString *const EventClassStringKeyboard = @"keyboard";
static NSString *const EventClassStringDelay = @"delay";
const double DEFAULT_SWIPE_DELTA = 10.0;
@interface FBSimulatorHIDEvent ()
+ (FBSimulatorHIDDirection)directionFromDirectionString:(NSString *)DirectionString;
+ (NSString *)directionStringFromDirection:(FBSimulatorHIDDirection)Direction;
@end
@interface FBSimulatorHIDEvent_Composite : FBSimulatorHIDEvent
@property (nonatomic, copy, readonly) NSArray<FBSimulatorHIDEvent *> *events;
@end
@implementation FBSimulatorHIDEvent_Composite
static NSString *const KeyEvents = @"events";
- (instancetype)initWithEvents:(NSArray<FBSimulatorHIDEvent *> *)events
{
self = [super init];
if (!self) {
return nil;
}
_events = events;
return self;
}
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
return [self performEvents:self.events onHid:hid];
}
- (FBFuture<NSNull *> *)performEvents:(NSArray<FBSimulatorHIDEvent *> *)events onHid:(FBSimulatorHID *)hid
{
if (events.count == 0) {
return FBFuture.empty;
}
FBSimulatorHIDEvent *event = events.firstObject;
NSArray<FBSimulatorHIDEvent *> *next = events.count == 1 ? @[] : [events subarrayWithRange:NSMakeRange(1, events.count - 1)];
return [[event
performOnHID:hid]
onQueue:dispatch_get_main_queue() fmap:^(id _){
return [self performEvents:next onHid:hid];
}];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Composite %@", [FBCollectionInformation oneLineDescriptionFromArray:self.events]];
}
- (BOOL)isEqual:(FBSimulatorHIDEvent_Composite *)event
{
if (![event isKindOfClass:self.class]) {
return NO;
}
return [self.events isEqualToArray:event.events];
}
- (NSUInteger)hash
{
return self.events.hash;
}
@end
@interface FBSimulatorHIDEvent_Touch : FBSimulatorHIDEvent
@property (nonatomic, assign, readonly) FBSimulatorHIDDirection direction;
@property (nonatomic, assign, readonly) double x;
@property (nonatomic, assign, readonly) double y;
@end
@implementation FBSimulatorHIDEvent_Touch
static NSString *const KeyX = @"x";
static NSString *const KeyY = @"y";
- (instancetype)initWithDirection:(FBSimulatorHIDDirection)direction x:(double)x y:(double)y
{
self = [super init];
if (!self) {
return nil;
}
_direction = direction;
_x = x;
_y = y;
return self;
}
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
return [hid sendTouchWithType:self.direction x:self.x y:self.y];
}
- (NSString *)description
{
return [NSString stringWithFormat:
@"Touch %@ at <hidden>",
[FBSimulatorHIDEvent directionStringFromDirection:self.direction]
];
}
- (BOOL)isEqual:(FBSimulatorHIDEvent_Touch *)event
{
if (![event isKindOfClass:self.class]) {
return NO;
}
return self.direction == event.direction && self.x == event.x && self.y == event.y;
}
- (NSUInteger)hash
{
return (NSUInteger) self.direction | ((NSUInteger) self.x ^ (NSUInteger) self.y);
}
@end
static NSString *const KeyButton = @"button";
static NSString *const ButtonApplePay = @"apple_pay";
static NSString *const ButtonHomeButton = @"home";
static NSString *const ButtonLock = @"lock";
static NSString *const ButtonSideButton = @"side";
static NSString *const ButtonSiri = @"siri";
@interface FBSimulatorHIDEvent_Button : FBSimulatorHIDEvent
@property (nonatomic, assign, readonly) FBSimulatorHIDDirection type;
@property (nonatomic, assign, readonly) FBSimulatorHIDButton button;
@end
@implementation FBSimulatorHIDEvent_Button
- (instancetype)initWithDirection:(FBSimulatorHIDDirection)type button:(FBSimulatorHIDButton)button
{
self = [super init];
if (!self) {
return nil;
}
_type = type;
_button = button;
return self;
}
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
return [hid sendButtonEventWithDirection:self.type button:self.button];
}
- (NSString *)description
{
return [NSString stringWithFormat:
@"Button %@ %@",
[FBSimulatorHIDEvent_Button buttonStringFromButton:self.button],
[FBSimulatorHIDEvent directionStringFromDirection:self.type]
];
}
- (BOOL)isEqual:(FBSimulatorHIDEvent_Button *)event
{
if (![event isKindOfClass:self.class]) {
return NO;
}
return self.type == event.type && self.button == event.button;
}
- (NSUInteger)hash
{
return (NSUInteger) self.type ^ (NSUInteger) self.button;
}
+ (NSString *)buttonStringFromButton:(FBSimulatorHIDButton)button
{
switch (button) {
case FBSimulatorHIDButtonApplePay:
return ButtonApplePay;
case FBSimulatorHIDButtonHomeButton:
return ButtonHomeButton;
case FBSimulatorHIDButtonLock:
return ButtonLock;
case FBSimulatorHIDButtonSideButton:
return ButtonSideButton;
case FBSimulatorHIDButtonSiri:
return ButtonSiri;
default:
return nil;
}
}
+ (FBSimulatorHIDButton)buttonFromButtonString:(NSString *)buttonString
{
if ([buttonString isEqualToString:ButtonApplePay]) {
return FBSimulatorHIDButtonApplePay;
}
if ([buttonString isEqualToString:ButtonHomeButton]) {
return FBSimulatorHIDButtonHomeButton;
}
if ([buttonString isEqualToString:ButtonSideButton]) {
return FBSimulatorHIDButtonSideButton;
}
if ([buttonString isEqualToString:ButtonSiri]) {
return FBSimulatorHIDButtonSiri;
}
if ([buttonString isEqualToString:ButtonLock]) {
return FBSimulatorHIDButtonLock;
}
return 0;
}
@end
static NSString *const KeyKeycode = @"keycode";
@interface FBSimulatorHIDEvent_Keyboard : FBSimulatorHIDEvent
@property (nonatomic, assign, readonly) FBSimulatorHIDDirection direction;
@property (nonatomic, assign, readonly) unsigned int keyCode;
@end
@implementation FBSimulatorHIDEvent_Keyboard
- (instancetype)initWithDirection:(FBSimulatorHIDDirection)direction keyCode:(unsigned int)keyCode
{
self = [super init];
if (!self) {
return nil;
}
_direction = direction;
_keyCode = keyCode;
return self;
}
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
return [hid sendKeyboardEventWithDirection:self.direction keyCode:self.keyCode];
}
- (NSString *)description
{
return [NSString stringWithFormat:
@"Keyboard Code=<hidden> %@",
[FBSimulatorHIDEvent directionStringFromDirection:self.direction]
];
}
- (BOOL)isEqual:(FBSimulatorHIDEvent_Keyboard *)event
{
if (![event isKindOfClass:self.class]) {
return NO;
}
return self.direction == event.direction && self.keyCode == event.keyCode;
}
- (NSUInteger)hash
{
return (NSUInteger) self.direction ^ (NSUInteger) self.keyCode;
}
@end
@interface FBSimulatorHIDEvent_Delay : FBSimulatorHIDEvent
@property (nonatomic, assign, readonly) double duration;
@end
@implementation FBSimulatorHIDEvent_Delay
static NSString *const KeyDuration = @"duration";
- (instancetype)initWithDuration:(double)duration
{
self = [super init];
if (!self) {
return nil;
}
_duration = duration;
return self;
}
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
return [FBFuture futureWithDelay:self.duration future:FBFuture.empty];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Delay for %f", self.duration];
}
- (BOOL)isEqual:(FBSimulatorHIDEvent_Delay *)event
{
if (![event isKindOfClass:self.class]) {
return NO;
}
return self.duration == event.duration;
}
- (NSUInteger)hash
{
return (NSUInteger) self.duration;
}
@end
@implementation FBSimulatorHIDEvent
#pragma mark Initializers
+ (instancetype)eventWithEvents:(NSArray<FBSimulatorHIDEvent *> *)events
{
return [[FBSimulatorHIDEvent_Composite alloc] initWithEvents:events];
}
+ (instancetype)touchDownAtX:(double)x y:(double)y
{
return [[FBSimulatorHIDEvent_Touch alloc] initWithDirection:FBSimulatorHIDDirectionDown x:x y:y];
}
+ (instancetype)touchUpAtX:(double)x y:(double)y
{
return [[FBSimulatorHIDEvent_Touch alloc] initWithDirection:FBSimulatorHIDDirectionUp x:x y:y];
}
+ (instancetype)buttonDown:(FBSimulatorHIDButton)button
{
return [[FBSimulatorHIDEvent_Button alloc] initWithDirection:FBSimulatorHIDDirectionDown button:button];
}
+ (instancetype)buttonUp:(FBSimulatorHIDButton)button
{
return [[FBSimulatorHIDEvent_Button alloc] initWithDirection:FBSimulatorHIDDirectionUp button:button];
}
+ (instancetype)keyDown:(unsigned int)keyCode
{
return [[FBSimulatorHIDEvent_Keyboard alloc] initWithDirection:FBSimulatorHIDDirectionDown keyCode:keyCode];
}
+ (instancetype)keyUp:(unsigned int)keyCode
{
return [[FBSimulatorHIDEvent_Keyboard alloc] initWithDirection:FBSimulatorHIDDirectionUp keyCode:keyCode];
}
+ (instancetype)tapAtX:(double)x y:(double)y
{
return [self eventWithEvents:@[
[self touchDownAtX:x y:y],
[self touchUpAtX:x y:y],
]];
}
+ (instancetype)shortButtonPress:(FBSimulatorHIDButton)button
{
return [self eventWithEvents:@[
[self buttonDown:button],
[self buttonUp:button],
]];
}
+ (instancetype)shortKeyPress:(unsigned int)keyCode
{
return [self eventWithEvents:@[
[self keyDown:keyCode],
[self keyUp:keyCode],
]];
}
+ (instancetype)shortKeyPressSequence:(NSArray<NSNumber *> *)sequence
{
NSMutableArray<FBSimulatorHIDEvent *> *events = [NSMutableArray array];
for (id keyCode in sequence) {
[events addObject:[self keyDown:[keyCode unsignedIntValue]]];
[events addObject:[self keyUp:[keyCode unsignedIntValue]]];
}
return [self eventWithEvents:events];
}
+ (instancetype)swipe:(double)xStart yStart:(double)yStart xEnd:(double)xEnd yEnd:(double)yEnd delta:(double)delta duration:(double)duration
{
NSMutableArray<FBSimulatorHIDEvent *> *events = [NSMutableArray array];
double distance = sqrt(pow(yEnd - yStart, 2) + pow(xEnd - xStart, 2));
if (delta <= 0.0) {
delta = DEFAULT_SWIPE_DELTA;
}
int steps = (int)(distance / delta);
double dx = (xEnd - xStart) / steps;
double dy = (yEnd - yStart) / steps;
double stepDelay = duration/(steps + 1);
for (int i = 0 ; i <= steps ; ++i) {
[events addObject:[self touchDownAtX:(xStart + dx * i) y:(yStart + dy * i)]];
[events addObject:[self delay:stepDelay]];
}
[events addObject:[self touchUpAtX:xEnd y:yEnd]];
return [self eventWithEvents:events];
}
+ (instancetype)delay:(double)duration
{
return [[FBSimulatorHIDEvent_Delay alloc] initWithDuration:duration];
}
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone
{
// All values are immutable.
return self;
}
#pragma mark Public Methods
- (FBFuture<NSNull *> *)performOnHID:(FBSimulatorHID *)hid
{
NSAssert(NO, @"-[%@ %@] is abstract and should be overridden", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
return nil;
}
#pragma mark Private Methods
static NSString *const DirectionDown = @"down";
static NSString *const DirectionUp = @"up";
+ (FBSimulatorHIDDirection)directionFromDirectionString:(NSString *)directionString
{
if ([directionString isEqualToString:DirectionDown]) {
return FBSimulatorHIDDirectionDown;
}
if ([directionString isEqualToString:DirectionUp]) {
return FBSimulatorHIDDirectionUp;
}
return 0;
}
+ (NSString *)directionStringFromDirection:(FBSimulatorHIDDirection)direction
{
switch (direction) {
case FBSimulatorHIDDirectionDown:
return DirectionDown;
case FBSimulatorHIDDirectionUp:
return DirectionUp;
default:
return nil;
}
}
@end