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