2dde6a8401f671820a3a9f07e7af584a28964c8a
[WebKit-https.git] / Tools / WebKitTestRunner / ios / HIDEventGenerator.mm
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "HIDEventGenerator.h"
28
29 #import "IOKitSPI.h"
30 #import "UIKitSPI.h"
31 #import <WebCore/SoftLinking.h>
32 #import <mach/mach_time.h>
33 #import <wtf/Assertions.h>
34 #import <wtf/RetainPtr.h>
35
36 SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
37 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));
38
39 static const NSTimeInterval fingerLiftDelay = 5e7;
40 static const NSTimeInterval multiTapInterval = 15e7;
41 static const NSTimeInterval fingerMoveInterval = 0.016;
42 static const IOHIDFloat defaultMajorRadius = 5;
43 static const IOHIDFloat defaultPathPressure = 0;
44 static const NSUInteger maxTouchCount = 5;
45 static const long nanosecondsPerSecond = 1e9;
46
47 static int fingerIdentifiers[maxTouchCount] = { 2, 3, 4, 5, 1 };
48
49 typedef enum {
50     HandEventNull,
51     HandEventTouched,
52     HandEventMoved,
53     HandEventChordChanged,
54     HandEventLifted,
55     HandEventCanceled,
56     HandEventInRange,
57     HandEventInRangeLift,
58 } HandEventType;
59
60 typedef struct {
61     int identifier;
62     CGPoint point;
63     IOHIDFloat pathMajorRadius;
64     IOHIDFloat pathPressure;
65     UInt8 pathProximity;
66 } SyntheticEventDigitizerInfo;
67
68 static CFTimeInterval secondsSinceAbsoluteTime(CFAbsoluteTime startTime)
69 {
70     return (CFAbsoluteTimeGetCurrent() - startTime);
71 }
72
73 static double simpleDragCurve(double a, double b, double t)
74 {
75     return (a + (b - a) * sin(sin(t * M_PI / 2) * t * M_PI / 2));
76 }
77
78 static CGPoint calculateNextLocation(CGPoint a, CGPoint b, CFTimeInterval t)
79 {
80     return CGPointMake(simpleDragCurve(a.x, b.x, t), simpleDragCurve(a.y, b.y, t));
81 }
82
83 static void delayBetweenMove(int eventIndex, double elapsed)
84 {
85     // Delay next event until expected elapsed time.
86     double delay = (eventIndex * fingerMoveInterval) - elapsed;
87     if (delay > 0) {
88         struct timespec moveDelay = { 0, static_cast<long>(delay * nanosecondsPerSecond) };
89         nanosleep(&moveDelay, NULL);
90     }   
91 }
92
93 @interface HIDEventGenerator ()
94 @property (nonatomic, strong) NSMutableDictionary *eventCallbacks;
95 @end
96
97 @implementation HIDEventGenerator {
98     IOHIDEventSystemClientRef _ioSystemClient;
99     SyntheticEventDigitizerInfo _activePoints[maxTouchCount];
100     NSUInteger _activePointCount;
101 }
102
103 + (HIDEventGenerator *)sharedHIDEventGenerator
104 {
105     static HIDEventGenerator *eventGenerator = nil;
106     if (!eventGenerator)
107         eventGenerator = [[HIDEventGenerator alloc] init];
108
109     return eventGenerator;
110 }
111
112 + (unsigned)nextEventCallbackID
113 {
114     static unsigned callbackID = 0;
115     return ++callbackID;
116 }
117
118 - (instancetype)init
119 {
120     self = [super init];
121     if (!self)
122         return nil;
123
124     for (NSUInteger i = 0; i < maxTouchCount; ++i)
125         _activePoints[i].identifier = fingerIdentifiers[i];
126
127     _eventCallbacks = [[NSMutableDictionary alloc] init];
128
129     return self;
130 }
131
132 - (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
133 {
134     BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged);
135
136     IOHIDDigitizerEventMask eventMask = kIOHIDDigitizerEventTouch;
137     if (eventType == HandEventMoved) {
138         eventMask &= ~kIOHIDDigitizerEventTouch;
139         eventMask |= kIOHIDDigitizerEventPosition;
140         eventMask |= kIOHIDDigitizerEventAttribute;
141     } else if (eventType == HandEventChordChanged) {
142         eventMask |= kIOHIDDigitizerEventPosition;
143         eventMask |= kIOHIDDigitizerEventAttribute;
144     } else if (eventType == HandEventTouched  || eventType == HandEventCanceled) {
145         eventMask |= kIOHIDDigitizerEventIdentity;
146     } else if (eventType == HandEventLifted)
147         eventMask |= kIOHIDDigitizerEventIdentity;
148
149     uint64_t machTime = mach_absolute_time();
150     RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
151         kIOHIDDigitizerTransducerTypeHand,
152         0,
153         0,
154         eventMask,
155         0,
156         0, 0, 0,
157         0,
158         0,
159         0,
160         isTouching,
161         kIOHIDEventOptionNone));
162
163     IOHIDEventSetIntegerValue(eventRef.get(), kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
164
165     for (NSUInteger i = 0; i < _activePointCount; ++i) {
166         SyntheticEventDigitizerInfo* pointInfo = &_activePoints[i];
167         if (eventType == HandEventTouched) {
168             if (!pointInfo->pathMajorRadius)
169                 pointInfo->pathMajorRadius = defaultMajorRadius;
170             if (!pointInfo->pathPressure)
171                 pointInfo->pathPressure = defaultPathPressure;
172             if (!pointInfo->pathProximity)
173                 pointInfo->pathProximity = kGSEventPathInfoInTouch | kGSEventPathInfoInRange;
174         } else if (eventType == HandEventLifted || eventType == HandEventCanceled) {
175             pointInfo->pathMajorRadius = 0;
176             pointInfo->pathPressure = 0;
177             pointInfo->pathProximity = 0;
178         }
179
180         CGPoint point = pointInfo->point;
181         point = CGPointMake(roundf(point.x), roundf(point.y));
182         RetainPtr<IOHIDEventRef> subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
183             pointInfo->identifier,
184             pointInfo->identifier,
185             eventMask,
186             point.x, point.y, 0,
187             pointInfo->pathPressure,
188             0,
189             pointInfo->pathProximity & kGSEventPathInfoInRange,
190             pointInfo->pathProximity & kGSEventPathInfoInTouch,
191             kIOHIDEventOptionNone));
192         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, pointInfo->pathMajorRadius);
193         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, pointInfo->pathMajorRadius);
194         IOHIDEventAppendEvent(eventRef.get(), subEvent.get(), 0);
195     }
196
197     if (_activePointCount) {
198         IOHIDFloat progress = _activePoints[0].pathPressure;
199         RetainPtr<IOHIDEventRef> forceEvent = adoptCF(IOHIDEventCreateForceEvent(kCFAllocatorDefault,
200             machTime,
201             0,
202             progress,
203             0,
204             0.0,
205             kIOHIDEventOptionNone));
206         IOHIDEventAppendEvent(eventRef.get(), forceEvent.get(), 0);
207     }
208
209     return eventRef.leakRef();
210 }
211
212 - (BOOL)_sendHIDEvent:(IOHIDEventRef)eventRef
213 {
214     if (!_ioSystemClient)
215         _ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
216
217     if (eventRef) {
218         RetainPtr<IOHIDEventRef> strongEvent = eventRef;
219         dispatch_async(dispatch_get_main_queue(), ^{
220             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
221             ASSERT(contextID);
222             BKSHIDEventSetDigitizerInfo(strongEvent.get(), contextID, false, false, NULL, 0, 0);
223             [[UIApplication sharedApplication] _enqueueHIDEvent:strongEvent.get()];
224         });
225     }
226     return YES;
227 }
228
229 - (BOOL)_sendMarkerHIDEventWithCompletionBlock:(void (^)(void))completionBlock
230 {
231     unsigned callbackID = [HIDEventGenerator nextEventCallbackID];
232     void (^completionBlockCopy)() = Block_copy(completionBlock);
233     [_eventCallbacks setObject:completionBlockCopy forKey:@(callbackID)];
234
235     uint64_t machTime = mach_absolute_time();
236     RetainPtr<IOHIDEventRef> markerEvent = adoptCF(IOHIDEventCreateVendorDefinedEvent(kCFAllocatorDefault,
237         machTime,
238         kHIDPage_VendorDefinedStart + 100,
239         0,
240         1,
241         (uint8_t*)&callbackID,
242         sizeof(unsigned),
243         kIOHIDEventOptionNone));
244     
245     if (markerEvent) {
246         markerEvent.get();
247         dispatch_async(dispatch_get_main_queue(), ^{
248             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
249             ASSERT(contextID);
250             BKSHIDEventSetDigitizerInfo(markerEvent.get(), contextID, false, false, NULL, 0, 0);
251             [[UIApplication sharedApplication] _enqueueHIDEvent:markerEvent.get()];
252         });
253     }
254     return YES;
255 }
256
257 - (void)_updateTouchPoints:(CGPoint*)points count:(NSUInteger)count
258 {
259     HandEventType handEventType;
260     
261     // The hand event type is based on previous state.
262     if (!_activePointCount)
263         handEventType = HandEventTouched;
264     else if (!count)
265         handEventType = HandEventLifted;
266     else if (count == _activePointCount)
267         handEventType = HandEventMoved;
268     else
269         handEventType = HandEventChordChanged;
270     
271     // Update previous count for next event.
272     _activePointCount = count;
273
274     // Update point locations.
275     for (NSUInteger i = 0; i < count; ++i)
276         _activePoints[i].point = points[i];
277     
278     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:handEventType]);
279     [self _sendHIDEvent:eventRef.get()];
280 }
281
282 - (void)touchDownAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
283 {
284     touchCount = MIN(touchCount, maxTouchCount);
285
286     _activePointCount = touchCount;
287
288     for (NSUInteger index = 0; index < touchCount; ++index)
289         _activePoints[index].point = locations[index];
290
291     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventTouched]);
292     [self _sendHIDEvent:eventRef.get()];
293 }
294
295 - (void)touchDown:(CGPoint)location touchCount:(NSUInteger)touchCount
296 {
297     touchCount = MIN(touchCount, maxTouchCount);
298
299     CGPoint locations[touchCount];
300
301     for (NSUInteger index = 0; index < touchCount; ++index)
302         locations[index] = location;
303     
304     [self touchDownAtPoints:locations touchCount:touchCount];
305 }
306
307 - (void)touchDown:(CGPoint)location
308 {
309     [self touchDownAtPoints:&location touchCount:1];
310 }
311
312 - (void)liftUpAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
313 {
314     touchCount = MIN(touchCount, maxTouchCount);
315     touchCount = MIN(touchCount, _activePointCount);
316
317     NSUInteger newPointCount = _activePointCount - touchCount;
318
319     for (NSUInteger index = 0; index < touchCount; ++index)
320         _activePoints[newPointCount + index].point = locations[index];
321     
322     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventLifted]);
323     [self _sendHIDEvent:eventRef.get()];
324     
325     _activePointCount = newPointCount;
326 }
327
328 - (void)liftUp:(CGPoint)location touchCount:(NSUInteger)touchCount
329 {
330     touchCount = MIN(touchCount, maxTouchCount);
331
332     CGPoint locations[touchCount];
333
334     for (NSUInteger index = 0; index < touchCount; ++index)
335         locations[index] = location;
336     
337     [self liftUpAtPoints:locations touchCount:touchCount];
338 }
339
340 - (void)liftUp:(CGPoint)location
341 {
342     [self liftUp:location touchCount:1];
343 }
344
345 - (void)moveToPoints:(CGPoint*)newLocations touchCount:(NSUInteger)touchCount duration:(NSTimeInterval)seconds
346 {
347     touchCount = MIN(touchCount, maxTouchCount);
348
349     CGPoint startLocations[touchCount];
350     CGPoint nextLocations[touchCount];
351
352     CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
353     CFTimeInterval elapsed = 0;
354
355     int eventIndex = 0;
356     while (elapsed < (seconds - fingerMoveInterval)) {
357         elapsed = secondsSinceAbsoluteTime(startTime);
358         CFTimeInterval interval = elapsed / seconds;
359         
360         for (NSUInteger i = 0; i < touchCount; ++i) {
361             if (!eventIndex)
362                 startLocations[i] = _activePoints[i].point;
363
364             nextLocations[i] = calculateNextLocation(startLocations[i], newLocations[i], interval);
365         }
366         [self _updateTouchPoints:nextLocations count:touchCount];
367
368         delayBetweenMove(eventIndex++, elapsed);
369     }
370
371     [self _updateTouchPoints:newLocations count:touchCount];
372 }
373
374 - (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount completionBlock:(void (^)(void))completionBlock
375 {
376     struct timespec doubleDelay = { 0, static_cast<long>(multiTapInterval) };
377     struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay) };
378
379     for (int i = 0; i < tapCount; i++) {
380         [self touchDown:location touchCount:touchCount];
381         nanosleep(&pressDelay, 0);
382         [self liftUp:location touchCount:touchCount];
383         if (i + 1 != tapCount) 
384             nanosleep(&doubleDelay, 0);
385     }
386     
387     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
388 }
389
390 - (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
391 {
392     [self sendTaps:1 location:location withNumberOfTouches:1 completionBlock:completionBlock];
393 }
394
395 - (void)doubleTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
396 {
397     [self sendTaps:2 location:location withNumberOfTouches:1 completionBlock:completionBlock];
398 }
399
400 - (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
401 {
402     [self sendTaps:1 location:location withNumberOfTouches:2 completionBlock:completionBlock];
403 }
404
405 - (void)dragWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
406 {
407 }
408
409 - (void)pinchCloseWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
410 {
411 }
412
413 - (void)pinchOpenWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
414 {
415 }
416
417 - (void)markerEventReceived:(IOHIDEventRef)event
418 {
419     if (IOHIDEventGetType(event) != kIOHIDEventTypeVendorDefined)
420         return;
421
422     CFIndex callbackID = IOHIDEventGetIntegerValue(event, kIOHIDEventFieldVendorDefinedData);
423     void (^completionBlock)() = [_eventCallbacks objectForKey:@(callbackID)];
424     if (completionBlock) {
425         [_eventCallbacks removeObjectForKey:@(callbackID)];
426         completionBlock();
427         Block_release(completionBlock);
428     }
429 }
430
431 @end