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