_WKTouchEventGenerator.mm   [plain text]

/*
* Copyright (C) 2015, 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "config.h"
#import "_WKTouchEventGenerator.h"

#if PLATFORM(IOS_FAMILY)

#import "UIKitSPI.h"
#import <mach/mach_time.h>
#import <pal/spi/cocoa/IOKitSPI.h>
#import <wtf/Assertions.h>
#import <wtf/RetainPtr.h>
#import <wtf/SoftLinking.h>

SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
SOFT_LINK(BackBoardServices, BKSHIDEventSetDigitizerInfo, void, (IOHIDEventRef digitizerEvent, uint32_t contextID, uint8_t systemGestureisPossible, uint8_t isSystemGestureStateChangeEvent, CFStringRef displayUUID, CFTimeInterval initialTouchTimestamp, float maxForce), (digitizerEvent, contextID, systemGestureisPossible, isSystemGestureStateChangeEvent, displayUUID, initialTouchTimestamp, maxForce));

static const NSTimeInterval fingerMoveInterval = 0.016;
static const IOHIDFloat defaultMajorRadius = 5;
static const IOHIDFloat defaultPathPressure = 0;
static const long nanosecondsPerSecond = 1e9;

NSUInteger const HIDMaxTouchCount = 5;

static int fingerIdentifiers[HIDMaxTouchCount] = { 2, 3, 4, 5, 1 };

typedef enum {
HandEventNull,
HandEventTouched,
HandEventMoved,
HandEventChordChanged,
HandEventLifted,
HandEventCanceled,
} HandEventType;

typedef struct {
int identifier;
CGPoint point;
IOHIDFloat pathMajorRadius;
IOHIDFloat pathPressure;
UInt8 pathProximity;
} SyntheticEventDigitizerInfo;

static CFTimeInterval secondsSinceAbsoluteTime(CFAbsoluteTime startTime)
{
return CFAbsoluteTimeGetCurrent() - startTime;
}

static double simpleCurveInterpolation(double a, double b, double t)
{
return a + (b - a) * sin(sin(t * M_PI / 2) * t * M_PI / 2);
}

static CGPoint calculateNextCurveLocation(CGPoint a, CGPoint b, CFTimeInterval t)
{
return CGPointMake(simpleCurveInterpolation(a.x, b.x, t), simpleCurveInterpolation(a.y, b.y, t));
}

static void delayBetweenMove(int eventIndex, double elapsed)
{
// Delay next event until expected elapsed time.
double delay = (eventIndex * fingerMoveInterval) - elapsed;
if (delay > 0) {
struct timespec moveDelay = { 0, static_cast<long>(delay * nanosecondsPerSecond) };
nanosleep(&moveDelay, NULL);
}
}

// NOTE: this event synthesizer is derived from WebKitTestRunner code.
// Compared to that version, this lacks support for stylus event simulation,
// event stream, and only single touches are exposed via the touch/lift/move method calls.
@interface _WKTouchEventGenerator ()
@property (nonatomic, strong) NSMutableDictionary *eventCallbacks;
@end

@implementation _WKTouchEventGenerator {
IOHIDEventSystemClientRef _ioSystemClient;
SyntheticEventDigitizerInfo _activePoints[HIDMaxTouchCount];
NSUInteger _activePointCount;
}

+ (_WKTouchEventGenerator *)sharedTouchEventGenerator
{
static _WKTouchEventGenerator *eventGenerator = [[_WKTouchEventGenerator alloc] init];
return eventGenerator;
}

+ (CFIndex)nextEventCallbackID
{
static CFIndex callbackID = 0;
return ++callbackID;
}

- (instancetype)init
{
self = [super init];
if (!self)
return nil;

for (NSUInteger i = 0; i < HIDMaxTouchCount; ++i)
_activePoints[i].identifier = fingerIdentifiers[i];

_eventCallbacks = [[NSMutableDictionary alloc] init];

return self;
}

- (void)dealloc
{
if (_ioSystemClient)
CFRelease(_ioSystemClient);

[_eventCallbacks release];
[super dealloc];
}

- (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
{
BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged);

IOHIDDigitizerEventMask eventMask = kIOHIDDigitizerEventTouch;
if (eventType == HandEventMoved) {
eventMask &= ~kIOHIDDigitizerEventTouch;
eventMask |= kIOHIDDigitizerEventPosition;
eventMask |= kIOHIDDigitizerEventAttribute;
} else if (eventType == HandEventChordChanged) {
eventMask |= kIOHIDDigitizerEventPosition;
eventMask |= kIOHIDDigitizerEventAttribute;
} else if (eventType == HandEventTouched || eventType == HandEventCanceled || eventType == HandEventLifted)
eventMask |= kIOHIDDigitizerEventIdentity;

uint64_t machTime = mach_absolute_time();
RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
kIOHIDDigitizerTransducerTypeHand,
0,
0,
eventMask,
0,
0, 0, 0,
0,
0,
0,
isTouching,
kIOHIDEventOptionNone));

IOHIDEventSetIntegerValue(eventRef.get(), kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);

for (NSUInteger i = 0; i < _activePointCount; ++i) {
SyntheticEventDigitizerInfo* pointInfo = &_activePoints[i];
if (eventType == HandEventTouched) {
if (!pointInfo->pathMajorRadius)
pointInfo->pathMajorRadius = defaultMajorRadius;
if (!pointInfo->pathPressure)
pointInfo->pathPressure = defaultPathPressure;
if (!pointInfo->pathProximity)
pointInfo->pathProximity = kGSEventPathInfoInTouch | kGSEventPathInfoInRange;
} else if (eventType == HandEventLifted || eventType == HandEventCanceled) {
pointInfo->pathMajorRadius = 0;
pointInfo->pathPressure = 0;
pointInfo->pathProximity = 0;
}

CGPoint point = pointInfo->point;
point = CGPointMake(roundf(point.x), roundf(point.y));

RetainPtr<IOHIDEventRef> subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
pointInfo->identifier,
pointInfo->identifier,
eventMask,
point.x, point.y, 0,
pointInfo->pathPressure,
0,
pointInfo->pathProximity & kGSEventPathInfoInRange,
pointInfo->pathProximity & kGSEventPathInfoInTouch,
kIOHIDEventOptionNone));

IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, pointInfo->pathMajorRadius);
IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, pointInfo->pathMajorRadius);

IOHIDEventAppendEvent(eventRef.get(), subEvent.get(), 0);
}

return eventRef.leakRef();
}

- (BOOL)_sendHIDEvent:(IOHIDEventRef)eventRef
{
if (!_ioSystemClient)
_ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);

if (eventRef) {
RetainPtr<IOHIDEventRef> strongEvent = eventRef;
dispatch_async(dispatch_get_main_queue(), ^{
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
ALLOW_DEPRECATED_DECLARATIONS_END
ASSERT(contextID);
BKSHIDEventSetDigitizerInfo(strongEvent.get(), contextID, false, false, NULL, 0, 0);
[[UIApplication sharedApplication] _enqueueHIDEvent:strongEvent.get()];
});
}
return YES;
}

- (BOOL)_sendMarkerHIDEventWithCompletionBlock:(void (^)(void))completionBlock
{
auto callbackID = [_WKTouchEventGenerator nextEventCallbackID];
[_eventCallbacks setObject:Block_copy(completionBlock) forKey:@(callbackID)];

auto markerEvent = adoptCF(IOHIDEventCreateVendorDefinedEvent(kCFAllocatorDefault,
mach_absolute_time(),
kHIDPage_VendorDefinedStart + 100,
0,
1,
(uint8_t*)&callbackID,
sizeof(CFIndex),
kIOHIDEventOptionNone));

if (markerEvent) {
dispatch_async(dispatch_get_main_queue(), [markerEvent = WTFMove(markerEvent)] {
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
auto contextID = [UIApplication sharedApplication].keyWindow._contextId;
ALLOW_DEPRECATED_DECLARATIONS_END
ASSERT(contextID);
BKSHIDEventSetDigitizerInfo(markerEvent.get(), contextID, false, false, NULL, 0, 0);
[[UIApplication sharedApplication] _enqueueHIDEvent:markerEvent.get()];
});
}
return YES;
}

- (void)_updateTouchPoints:(CGPoint*)points count:(NSUInteger)count
{
HandEventType handEventType;

// The hand event type is based on previous state.
if (!_activePointCount)
handEventType = HandEventTouched;
else if (!count)
handEventType = HandEventLifted;
else if (count == _activePointCount)
handEventType = HandEventMoved;
else
handEventType = HandEventChordChanged;

// Update previous count for next event.
_activePointCount = count;

// Update point locations.
for (NSUInteger i = 0; i < count; ++i)
_activePoints[i].point = points[i];

RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:handEventType]);
[self _sendHIDEvent:eventRef.get()];
}

- (void)touchDownAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
{
touchCount = std::min(touchCount, HIDMaxTouchCount);

_activePointCount = touchCount;

for (NSUInteger index = 0; index < touchCount; ++index)
_activePoints[index].point = locations[index];

RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventTouched]);
[self _sendHIDEvent:eventRef.get()];
}

- (void)touchDown:(CGPoint)location touchCount:(NSUInteger)touchCount
{
touchCount = std::min(touchCount, HIDMaxTouchCount);

CGPoint locations[touchCount];

for (NSUInteger index = 0; index < touchCount; ++index)
locations[index] = location;

[self touchDownAtPoints:locations touchCount:touchCount];
}

- (void)touchDown:(CGPoint)location
{
[self touchDownAtPoints:&location touchCount:1];
}

- (void)liftUpAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
{
touchCount = std::min(touchCount, HIDMaxTouchCount);
touchCount = std::min(touchCount, _activePointCount);

NSUInteger newPointCount = _activePointCount - touchCount;

for (NSUInteger index = 0; index < touchCount; ++index)
_activePoints[newPointCount + index].point = locations[index];

RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventLifted]);
[self _sendHIDEvent:eventRef.get()];

_activePointCount = newPointCount;
}

- (void)liftUp:(CGPoint)location touchCount:(NSUInteger)touchCount
{
touchCount = std::min(touchCount, HIDMaxTouchCount);

CGPoint locations[touchCount];

for (NSUInteger index = 0; index < touchCount; ++index)
locations[index] = location;

[self liftUpAtPoints:locations touchCount:touchCount];
}

- (void)liftUp:(CGPoint)location
{
[self liftUp:location touchCount:1];
}

- (void)moveToPoints:(CGPoint*)newLocations touchCount:(NSUInteger)touchCount duration:(NSTimeInterval)seconds
{
touchCount = std::min(touchCount, HIDMaxTouchCount);

CGPoint startLocations[touchCount];
CGPoint nextLocations[touchCount];

CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
CFTimeInterval elapsed = 0;

int eventIndex = 0;
while (elapsed < (seconds - fingerMoveInterval)) {
elapsed = secondsSinceAbsoluteTime(startTime);
CFTimeInterval interval = elapsed / seconds;

for (NSUInteger i = 0; i < touchCount; ++i) {
if (!eventIndex)
startLocations[i] = _activePoints[i].point;

nextLocations[i] = calculateNextCurveLocation(startLocations[i], newLocations[i], interval);
}
[self _updateTouchPoints:nextLocations count:touchCount];

delayBetweenMove(eventIndex++, elapsed);
}

[self _updateTouchPoints:newLocations count:touchCount];
}

- (void)touchDown:(CGPoint)location completionBlock:(void (^)(void))completionBlock
{
[self touchDown:location touchCount:1];
[self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
}

- (void)liftUp:(CGPoint)location completionBlock:(void (^)(void))completionBlock
{
[self liftUp:location touchCount:1];
[self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
}

- (void)moveToPoint:(CGPoint)location duration:(NSTimeInterval)seconds completionBlock:(void (^)(void))completionBlock
{
CGPoint locations[1];
locations[0] = location;
[self moveToPoints:locations touchCount:1 duration:seconds];
[self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
}

- (void)receivedHIDEvent:(IOHIDEventRef)event
{
if (IOHIDEventGetType(event) != kIOHIDEventTypeVendorDefined)
return;

CFIndex callbackID = IOHIDEventGetIntegerValue(event, kIOHIDEventFieldVendorDefinedData);
NSNumber *key = @(callbackID);
void (^completionBlock)() = [_eventCallbacks objectForKey:key];
if (completionBlock) {
[_eventCallbacks removeObjectForKey:key];
completionBlock();
Block_release(completionBlock);
}
}

@end

#endif // PLATFORM(IOS_FAMILY)