Implement keyboard event sending for iOS in WebKitTestRunner
[WebKit-https.git] / Tools / WebKitTestRunner / ios / HIDEventGenerator.mm
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "HIDEventGenerator.h"
28
29 #import "IOKitSPI.h"
30 #import "UIKitSPI.h"
31 #import <WebCore/SoftLinking.h>
32 #import <mach/mach_time.h>
33 #import <wtf/Assertions.h>
34 #import <wtf/RetainPtr.h>
35
36 SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
37 SOFT_LINK(BackBoardServices, BKSHIDEventSetDigitizerInfo, void, (IOHIDEventRef digitizerEvent, uint32_t contextID, uint8_t systemGestureisPossible, uint8_t isSystemGestureStateChangeEvent, CFStringRef displayUUID, CFTimeInterval initialTouchTimestamp, float maxForce), (digitizerEvent, contextID, systemGestureisPossible, isSystemGestureStateChangeEvent, displayUUID, initialTouchTimestamp, maxForce));
38
39 static const NSTimeInterval fingerLiftDelay = 5e7;
40 static const NSTimeInterval multiTapInterval = 15e7;
41 static const NSTimeInterval fingerMoveInterval = 0.016;
42 static const IOHIDFloat defaultMajorRadius = 5;
43 static const IOHIDFloat defaultPathPressure = 0;
44 static const NSUInteger maxTouchCount = 5;
45 static const long nanosecondsPerSecond = 1e9;
46
47 static int fingerIdentifiers[maxTouchCount] = { 2, 3, 4, 5, 1 };
48
49 typedef enum {
50     HandEventNull,
51     HandEventTouched,
52     HandEventMoved,
53     HandEventChordChanged,
54     HandEventLifted,
55     HandEventCanceled,
56     HandEventInRange,
57     HandEventInRangeLift,
58 } HandEventType;
59
60 typedef struct {
61     int identifier;
62     CGPoint point;
63     IOHIDFloat pathMajorRadius;
64     IOHIDFloat pathPressure;
65     UInt8 pathProximity;
66 } SyntheticEventDigitizerInfo;
67
68 static CFTimeInterval secondsSinceAbsoluteTime(CFAbsoluteTime startTime)
69 {
70     return (CFAbsoluteTimeGetCurrent() - startTime);
71 }
72
73 static double simpleDragCurve(double a, double b, double t)
74 {
75     return (a + (b - a) * sin(sin(t * M_PI / 2) * t * M_PI / 2));
76 }
77
78 static CGPoint calculateNextLocation(CGPoint a, CGPoint b, CFTimeInterval t)
79 {
80     return CGPointMake(simpleDragCurve(a.x, b.x, t), simpleDragCurve(a.y, b.y, t));
81 }
82
83 static void delayBetweenMove(int eventIndex, double elapsed)
84 {
85     // Delay next event until expected elapsed time.
86     double delay = (eventIndex * fingerMoveInterval) - elapsed;
87     if (delay > 0) {
88         struct timespec moveDelay = { 0, static_cast<long>(delay * nanosecondsPerSecond) };
89         nanosleep(&moveDelay, NULL);
90     }   
91 }
92
93 @interface HIDEventGenerator ()
94 @property (nonatomic, strong) NSMutableDictionary *eventCallbacks;
95 @end
96
97 @implementation HIDEventGenerator {
98     IOHIDEventSystemClientRef _ioSystemClient;
99     SyntheticEventDigitizerInfo _activePoints[maxTouchCount];
100     NSUInteger _activePointCount;
101 }
102
103 + (HIDEventGenerator *)sharedHIDEventGenerator
104 {
105     static HIDEventGenerator *eventGenerator = nil;
106     if (!eventGenerator)
107         eventGenerator = [[HIDEventGenerator alloc] init];
108
109     return eventGenerator;
110 }
111
112 + (unsigned)nextEventCallbackID
113 {
114     static unsigned callbackID = 0;
115     return ++callbackID;
116 }
117
118 - (instancetype)init
119 {
120     self = [super init];
121     if (!self)
122         return nil;
123
124     for (NSUInteger i = 0; i < maxTouchCount; ++i)
125         _activePoints[i].identifier = fingerIdentifiers[i];
126
127     _eventCallbacks = [[NSMutableDictionary alloc] init];
128
129     return self;
130 }
131
132 - (void)_sendIOHIDKeyboardEvent:(uint64_t)timestamp usage:(uint32_t)usage isKeyDown:(bool)isKeyDown
133 {
134     RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateKeyboardEvent(kCFAllocatorDefault,
135         timestamp,
136         kHIDPage_KeyboardOrKeypad,
137         usage,
138         isKeyDown,
139         kIOHIDEventOptionNone));
140     [self _sendHIDEvent:eventRef.get()];
141 }
142
143 - (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
144 {
145     BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged);
146
147     IOHIDDigitizerEventMask eventMask = kIOHIDDigitizerEventTouch;
148     if (eventType == HandEventMoved) {
149         eventMask &= ~kIOHIDDigitizerEventTouch;
150         eventMask |= kIOHIDDigitizerEventPosition;
151         eventMask |= kIOHIDDigitizerEventAttribute;
152     } else if (eventType == HandEventChordChanged) {
153         eventMask |= kIOHIDDigitizerEventPosition;
154         eventMask |= kIOHIDDigitizerEventAttribute;
155     } else if (eventType == HandEventTouched  || eventType == HandEventCanceled) {
156         eventMask |= kIOHIDDigitizerEventIdentity;
157     } else if (eventType == HandEventLifted)
158         eventMask |= kIOHIDDigitizerEventIdentity;
159
160     uint64_t machTime = mach_absolute_time();
161     RetainPtr<IOHIDEventRef> eventRef = adoptCF(IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
162         kIOHIDDigitizerTransducerTypeHand,
163         0,
164         0,
165         eventMask,
166         0,
167         0, 0, 0,
168         0,
169         0,
170         0,
171         isTouching,
172         kIOHIDEventOptionNone));
173
174     IOHIDEventSetIntegerValue(eventRef.get(), kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
175
176     for (NSUInteger i = 0; i < _activePointCount; ++i) {
177         SyntheticEventDigitizerInfo* pointInfo = &_activePoints[i];
178         if (eventType == HandEventTouched) {
179             if (!pointInfo->pathMajorRadius)
180                 pointInfo->pathMajorRadius = defaultMajorRadius;
181             if (!pointInfo->pathPressure)
182                 pointInfo->pathPressure = defaultPathPressure;
183             if (!pointInfo->pathProximity)
184                 pointInfo->pathProximity = kGSEventPathInfoInTouch | kGSEventPathInfoInRange;
185         } else if (eventType == HandEventLifted || eventType == HandEventCanceled) {
186             pointInfo->pathMajorRadius = 0;
187             pointInfo->pathPressure = 0;
188             pointInfo->pathProximity = 0;
189         }
190
191         CGPoint point = pointInfo->point;
192         point = CGPointMake(roundf(point.x), roundf(point.y));
193         RetainPtr<IOHIDEventRef> subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
194             pointInfo->identifier,
195             pointInfo->identifier,
196             eventMask,
197             point.x, point.y, 0,
198             pointInfo->pathPressure,
199             0,
200             pointInfo->pathProximity & kGSEventPathInfoInRange,
201             pointInfo->pathProximity & kGSEventPathInfoInTouch,
202             kIOHIDEventOptionNone));
203         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, pointInfo->pathMajorRadius);
204         IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, pointInfo->pathMajorRadius);
205         IOHIDEventAppendEvent(eventRef.get(), subEvent.get(), 0);
206     }
207
208     if (_activePointCount) {
209         IOHIDFloat progress = _activePoints[0].pathPressure;
210         RetainPtr<IOHIDEventRef> forceEvent = adoptCF(IOHIDEventCreateForceEvent(kCFAllocatorDefault,
211             machTime,
212             0,
213             progress,
214             0,
215             0.0,
216             kIOHIDEventOptionNone));
217         IOHIDEventAppendEvent(eventRef.get(), forceEvent.get(), 0);
218     }
219
220     return eventRef.leakRef();
221 }
222
223 - (BOOL)_sendHIDEvent:(IOHIDEventRef)eventRef
224 {
225     if (!_ioSystemClient)
226         _ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
227
228     if (eventRef) {
229         RetainPtr<IOHIDEventRef> strongEvent = eventRef;
230         dispatch_async(dispatch_get_main_queue(), ^{
231             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
232             ASSERT(contextID);
233             BKSHIDEventSetDigitizerInfo(strongEvent.get(), contextID, false, false, NULL, 0, 0);
234             [[UIApplication sharedApplication] _enqueueHIDEvent:strongEvent.get()];
235         });
236     }
237     return YES;
238 }
239
240 - (BOOL)_sendMarkerHIDEventWithCompletionBlock:(void (^)(void))completionBlock
241 {
242     unsigned callbackID = [HIDEventGenerator nextEventCallbackID];
243     void (^completionBlockCopy)() = Block_copy(completionBlock);
244     [_eventCallbacks setObject:completionBlockCopy forKey:@(callbackID)];
245
246     uint64_t machTime = mach_absolute_time();
247     RetainPtr<IOHIDEventRef> markerEvent = adoptCF(IOHIDEventCreateVendorDefinedEvent(kCFAllocatorDefault,
248         machTime,
249         kHIDPage_VendorDefinedStart + 100,
250         0,
251         1,
252         (uint8_t*)&callbackID,
253         sizeof(unsigned),
254         kIOHIDEventOptionNone));
255     
256     if (markerEvent) {
257         markerEvent.get();
258         dispatch_async(dispatch_get_main_queue(), ^{
259             uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
260             ASSERT(contextID);
261             BKSHIDEventSetDigitizerInfo(markerEvent.get(), contextID, false, false, NULL, 0, 0);
262             [[UIApplication sharedApplication] _enqueueHIDEvent:markerEvent.get()];
263         });
264     }
265     return YES;
266 }
267
268 - (void)_updateTouchPoints:(CGPoint*)points count:(NSUInteger)count
269 {
270     HandEventType handEventType;
271     
272     // The hand event type is based on previous state.
273     if (!_activePointCount)
274         handEventType = HandEventTouched;
275     else if (!count)
276         handEventType = HandEventLifted;
277     else if (count == _activePointCount)
278         handEventType = HandEventMoved;
279     else
280         handEventType = HandEventChordChanged;
281     
282     // Update previous count for next event.
283     _activePointCount = count;
284
285     // Update point locations.
286     for (NSUInteger i = 0; i < count; ++i)
287         _activePoints[i].point = points[i];
288     
289     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:handEventType]);
290     [self _sendHIDEvent:eventRef.get()];
291 }
292
293 - (void)touchDownAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
294 {
295     touchCount = MIN(touchCount, maxTouchCount);
296
297     _activePointCount = touchCount;
298
299     for (NSUInteger index = 0; index < touchCount; ++index)
300         _activePoints[index].point = locations[index];
301
302     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventTouched]);
303     [self _sendHIDEvent:eventRef.get()];
304 }
305
306 - (void)touchDown:(CGPoint)location touchCount:(NSUInteger)touchCount
307 {
308     touchCount = MIN(touchCount, maxTouchCount);
309
310     CGPoint locations[touchCount];
311
312     for (NSUInteger index = 0; index < touchCount; ++index)
313         locations[index] = location;
314     
315     [self touchDownAtPoints:locations touchCount:touchCount];
316 }
317
318 - (void)touchDown:(CGPoint)location
319 {
320     [self touchDownAtPoints:&location touchCount:1];
321 }
322
323 - (void)liftUpAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
324 {
325     touchCount = MIN(touchCount, maxTouchCount);
326     touchCount = MIN(touchCount, _activePointCount);
327
328     NSUInteger newPointCount = _activePointCount - touchCount;
329
330     for (NSUInteger index = 0; index < touchCount; ++index)
331         _activePoints[newPointCount + index].point = locations[index];
332     
333     RetainPtr<IOHIDEventRef> eventRef = adoptCF([self _createIOHIDEventType:HandEventLifted]);
334     [self _sendHIDEvent:eventRef.get()];
335     
336     _activePointCount = newPointCount;
337 }
338
339 - (void)liftUp:(CGPoint)location touchCount:(NSUInteger)touchCount
340 {
341     touchCount = MIN(touchCount, maxTouchCount);
342
343     CGPoint locations[touchCount];
344
345     for (NSUInteger index = 0; index < touchCount; ++index)
346         locations[index] = location;
347     
348     [self liftUpAtPoints:locations touchCount:touchCount];
349 }
350
351 - (void)liftUp:(CGPoint)location
352 {
353     [self liftUp:location touchCount:1];
354 }
355
356 - (void)moveToPoints:(CGPoint*)newLocations touchCount:(NSUInteger)touchCount duration:(NSTimeInterval)seconds
357 {
358     touchCount = MIN(touchCount, maxTouchCount);
359
360     CGPoint startLocations[touchCount];
361     CGPoint nextLocations[touchCount];
362
363     CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
364     CFTimeInterval elapsed = 0;
365
366     int eventIndex = 0;
367     while (elapsed < (seconds - fingerMoveInterval)) {
368         elapsed = secondsSinceAbsoluteTime(startTime);
369         CFTimeInterval interval = elapsed / seconds;
370         
371         for (NSUInteger i = 0; i < touchCount; ++i) {
372             if (!eventIndex)
373                 startLocations[i] = _activePoints[i].point;
374
375             nextLocations[i] = calculateNextLocation(startLocations[i], newLocations[i], interval);
376         }
377         [self _updateTouchPoints:nextLocations count:touchCount];
378
379         delayBetweenMove(eventIndex++, elapsed);
380     }
381
382     [self _updateTouchPoints:newLocations count:touchCount];
383 }
384
385 - (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount completionBlock:(void (^)(void))completionBlock
386 {
387     struct timespec doubleDelay = { 0, static_cast<long>(multiTapInterval) };
388     struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay) };
389
390     for (int i = 0; i < tapCount; i++) {
391         [self touchDown:location touchCount:touchCount];
392         nanosleep(&pressDelay, 0);
393         [self liftUp:location touchCount:touchCount];
394         if (i + 1 != tapCount) 
395             nanosleep(&doubleDelay, 0);
396     }
397     
398     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
399 }
400
401 - (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
402 {
403     [self sendTaps:1 location:location withNumberOfTouches:1 completionBlock:completionBlock];
404 }
405
406 - (void)doubleTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
407 {
408     [self sendTaps:2 location:location withNumberOfTouches:1 completionBlock:completionBlock];
409 }
410
411 - (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
412 {
413     [self sendTaps:1 location:location withNumberOfTouches:2 completionBlock:completionBlock];
414 }
415
416 - (void)dragWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
417 {
418 }
419
420 - (void)pinchCloseWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
421 {
422 }
423
424 - (void)pinchOpenWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
425 {
426 }
427
428 - (void)markerEventReceived:(IOHIDEventRef)event
429 {
430     if (IOHIDEventGetType(event) != kIOHIDEventTypeVendorDefined)
431         return;
432
433     CFIndex callbackID = IOHIDEventGetIntegerValue(event, kIOHIDEventFieldVendorDefinedData);
434     void (^completionBlock)() = [_eventCallbacks objectForKey:@(callbackID)];
435     if (completionBlock) {
436         [_eventCallbacks removeObjectForKey:@(callbackID)];
437         completionBlock();
438         Block_release(completionBlock);
439     }
440 }
441
442 static inline bool shouldWrapWithShiftKeyEventForCharacter(NSString *key)
443 {
444     if (key.length != 1)
445         return false;
446     int keyCode = [key characterAtIndex:0];
447     if (65 <= keyCode && keyCode <= 90)
448         return true;
449     switch (keyCode) {
450     case '`':
451     case '!':
452     case '@':
453     case '#':
454     case '$':
455     case '%':
456     case '^':
457     case '&':
458     case '*':
459     case '(':
460     case ')':
461     case '_':
462     case '+':
463     case '{':
464     case '}':
465     case '|':
466     case ':':
467     case '"':
468     case '<':
469     case '>':
470     case '?':
471     case '~':
472         return true;
473     }
474     return false;
475 }
476
477 static inline uint32_t hidUsageCodeForCharacter(NSString *key)
478 {
479     const int uppercaseAlphabeticOffset = 'A' - kHIDUsage_KeyboardA;
480     const int lowercaseAlphabeticOffset = 'a' - kHIDUsage_KeyboardA;
481     const int numericNonZeroOffset = '1' - kHIDUsage_Keyboard1;
482     if (key.length == 1) {
483         // Handle alphanumeric characters and basic symbols.
484         int keyCode = [key characterAtIndex:0];
485         if (97 <= keyCode && keyCode <= 122) // Handle a-z.
486             return keyCode - lowercaseAlphabeticOffset;
487
488         if (65 <= keyCode && keyCode <= 90) // Handle A-Z.
489             return keyCode - uppercaseAlphabeticOffset;
490
491         if (49 <= keyCode && keyCode <= 57) // Handle 1-9.
492             return keyCode - numericNonZeroOffset;
493
494         // Handle all other cases.
495         switch (keyCode) {
496         case '`':
497         case '~':
498             return kHIDUsage_KeyboardGraveAccentAndTilde;
499         case '!':
500             return kHIDUsage_Keyboard1;
501         case '@':
502             return kHIDUsage_Keyboard2;
503         case '#':
504             return kHIDUsage_Keyboard3;
505         case '$':
506             return kHIDUsage_Keyboard4;
507         case '%':
508             return kHIDUsage_Keyboard5;
509         case '^':
510             return kHIDUsage_Keyboard6;
511         case '&':
512             return kHIDUsage_Keyboard7;
513         case '*':
514             return kHIDUsage_Keyboard8;
515         case '(':
516             return kHIDUsage_Keyboard9;
517         case ')':
518         case '0':
519             return kHIDUsage_Keyboard0;
520         case '-':
521         case '_':
522             return kHIDUsage_KeyboardHyphen;
523         case '=':
524         case '+':
525             return kHIDUsage_KeyboardEqualSign;
526         case '\t':
527             return kHIDUsage_KeyboardTab;
528         case '[':
529         case '{':
530             return kHIDUsage_KeyboardOpenBracket;
531         case ']':
532         case '}':
533             return kHIDUsage_KeyboardCloseBracket;
534         case '\\':
535         case '|':
536             return kHIDUsage_KeyboardBackslash;
537         case ';':
538         case ':':
539             return kHIDUsage_KeyboardSemicolon;
540         case '\'':
541         case '"':
542             return kHIDUsage_KeyboardQuote;
543         case '\r':
544         case '\n':
545             return kHIDUsage_KeyboardReturnOrEnter;
546         case ',':
547         case '<':
548             return kHIDUsage_KeyboardComma;
549         case '.':
550         case '>':
551             return kHIDUsage_KeyboardPeriod;
552         case '/':
553         case '?':
554             return kHIDUsage_KeyboardSlash;
555         case ' ':
556             return kHIDUsage_KeyboardSpacebar;
557         }
558     }
559     const int functionKeyOffset = kHIDUsage_KeyboardF1;
560     for (int functionKeyIndex = 1; functionKeyIndex <= 12; ++functionKeyIndex) {
561         if ([key isEqualToString:[NSString stringWithFormat:@"F%d", functionKeyIndex]])
562             return functionKeyOffset + functionKeyIndex - 1;
563     }
564     if ([key isEqualToString:@"escape"])
565         return kHIDUsage_KeyboardEscape;
566     if ([key isEqualToString:@"return"] || [key isEqualToString:@"enter"])
567         return kHIDUsage_KeyboardReturnOrEnter;
568     if ([key isEqualToString:@"leftArrow"])
569         return kHIDUsage_KeyboardLeftArrow;
570     if ([key isEqualToString:@"rightArrow"])
571         return kHIDUsage_KeyboardRightArrow;
572     if ([key isEqualToString:@"upArrow"])
573         return kHIDUsage_KeyboardUpArrow;
574     if ([key isEqualToString:@"downArrow"])
575         return kHIDUsage_KeyboardDownArrow;
576     if ([key isEqualToString:@"delete"])
577         return kHIDUsage_KeyboardDeleteOrBackspace;
578     // The simulator keyboard interprets both left and right modifier keys using the left version of the usage code.
579     if ([key isEqualToString:@"leftControl"] || [key isEqualToString:@"rightControl"])
580         return kHIDUsage_KeyboardLeftControl;
581     if ([key isEqualToString:@"leftShift"] || [key isEqualToString:@"rightShift"])
582         return kHIDUsage_KeyboardLeftShift;
583     if ([key isEqualToString:@"leftAlt"] || [key isEqualToString:@"rightAlt"])
584         return kHIDUsage_KeyboardLeftAlt;
585
586     return 0;
587 }
588
589 - (void)keyDown:(NSString *)character completionBlock:(void (^)(void))completionBlock
590 {
591     bool shouldWrapWithShift = shouldWrapWithShiftKeyEventForCharacter(character);
592     uint32_t usage = hidUsageCodeForCharacter(character);
593     uint64_t absoluteMachTime = mach_absolute_time();
594
595     if (shouldWrapWithShift)
596         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:true];
597
598     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:true];
599     [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:false];
600
601     if (shouldWrapWithShift)
602         [self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:false];
603
604     [self _sendMarkerHIDEventWithCompletionBlock:completionBlock];
605 }
606
607 @end