30de4ed5b73f6d9cd65bd1adb59f87eea99908c0
[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/BlockPtr.h>
35 #import <wtf/RetainPtr.h>
36
37 SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
38 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));
39
40 NSString* const TopLevelEventInfoKey = @"events";
41 NSString* const HIDEventInputType = @"inputType";
42 NSString* const HIDEventTimeOffsetKey = @"timeOffset";
43 NSString* const HIDEventTouchesKey = @"touches";
44 NSString* const HIDEventPhaseKey = @"phase";
45 NSString* const HIDEventInterpolateKey = @"interpolate";
46 NSString* const HIDEventTimestepKey = @"timestep";
47 NSString* const HIDEventStartEventKey = @"startEvent";
48 NSString* const HIDEventEndEventKey = @"endEvent";
49 NSString* const HIDEventTouchIDKey = @"id";
50 NSString* const HIDEventPressureKey = @"pressure";
51 NSString* const HIDEventXKey = @"x";
52 NSString* const HIDEventYKey = @"y";
53 NSString* const HIDEventTwistKey = @"twist";
54 NSString* const HIDEventMajorRadiusKey = @"majorRadius";
55 NSString* const HIDEventMinorRadiusKey = @"minorRadius";
56
57 NSString* const HIDEventInputTypeHand = @"hand";
58 NSString* const HIDEventInputTypeFinger = @"finger";
59 NSString* const HIDEventInputTypeStylus = @"stylus";
60
61 NSString* const HIDEventInterpolationTypeLinear = @"linear";
62 NSString* const HIDEventInterpolationTypeSimpleCurve = @"simpleCurve";
63
64 NSString* const HIDEventPhaseBegan = @"began";
65 NSString* const HIDEventPhaseStationary = @"stationary";
66 NSString* const HIDEventPhaseMoved = @"moved";
67 NSString* const HIDEventPhaseEnded = @"ended";
68 NSString* const HIDEventPhaseCanceled = @"canceled";
69
70 static const NSTimeInterval fingerLiftDelay = 0.05;
71 static const NSTimeInterval multiTapInterval = 0.15;
72 static const NSTimeInterval fingerMoveInterval = 0.016;
73 static const NSTimeInterval longPressHoldDelay = 2.0;
74 static const IOHIDFloat defaultMajorRadius = 5;
75 static const IOHIDFloat defaultPathPressure = 0;
76 static const NSUInteger maxTouchCount = 5;
77 static const long nanosecondsPerSecond = 1e9;
78
79 static int fingerIdentifiers[maxTouchCount] = { 2, 3, 4, 5, 1 };
80
81 typedef enum {
82     InterpolationTypeLinear,
83     InterpolationTypeSimpleCurve,
84 } InterpolationType;
85
86 typedef enum {
87     HandEventNull,
88     HandEventTouched,
89     HandEventMoved,
90     HandEventChordChanged,
91     HandEventLifted,
92     HandEventCanceled,
93     StylusEventTouched,
94     StylusEventMoved,
95     StylusEventLifted,
96 } HandEventType;
97
98 typedef struct {
99     int identifier;
100     CGPoint point;
101     IOHIDFloat pathMajorRadius;
102     IOHIDFloat pathPressure;
103     UInt8 pathProximity;
104     BOOL isStylus;
105     IOHIDFloat azimuthAngle;
106     IOHIDFloat altitudeAngle;
107 } SyntheticEventDigitizerInfo;
108
109 static CFTimeInterval secondsSinceAbsoluteTime(CFAbsoluteTime startTime)
110 {
111     return (CFAbsoluteTimeGetCurrent() - startTime);
112 }
113
114 static double linearInterpolation(double a, double b, double t)
115 {
116     return (a + (b - a) * t );
117 }
118
119 static double simpleCurveInterpolation(double a, double b, double t)
120 {
121     return (a + (b - a) * sin(sin(t * M_PI / 2) * t * M_PI / 2));
122 }
123
124
125 static CGPoint calculateNextCurveLocation(CGPoint a, CGPoint b, CFTimeInterval t)
126 {
127     return CGPointMake(simpleCurveInterpolation(a.x, b.x, t), simpleCurveInterpolation(a.y, b.y, t));
128 }
129
130 typedef double(*pressureInterpolationFunction)(double, double, CFTimeInterval);
131 static pressureInterpolationFunction interpolations[] = {
132     linearInterpolation,
133     simpleCurveInterpolation,
134 };
135
136 static void delayBetweenMove(int eventIndex, double elapsed)
137 {
138     // Delay next event until expected elapsed time.
139     double delay = (eventIndex * fingerMoveInterval) - elapsed;
140     if (delay > 0) {
141         struct timespec moveDelay = { 0, static_cast<long>(delay * nanosecondsPerSecond) };
142         nanosleep(&moveDelay, NULL);
143     }   
144 }
145
146 @interface HIDEventGenerator ()
147 @property (nonatomic, strong) NSMutableDictionary *eventCallbacks;
148 @end
149
150 @implementation HIDEventGenerator {
151     IOHIDEventSystemClientRef _ioSystemClient;
152     SyntheticEventDigitizerInfo _activePoints[maxTouchCount];
153     NSUInteger _activePointCount;
154 }
155
156 + (HIDEventGenerator *)sharedHIDEventGenerator
157 {
158     static HIDEventGenerator *eventGenerator = nil;
159     if (!eventGenerator)
160         eventGenerator = [[HIDEventGenerator alloc] init];
161
162     return eventGenerator;
163 }
164
165 + (unsigned)nextEventCallbackID
166 {
167     static unsigned callbackID = 0;
168     return ++callbackID;
169 }
170
171 - (instancetype)init
172 {
173     self = [super init];
174     if (!self)
175         return nil;
176
177     for (NSUInteger i = 0; i < maxTouchCount; ++i)
178         _activePoints[i].identifier = fingerIdentifiers[i];
179
180     _eventCallbacks = [[NSMutableDictionary alloc] init];
181
182     return self;
183 }
184
185 - (void)_sendIOHIDKeyboardEvent:(uint64_t)timestamp usage:(uint32_t)usage isKeyDown:(bool)isKeyDown
186 {
187     RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateKeyboardEvent(kCFAllocatorDefault,
188         timestamp,
189         kHIDPage_KeyboardOrKeypad,
190         usage,
191         isKeyDown,
192         kIOHIDEventOptionNone));
193     [self _sendHIDEvent:eventRef.get()];
194 }
195
196 static IOHIDDigitizerTransducerType transducerTypeFromString(NSString * transducerTypeString)
197 {
198     if ([transducerTypeString isEqualToString:HIDEventInputTypeHand])
199         return kIOHIDDigitizerTransducerTypeHand;
200
201     if ([transducerTypeString isEqualToString:HIDEventInputTypeFinger])
202         return kIOHIDDigitizerTransducerTypeFinger;
203
204     if ([transducerTypeString isEqualToString:HIDEventInputTypeStylus])
205         return kIOHIDDigitizerTransducerTypeStylus;
206     
207     ASSERT_NOT_REACHED();
208     return 0;
209 }
210
211 static UITouchPhase phaseFromString(NSString *string)
212 {
213     if ([string isEqualToString:HIDEventPhaseBegan])
214         return UITouchPhaseBegan;
215     
216     if ([string isEqualToString:HIDEventPhaseStationary])
217         return UITouchPhaseStationary;
218
219     if ([string isEqualToString:HIDEventPhaseMoved])
220         return UITouchPhaseMoved;
221
222     if ([string isEqualToString:HIDEventPhaseEnded])
223         return UITouchPhaseEnded;
224
225     if ([string isEqualToString:HIDEventPhaseCanceled])
226         return UITouchPhaseCancelled;
227
228     return UITouchPhaseStationary;
229 }
230
231 static InterpolationType interpolationFromString(NSString *string)
232 {
233     if ([string isEqualToString:HIDEventInterpolationTypeLinear])
234         return InterpolationTypeLinear;
235     
236     if ([string isEqualToString:HIDEventInterpolationTypeSimpleCurve])
237         return InterpolationTypeSimpleCurve;
238     
239     return InterpolationTypeLinear;
240 }
241
242 - (IOHIDDigitizerEventMask)eventMaskFromEventInfo:(NSDictionary *)info
243 {
244     IOHIDDigitizerEventMask eventMask = 0;
245     NSArray *childEvents = info[HIDEventTouchesKey];
246     for (NSDictionary *touchInfo in childEvents) {
247         UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
248         // If there are any new or ended events, mask includes touch.
249         if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
250             eventMask |= kIOHIDDigitizerEventTouch;
251         // If there are any pressure readings, set mask must include attribute
252         if ([touchInfo[HIDEventPressureKey] floatValue])
253             eventMask |= kIOHIDDigitizerEventAttribute;
254     }
255     
256     return eventMask;
257 }
258
259 // Returns 1 for all events where the fingers are on the glass (everything but ended and canceled).
260 - (CFIndex)touchFromEventInfo:(NSDictionary *)info
261 {
262     NSArray *childEvents = info[HIDEventTouchesKey];
263     for (NSDictionary *touchInfo in childEvents) {
264         UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
265         if (phase == UITouchPhaseBegan || phase == UITouchPhaseMoved || phase == UITouchPhaseStationary)
266             return 1;
267     }
268     
269     return 0;
270 }
271
272 // FIXME: callers of _createIOHIDEventType could switch to this.
273 - (IOHIDEventRef)_createIOHIDEventWithInfo:(NSDictionary *)info
274 {
275     uint64_t machTime = mach_absolute_time();
276
277     IOHIDDigitizerEventMask eventMask = [self eventMaskFromEventInfo:info];
278
279     CFIndex range = 0;
280     // touch is 1 if a finger is down.
281     CFIndex touch = [self touchFromEventInfo:info];
282
283     IOHIDEventRef eventRef = IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
284         transducerTypeFromString(info[HIDEventInputType]),  // transducerType
285         0,                                                  // index
286         0,                                                  // identifier
287         eventMask,                                          // event mask
288         0,                                                  // button event
289         0,                                                  // x
290         0,                                                  // y
291         0,                                                  // z
292         0,                                                  // presure
293         0,                                                  // twist
294         range,                                              // range
295         touch,                                              // touch
296         kIOHIDEventOptionNone);
297
298     IOHIDEventSetIntegerValue(eventRef, kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
299
300     NSArray *childEvents = info[HIDEventTouchesKey];
301     for (NSDictionary *touchInfo in childEvents) {
302
303         IOHIDDigitizerEventMask childEventMask = 0;
304
305         UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
306         if (phase != UITouchPhaseCancelled && phase != UITouchPhaseBegan && phase != UITouchPhaseEnded && phase != UITouchPhaseStationary)
307             childEventMask |= kIOHIDDigitizerEventPosition;
308
309         if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
310             childEventMask |= (kIOHIDDigitizerEventTouch | kIOHIDDigitizerEventRange);
311
312         if (phase == UITouchPhaseCancelled)
313             childEventMask |= kIOHIDDigitizerEventCancel;
314         
315         if ([touchInfo[HIDEventPressureKey] floatValue])
316             childEventMask |= kIOHIDDigitizerEventAttribute;
317
318         IOHIDEventRef subEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
319             [touchInfo[HIDEventTouchIDKey] intValue],               // index
320             2,                                                      // identifier (which finger we think it is). FIXME: this should come from the data.
321             childEventMask,
322             [touchInfo[HIDEventXKey] floatValue],
323             [touchInfo[HIDEventYKey] floatValue],
324             0, // z
325             [touchInfo[HIDEventPressureKey] floatValue],
326             [touchInfo[HIDEventTwistKey] floatValue],
327             touch,                                                  // range
328             touch,                                                  // touch
329             kIOHIDEventOptionNone);
330
331         IOHIDEventSetFloatValue(subEvent, kIOHIDEventFieldDigitizerMajorRadius, [touchInfo[HIDEventMajorRadiusKey] floatValue]);
332         IOHIDEventSetFloatValue(subEvent, kIOHIDEventFieldDigitizerMinorRadius, [touchInfo[HIDEventMinorRadiusKey] floatValue]);
333
334         IOHIDEventAppendEvent(eventRef, subEvent, 0);
335         CFRelease(subEvent);
336     }
337
338     return eventRef;
339 }
340
341 - (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
342 {
343     BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged || eventType == StylusEventTouched || eventType == StylusEventMoved);
344
345     IOHIDDigitizerEventMask eventMask = kIOHIDDigitizerEventTouch;
346     if (eventType == HandEventMoved) {
347         eventMask &= ~kIOHIDDigitizerEventTouch;
348         eventMask |= kIOHIDDigitizerEventPosition;
349         eventMask |= kIOHIDDigitizerEventAttribute;
350     } else if (eventType == HandEventChordChanged) {
351         eventMask |= kIOHIDDigitizerEventPosition;
352         eventMask |= kIOHIDDigitizerEventAttribute;
353     } else if (eventType == HandEventTouched || eventType == HandEventCanceled || eventType == HandEventLifted)
354         eventMask |= kIOHIDDigitizerEventIdentity;
355
356     uint64_t machTime = mach_absolute_time();
357     RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
358         kIOHIDDigitizerTransducerTypeHand,
359         0,
360         0,
361         eventMask,
362         0,
363         0, 0, 0,
364         0,
365         0,
366         0,
367         isTouching,
368         kIOHIDEventOptionNone));
369
370     IOHIDEventSetIntegerValue(eventRef.get(), kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
371
372     for (NSUInteger i = 0; i < _activePointCount; ++i) {
373         SyntheticEventDigitizerInfo* pointInfo = &_activePoints[i];
374         if (eventType == HandEventTouched) {
375             if (!pointInfo->pathMajorRadius)
376                 pointInfo->pathMajorRadius = defaultMajorRadius;
377             if (!pointInfo->pathPressure)
378                 pointInfo->pathPressure = defaultPathPressure;
379             if (!pointInfo->pathProximity)
380                 pointInfo->pathProximity = kGSEventPathInfoInTouch | kGSEventPathInfoInRange;
381         } else if (eventType == HandEventLifted || eventType == HandEventCanceled || eventType == StylusEventLifted) {
382             pointInfo->pathMajorRadius = 0;
383             pointInfo->pathPressure = 0;
384             pointInfo->pathProximity = 0;
385         }
386
387         CGPoint point = pointInfo->point;
388         point = CGPointMake(roundf(point.x), roundf(point.y));
389         RetainPtr<IOHIDEventRef> subEvent;
390         if (pointInfo->isStylus) {
391             if (eventType == StylusEventTouched) {
392 #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 100000
393                 eventMask |= kIOHIDDigitizerEventEstimatedAltitude;
394                 eventMask |= kIOHIDDigitizerEventEstimatedAzimuth;
395                 eventMask |= kIOHIDDigitizerEventEstimatedPressure;
396 #else
397                 eventMask |= kIOHIDDigitizerEventUpdateAltitudeMask;
398                 eventMask |= kIOHIDDigitizerEventUpdateAzimuthMask;
399                 eventMask |= kIOHIDDigitizerEventUpdatePressureMask;
400 #endif
401             } else if (eventType == StylusEventMoved)
402                 eventMask = kIOHIDDigitizerEventPosition;
403
404             subEvent = adoptCF(IOHIDEventCreateDigitizerStylusEventWithPolarOrientation(kCFAllocatorDefault, machTime,
405                 pointInfo->identifier,
406                 pointInfo->identifier,
407                 eventMask,
408                 0,
409                 point.x, point.y, 0,
410                 pointInfo->pathPressure,
411                 pointInfo->pathPressure,
412                 0,
413                 pointInfo->altitudeAngle,
414                 pointInfo->azimuthAngle,
415                 1,
416                 0,
417                 isTouching ? kIOHIDTransducerTouch : 0));
418
419             if (eventType == StylusEventTouched)
420                 IOHIDEventSetIntegerValue(subEvent.get(), kIOHIDEventFieldDigitizerWillUpdateMask, 0x0400);
421             else if (eventType == StylusEventMoved)
422                 IOHIDEventSetIntegerValue(subEvent.get(), kIOHIDEventFieldDigitizerDidUpdateMask, 0x0400);
423
424         } else {
425             subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
426                 pointInfo->identifier,
427                 pointInfo->identifier,
428                 eventMask,
429                 point.x, point.y, 0,
430                 pointInfo->pathPressure,
431                 0,
432                 pointInfo->pathProximity & kGSEventPathInfoInRange,
433                 pointInfo->pathProximity & kGSEventPathInfoInTouch,
434                 kIOHIDEventOptionNone));
435         }
436
437         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, pointInfo->pathMajorRadius);
438         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, pointInfo->pathMajorRadius);
439
440         IOHIDEventAppendEvent(eventRef.get(), subEvent.get(), 0);
441     }
442
443     return eventRef.leakRef();
444 }
445
446 - (BOOL)_sendHIDEvent:(IOHIDEventRef)eventRef
447 {
448     if (!_ioSystemClient)
449         _ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
450
451     if (eventRef) {
452         RetainPtr<IOHIDEventRef> strongEvent = eventRef;
453         dispatch_async(dispatch_get_main_queue(), ^{
454             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
455             ASSERT(contextID);
456             BKSHIDEventSetDigitizerInfo(strongEvent.get(), contextID, false, false, NULL, 0, 0);
457             [[UIApplication sharedApplication] _enqueueHIDEvent:strongEvent.get()];
458         });
459     }
460     return YES;
461 }
462
463 - (BOOL)_sendMarkerHIDEventWithCompletionBlock:(void (^)(void))completionBlock
464 {
465     unsigned callbackID = [HIDEventGenerator nextEventCallbackID];
466     void (^completionBlockCopy)() = Block_copy(completionBlock);
467     [_eventCallbacks setObject:completionBlockCopy forKey:@(callbackID)];
468
469     uint64_t machTime = mach_absolute_time();
470     RetainPtr<IOHIDEventRef> markerEvent = adoptCF(IOHIDEventCreateVendorDefinedEvent(kCFAllocatorDefault,
471         machTime,
472         kHIDPage_VendorDefinedStart + 100,
473         0,
474         1,
475         (uint8_t*)&callbackID,
476         sizeof(unsigned),
477         kIOHIDEventOptionNone));
478     
479     if (markerEvent) {
480         markerEvent.get();
481         dispatch_async(dispatch_get_main_queue(), ^{
482             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
483             ASSERT(contextID);
484             BKSHIDEventSetDigitizerInfo(markerEvent.get(), contextID, false, false, NULL, 0, 0);
485             [[UIApplication sharedApplication] _enqueueHIDEvent:markerEvent.get()];
486         });
487     }
488     return YES;
489 }
490
491 - (void)_updateTouchPoints:(CGPoint*)points count:(NSUInteger)count
492 {
493     HandEventType handEventType;
494     
495     // The hand event type is based on previous state.
496     if (!_activePointCount)
497         handEventType = HandEventTouched;
498     else if (!count)
499         handEventType = HandEventLifted;
500     else if (count == _activePointCount)
501         handEventType = HandEventMoved;
502     else
503         handEventType = HandEventChordChanged;
504     
505     // Update previous count for next event.
506     _activePointCount = count;
507
508     // Update point locations.
509     for (NSUInteger i = 0; i < count; ++i)
510         _activePoints[i].point = points[i];
511     
512     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:handEventType]);
513     [self _sendHIDEvent:eventRef.get()];
514 }
515
516 - (void)touchDownAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
517 {
518     touchCount = MIN(touchCount, maxTouchCount);
519
520     _activePointCount = touchCount;
521
522     for (NSUInteger index = 0; index < touchCount; ++index) {
523         _activePoints[index].point = locations[index];
524         _activePoints[index].isStylus = NO;
525     }
526
527     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventTouched]);
528     [self _sendHIDEvent:eventRef.get()];
529 }
530
531 - (void)touchDown:(CGPoint)location touchCount:(NSUInteger)touchCount
532 {
533     touchCount = MIN(touchCount, maxTouchCount);
534
535     CGPoint locations[touchCount];
536
537     for (NSUInteger index = 0; index < touchCount; ++index)
538         locations[index] = location;
539     
540     [self touchDownAtPoints:locations touchCount:touchCount];
541 }
542
543 - (void)touchDown:(CGPoint)location
544 {
545     [self touchDownAtPoints:&location touchCount:1];
546 }
547
548 - (void)liftUpAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
549 {
550     touchCount = MIN(touchCount, maxTouchCount);
551     touchCount = MIN(touchCount, _activePointCount);
552
553     NSUInteger newPointCount = _activePointCount - touchCount;
554
555     for (NSUInteger index = 0; index < touchCount; ++index)
556         _activePoints[newPointCount + index].point = locations[index];
557     
558     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventLifted]);
559     [self _sendHIDEvent:eventRef.get()];
560     
561     _activePointCount = newPointCount;
562 }
563
564 - (void)liftUp:(CGPoint)location touchCount:(NSUInteger)touchCount
565 {
566     touchCount = MIN(touchCount, maxTouchCount);
567
568     CGPoint locations[touchCount];
569
570     for (NSUInteger index = 0; index < touchCount; ++index)
571         locations[index] = location;
572     
573     [self liftUpAtPoints:locations touchCount:touchCount];
574 }
575
576 - (void)liftUp:(CGPoint)location
577 {
578     [self liftUp:location touchCount:1];
579 }
580
581 - (void)moveToPoints:(CGPoint*)newLocations touchCount:(NSUInteger)touchCount duration:(NSTimeInterval)seconds
582 {
583     touchCount = MIN(touchCount, maxTouchCount);
584
585     CGPoint startLocations[touchCount];
586     CGPoint nextLocations[touchCount];
587
588     CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
589     CFTimeInterval elapsed = 0;
590
591     int eventIndex = 0;
592     while (elapsed < (seconds - fingerMoveInterval)) {
593         elapsed = secondsSinceAbsoluteTime(startTime);
594         CFTimeInterval interval = elapsed / seconds;
595         
596         for (NSUInteger i = 0; i < touchCount; ++i) {
597             if (!eventIndex)
598                 startLocations[i] = _activePoints[i].point;
599
600             nextLocations[i] = calculateNextCurveLocation(startLocations[i], newLocations[i], interval);
601         }
602         [self _updateTouchPoints:nextLocations count:touchCount];
603
604         delayBetweenMove(eventIndex++, elapsed);
605     }
606
607     [self _updateTouchPoints:newLocations count:touchCount];
608 }
609
610 - (void)touchDown:(CGPoint)location touchCount:(NSUInteger)count completionBlock:(void (^)(void))completionBlock
611 {
612     [self touchDown:location touchCount:count];
613     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
614 }
615
616 - (void)liftUp:(CGPoint)location touchCount:(NSUInteger)count completionBlock:(void (^)(void))completionBlock
617 {
618     [self liftUp:location touchCount:count];
619     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
620 }
621
622 - (void)stylusDownAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure
623 {
624     _activePointCount = 1;
625     _activePoints[0].point = location;
626     _activePoints[0].isStylus = YES;
627
628     // At the time of writing, the IOKit documentation isn't always correct. For example
629     // it says that pressure is a value [0,1], but in practice it is [0,500] for stylus
630     // data. It does not mention that the azimuth angle is offset from a full rotation.
631     // Also, UIKit and IOHID interpret the altitude as different adjacent angles.
632     _activePoints[0].pathPressure = pressure * 500;
633     _activePoints[0].azimuthAngle = M_PI * 2 - azimuthAngle;
634     _activePoints[0].altitudeAngle = M_PI_2 - altitudeAngle;
635
636     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:StylusEventTouched]);
637     [self _sendHIDEvent:eventRef.get()];
638 }
639
640 - (void)stylusMoveToPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure
641 {
642     _activePointCount = 1;
643     _activePoints[0].point = location;
644     _activePoints[0].isStylus = YES;
645     // See notes above for details on these calculations.
646     _activePoints[0].pathPressure = pressure * 500;
647     _activePoints[0].azimuthAngle = M_PI * 2 - azimuthAngle;
648     _activePoints[0].altitudeAngle = M_PI_2 - altitudeAngle;
649
650     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:StylusEventMoved]);
651     [self _sendHIDEvent:eventRef.get()];
652 }
653
654 - (void)stylusUpAtPoint:(CGPoint)location
655 {
656     _activePointCount = 1;
657     _activePoints[0].point = location;
658     _activePoints[0].isStylus = YES;
659     _activePoints[0].pathPressure = 0;
660     _activePoints[0].azimuthAngle = 0;
661     _activePoints[0].altitudeAngle = 0;
662
663     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:StylusEventLifted]);
664     [self _sendHIDEvent:eventRef.get()];
665 }
666
667 - (void)stylusDownAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
668 {
669     [self stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
670     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
671 }
672
673 - (void)stylusMoveToPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
674 {
675     [self stylusMoveToPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
676     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
677 }
678
679 - (void)stylusUpAtPoint:(CGPoint)location completionBlock:(void (^)(void))completionBlock
680 {
681     [self stylusUpAtPoint:location];
682     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
683 }
684
685 - (void)stylusTapAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
686 {
687     struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay * nanosecondsPerSecond) };
688
689     [self stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
690     nanosleep(&pressDelay, 0);
691     [self stylusUpAtPoint:location];
692
693     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
694 }
695
696 - (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount completionBlock:(void (^)(void))completionBlock
697 {
698     struct timespec doubleDelay = { 0, static_cast<long>(multiTapInterval * nanosecondsPerSecond) };
699     struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay * nanosecondsPerSecond) };
700
701     for (int i = 0; i < tapCount; i++) {
702         [self touchDown:location touchCount:touchCount];
703         nanosleep(&pressDelay, 0);
704         [self liftUp:location touchCount:touchCount];
705         if (i + 1 != tapCount) 
706             nanosleep(&doubleDelay, 0);
707     }
708     
709     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
710 }
711
712 - (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
713 {
714     [self sendTaps:1 location:location withNumberOfTouches:1 completionBlock:completionBlock];
715 }
716
717 - (void)doubleTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
718 {
719     [self sendTaps:2 location:location withNumberOfTouches:1 completionBlock:completionBlock];
720 }
721
722 - (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
723 {
724     [self sendTaps:1 location:location withNumberOfTouches:2 completionBlock:completionBlock];
725 }
726
727 - (void)longPress:(CGPoint)location completionBlock:(void (^)(void))completionBlock
728 {
729     [self touchDown:location touchCount:1];
730     auto completionBlockCopy = makeBlockPtr(completionBlock);
731
732     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, longPressHoldDelay * nanosecondsPerSecond), dispatch_get_main_queue(), ^ {
733         [self liftUp:location];
734         [self _sendMarkerHIDEventWithCompletionBlock:completionBlockCopy.get()];
735     });
736 }
737
738 - (void)dragWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
739 {
740     [self touchDown:startLocation touchCount:1];
741     [self moveToPoints:&endLocation touchCount:1 duration:seconds];
742     [self liftUp:endLocation];
743     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
744 }
745
746 - (void)pinchCloseWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
747 {
748 }
749
750 - (void)pinchOpenWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
751 {
752 }
753
754 - (void)markerEventReceived:(IOHIDEventRef)event
755 {
756     if (IOHIDEventGetType(event) != kIOHIDEventTypeVendorDefined)
757         return;
758
759     CFIndex callbackID = IOHIDEventGetIntegerValue(event, kIOHIDEventFieldVendorDefinedData);
760     void (^completionBlock)() = [_eventCallbacks objectForKey:@(callbackID)];
761     if (completionBlock) {
762         [_eventCallbacks removeObjectForKey:@(callbackID)];
763         completionBlock();
764         Block_release(completionBlock);
765     }
766 }
767
768 static inline bool shouldWrapWithShiftKeyEventForCharacter(NSString *key)
769 {
770     if (key.length != 1)
771         return false;
772     int keyCode = [key characterAtIndex:0];
773     if (65 <= keyCode && keyCode <= 90)
774         return true;
775     switch (keyCode) {
776     case '`':
777     case '!':
778     case '@':
779     case '#':
780     case '$':
781     case '%':
782     case '^':
783     case '&':
784     case '*':
785     case '(':
786     case ')':
787     case '_':
788     case '+':
789     case '{':
790     case '}':
791     case '|':
792     case ':':
793     case '"':
794     case '<':
795     case '>':
796     case '?':
797     case '~':
798         return true;
799     }
800     return false;
801 }
802
803 static inline uint32_t hidUsageCodeForCharacter(NSString *key)
804 {
805     const int uppercaseAlphabeticOffset = 'A' - kHIDUsage_KeyboardA;
806     const int lowercaseAlphabeticOffset = 'a' - kHIDUsage_KeyboardA;
807     const int numericNonZeroOffset = '1' - kHIDUsage_Keyboard1;
808     if (key.length == 1) {
809         // Handle alphanumeric characters and basic symbols.
810         int keyCode = [key characterAtIndex:0];
811         if (97 <= keyCode && keyCode <= 122) // Handle a-z.
812             return keyCode - lowercaseAlphabeticOffset;
813
814         if (65 <= keyCode && keyCode <= 90) // Handle A-Z.
815             return keyCode - uppercaseAlphabeticOffset;
816
817         if (49 <= keyCode && keyCode <= 57) // Handle 1-9.
818             return keyCode - numericNonZeroOffset;
819
820         // Handle all other cases.
821         switch (keyCode) {
822         case '`':
823         case '~':
824             return kHIDUsage_KeyboardGraveAccentAndTilde;
825         case '!':
826             return kHIDUsage_Keyboard1;
827         case '@':
828             return kHIDUsage_Keyboard2;
829         case '#':
830             return kHIDUsage_Keyboard3;
831         case '$':
832             return kHIDUsage_Keyboard4;
833         case '%':
834             return kHIDUsage_Keyboard5;
835         case '^':
836             return kHIDUsage_Keyboard6;
837         case '&':
838             return kHIDUsage_Keyboard7;
839         case '*':
840             return kHIDUsage_Keyboard8;
841         case '(':
842             return kHIDUsage_Keyboard9;
843         case ')':
844         case '0':
845             return kHIDUsage_Keyboard0;
846         case '-':
847         case '_':
848             return kHIDUsage_KeyboardHyphen;
849         case '=':
850         case '+':
851             return kHIDUsage_KeyboardEqualSign;
852         case '\t':
853             return kHIDUsage_KeyboardTab;
854         case '[':
855         case '{':
856             return kHIDUsage_KeyboardOpenBracket;
857         case ']':
858         case '}':
859             return kHIDUsage_KeyboardCloseBracket;
860         case '\\':
861         case '|':
862             return kHIDUsage_KeyboardBackslash;
863         case ';':
864         case ':':
865             return kHIDUsage_KeyboardSemicolon;
866         case '\'':
867         case '"':
868             return kHIDUsage_KeyboardQuote;
869         case '\r':
870         case '\n':
871             return kHIDUsage_KeyboardReturnOrEnter;
872         case ',':
873         case '<':
874             return kHIDUsage_KeyboardComma;
875         case '.':
876         case '>':
877             return kHIDUsage_KeyboardPeriod;
878         case '/':
879         case '?':
880             return kHIDUsage_KeyboardSlash;
881         case ' ':
882             return kHIDUsage_KeyboardSpacebar;
883         }
884     }
885     const int functionKeyOffset = kHIDUsage_KeyboardF1;
886     for (int functionKeyIndex = 1; functionKeyIndex <= 12; ++functionKeyIndex) {
887         if ([key isEqualToString:[NSString stringWithFormat:@"F%d", functionKeyIndex]])
888             return functionKeyOffset + functionKeyIndex - 1;
889     }
890     if ([key isEqualToString:@"escape"])
891         return kHIDUsage_KeyboardEscape;
892     if ([key isEqualToString:@"return"] || [key isEqualToString:@"enter"])
893         return kHIDUsage_KeyboardReturnOrEnter;
894     if ([key isEqualToString:@"leftArrow"])
895         return kHIDUsage_KeyboardLeftArrow;
896     if ([key isEqualToString:@"rightArrow"])
897         return kHIDUsage_KeyboardRightArrow;
898     if ([key isEqualToString:@"upArrow"])
899         return kHIDUsage_KeyboardUpArrow;
900     if ([key isEqualToString:@"downArrow"])
901         return kHIDUsage_KeyboardDownArrow;
902     if ([key isEqualToString:@"delete"])
903         return kHIDUsage_KeyboardDeleteOrBackspace;
904     // The simulator keyboard interprets both left and right modifier keys using the left version of the usage code.
905     if ([key isEqualToString:@"leftControl"] || [key isEqualToString:@"rightControl"])
906         return kHIDUsage_KeyboardLeftControl;
907     if ([key isEqualToString:@"leftShift"] || [key isEqualToString:@"rightShift"])
908         return kHIDUsage_KeyboardLeftShift;
909     if ([key isEqualToString:@"leftAlt"] || [key isEqualToString:@"rightAlt"])
910         return kHIDUsage_KeyboardLeftAlt;
911
912     return 0;
913 }
914
915 - (void)keyPress:(NSString *)character completionBlock:(void (^)(void))completionBlock
916 {
917     bool shouldWrapWithShift = shouldWrapWithShiftKeyEventForCharacter(character);
918     uint32_t usage = hidUsageCodeForCharacter(character);
919     uint64_t absoluteMachTime = mach_absolute_time();
920
921     if (shouldWrapWithShift)
922         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:true];
923
924     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:true];
925     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:false];
926
927     if (shouldWrapWithShift)
928         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:false];
929
930     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
931 }
932
933 - (void)keyDown:(NSString *)character completionBlock:(void (^)(void))completionBlock
934 {
935     bool shouldWrapWithShift = shouldWrapWithShiftKeyEventForCharacter(character);
936     uint32_t usage = hidUsageCodeForCharacter(character);
937     uint64_t absoluteMachTime = mach_absolute_time();
938
939     if (shouldWrapWithShift)
940         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:true];
941
942     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:true];
943
944     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
945 }
946
947 - (void)keyUp:(NSString *)character completionBlock:(void (^)(void))completionBlock
948 {
949     bool shouldWrapWithShift = shouldWrapWithShiftKeyEventForCharacter(character);
950     uint32_t usage = hidUsageCodeForCharacter(character);
951     uint64_t absoluteMachTime = mach_absolute_time();
952
953     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:false];
954
955     if (shouldWrapWithShift)
956         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:false];
957
958     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
959 }
960
961 - (void)dispatchEventWithInfo:(NSDictionary *)eventInfo
962 {
963     ASSERT([NSThread isMainThread]);
964
965     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventWithInfo:eventInfo]);
966     [self _sendHIDEvent:eventRef.get()];
967 }
968
969 - (NSArray *)interpolatedEvents:(NSDictionary *)interpolationsDictionary
970 {
971     NSDictionary *startEvent = interpolationsDictionary[HIDEventStartEventKey];
972     NSDictionary *endEvent = interpolationsDictionary[HIDEventEndEventKey];
973     NSTimeInterval timeStep = [interpolationsDictionary[HIDEventTimestepKey] doubleValue];
974     InterpolationType interpolationType = interpolationFromString(interpolationsDictionary[HIDEventInterpolateKey]);
975     
976     NSMutableArray *interpolatedEvents = [NSMutableArray arrayWithObject:startEvent];
977     
978     NSTimeInterval startTime = [startEvent[HIDEventTimeOffsetKey] doubleValue];
979     NSTimeInterval endTime = [endEvent[HIDEventTimeOffsetKey] doubleValue];
980     NSTimeInterval time = startTime + timeStep;
981     
982     NSArray *startTouches = startEvent[HIDEventTouchesKey];
983     NSArray *endTouches = endEvent[HIDEventTouchesKey];
984     
985     while (time < endTime) {
986         NSMutableDictionary *newEvent = [endEvent mutableCopy];
987         double timeRatio = (time - startTime) / (endTime - startTime);
988         newEvent[HIDEventTimeOffsetKey] = [NSNumber numberWithDouble:(time)];
989         
990         NSEnumerator *startEnumerator = [startTouches objectEnumerator];
991         NSDictionary *startTouch;
992         NSMutableArray *newTouches = [NSMutableArray arrayWithCapacity:[endTouches count]];
993         while (startTouch = [startEnumerator nextObject])  {
994             NSEnumerator *endEnumerator = [endTouches objectEnumerator];
995             NSDictionary *endTouch = [endEnumerator nextObject];
996             NSInteger startTouchID = [startTouch[HIDEventTouchIDKey] integerValue];
997             
998             while (endTouch && ([endTouch[HIDEventTouchIDKey] integerValue] != startTouchID))
999                 endTouch = [endEnumerator nextObject];
1000             
1001             if (endTouch) {
1002                 NSMutableDictionary *newTouch = [endTouch mutableCopy];
1003                 
1004                 if (newTouch[HIDEventXKey] != startTouch[HIDEventXKey])
1005                     newTouch[HIDEventXKey] = @(interpolations[interpolationType]([startTouch[HIDEventXKey] doubleValue], [endTouch[HIDEventXKey] doubleValue], timeRatio));
1006                 
1007                 if (newTouch[HIDEventYKey] != startTouch[HIDEventYKey])
1008                     newTouch[HIDEventYKey] = @(interpolations[interpolationType]([startTouch[HIDEventYKey] doubleValue], [endTouch[HIDEventYKey] doubleValue], timeRatio));
1009                 
1010                 if (newTouch[HIDEventPressureKey] != startTouch[HIDEventPressureKey])
1011                     newTouch[HIDEventPressureKey] = @(interpolations[interpolationType]([startTouch[HIDEventPressureKey] doubleValue], [endTouch[HIDEventPressureKey] doubleValue], timeRatio));
1012                 
1013                 [newTouches addObject:newTouch];
1014                 [newTouch release];
1015             } else
1016                 NSLog(@"Missing End Touch with ID: %ld", (long)startTouchID);
1017         }
1018         
1019         newEvent[HIDEventTouchesKey] = newTouches;
1020         
1021         [interpolatedEvents addObject:newEvent];
1022         [newEvent release];
1023         time += timeStep;
1024     }
1025
1026     return interpolatedEvents;
1027 }
1028
1029 - (NSArray *)expandEvents:(NSArray *)events withStartTime:(CFAbsoluteTime)startTime
1030 {
1031     NSMutableArray *expandedEvents = [NSMutableArray array];
1032     for (NSDictionary *event in events) {
1033         NSString *interpolate = event[HIDEventInterpolateKey];
1034         // we have key events that we need to generate
1035         if (interpolate) {
1036             NSArray *newEvents = [self interpolatedEvents:event];
1037             [expandedEvents addObjectsFromArray:[self expandEvents:newEvents withStartTime:startTime]];
1038         } else
1039             [expandedEvents addObject:event];
1040     }
1041     return expandedEvents;
1042 }
1043
1044 - (void)eventDispatchThreadEntry:(NSDictionary *)threadData
1045 {
1046     NSDictionary *eventStream = threadData[@"eventInfo"];
1047     void (^completionBlock)() = threadData[@"completionBlock"];
1048     
1049     NSArray *events = eventStream[TopLevelEventInfoKey];
1050     if (!events.count) {
1051         NSLog(@"No events found in event stream");
1052         return;
1053     }
1054     
1055     CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
1056     
1057     NSArray *expandedEvents = [self expandEvents:events withStartTime:startTime];
1058     
1059     for (NSDictionary *eventInfo in expandedEvents) {
1060         NSTimeInterval eventRelativeTime = [eventInfo[HIDEventTimeOffsetKey] doubleValue];
1061         CFAbsoluteTime targetTime = startTime + eventRelativeTime;
1062         
1063         CFTimeInterval waitTime = targetTime - CFAbsoluteTimeGetCurrent();
1064         if (waitTime > 0)
1065             [NSThread sleepForTimeInterval:waitTime];
1066         
1067         dispatch_async(dispatch_get_main_queue(), ^ {
1068             [self dispatchEventWithInfo:eventInfo];
1069         });
1070     }
1071     
1072     dispatch_async(dispatch_get_main_queue(), ^ {
1073         [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
1074     });
1075 }
1076
1077 - (void)sendEventStream:(NSDictionary *)eventInfo completionBlock:(void (^)(void))completionBlock
1078 {
1079     if (!eventInfo) {
1080         NSLog(@"eventInfo is nil");
1081         if (completionBlock)
1082             completionBlock();
1083         return;
1084     }
1085     
1086     NSDictionary* threadData = @{
1087         @"eventInfo": [eventInfo copy],
1088         @"completionBlock": [[completionBlock copy] autorelease]
1089     };
1090     
1091     NSThread *eventDispatchThread = [[[NSThread alloc] initWithTarget:self selector:@selector(eventDispatchThreadEntry:) object:threadData] autorelease];
1092     eventDispatchThread.qualityOfService = NSQualityOfServiceUserInteractive;
1093     [eventDispatchThread start];
1094 }
1095
1096 @end