| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/testing/hardware_keyboard_util.h" |
| |
| #import <dlfcn.h> |
| #import <objc/runtime.h> |
| |
| #import "base/test/ios/wait_util.h" |
| |
| #pragma mark - Private API from IOKit to support CreateHIDKeyEvent |
| |
| typedef UInt32 IOOptionBits; |
| |
| typedef struct __IOHIDEvent* IOHIDEventRef; |
| |
| extern "C" { |
| |
| // This function is privately defined in IOKit framework. |
| IOHIDEventRef IOHIDEventCreateKeyboardEvent(CFAllocatorRef, |
| uint64_t, |
| uint32_t, |
| uint32_t, |
| boolean_t, |
| IOOptionBits); |
| } |
| |
| // This enum is defined in IOKit framework. |
| typedef enum { |
| kUIKeyboardInputRepeat = 1 << 0, |
| kUIKeyboardInputPopupVariant = 1 << 1, |
| kUIKeyboardInputMultitap = 1 << 2, |
| kUIKeyboardInputSkipCandidateSelection = 1 << 3, |
| kUIKeyboardInputDeadKey = 1 << 4, |
| kUIKeyboardInputModifierFlagsChanged = 1 << 5, |
| kUIKeyboardInputFlick = 1 << 6, |
| kUIKeyboardInputPreProcessed = 1 << 7, |
| } UIKeyboardInputFlags; |
| |
| // This enum is defined in IOKit framework. |
| enum { |
| kHIDUsage_KeyboardA = 0x04, |
| kHIDUsage_Keyboard1 = 0x1E, |
| kHIDUsage_Keyboard2 = 0x1F, |
| kHIDUsage_Keyboard3 = 0x20, |
| kHIDUsage_Keyboard4 = 0x21, |
| kHIDUsage_Keyboard5 = 0x22, |
| kHIDUsage_Keyboard6 = 0x23, |
| kHIDUsage_Keyboard7 = 0x24, |
| kHIDUsage_Keyboard8 = 0x25, |
| kHIDUsage_Keyboard9 = 0x26, |
| kHIDUsage_Keyboard0 = 0x27, |
| kHIDUsage_KeyboardReturnOrEnter = 0x28, |
| kHIDUsage_KeyboardEscape = 0x29, |
| kHIDUsage_KeyboardDeleteOrBackspace = 0x2A, |
| kHIDUsage_KeyboardTab = 0x2B, |
| kHIDUsage_KeyboardSpacebar = 0x2C, |
| kHIDUsage_KeyboardHyphen = 0x2D, |
| kHIDUsage_KeyboardEqualSign = 0x2E, |
| kHIDUsage_KeyboardOpenBracket = 0x2F, |
| kHIDUsage_KeyboardCloseBracket = 0x30, |
| kHIDUsage_KeyboardBackslash = 0x31, |
| kHIDUsage_KeyboardSemicolon = 0x33, |
| kHIDUsage_KeyboardQuote = 0x34, |
| kHIDUsage_KeyboardGraveAccentAndTilde = 0x35, |
| kHIDUsage_KeyboardComma = 0x36, |
| kHIDUsage_KeyboardPeriod = 0x37, |
| kHIDUsage_KeyboardSlash = 0x38, |
| kHIDUsage_KeyboardCapsLock = 0x39, |
| kHIDUsage_KeyboardF1 = 0x3A, |
| kHIDUsage_KeyboardF12 = 0x45, |
| kHIDUsage_KeyboardPrintScreen = 0x46, |
| kHIDUsage_KeyboardInsert = 0x49, |
| kHIDUsage_KeyboardHome = 0x4A, |
| kHIDUsage_KeyboardPageUp = 0x4B, |
| kHIDUsage_KeyboardDeleteForward = 0x4C, |
| kHIDUsage_KeyboardEnd = 0x4D, |
| kHIDUsage_KeyboardPageDown = 0x4E, |
| kHIDUsage_KeyboardRightArrow = 0x4F, |
| kHIDUsage_KeyboardLeftArrow = 0x50, |
| kHIDUsage_KeyboardDownArrow = 0x51, |
| kHIDUsage_KeyboardUpArrow = 0x52, |
| kHIDUsage_KeypadNumLock = 0x53, |
| kHIDUsage_KeyboardF13 = 0x68, |
| kHIDUsage_KeyboardF24 = 0x73, |
| kHIDUsage_KeyboardMenu = 0x76, |
| kHIDUsage_KeypadComma = 0x85, |
| kHIDUsage_KeyboardLeftControl = 0xE0, |
| kHIDUsage_KeyboardLeftShift = 0xE1, |
| kHIDUsage_KeyboardLeftAlt = 0xE2, |
| kHIDUsage_KeyboardLeftGUI = 0xE3, |
| kHIDUsage_KeyboardRightControl = 0xE4, |
| kHIDUsage_KeyboardRightShift = 0xE5, |
| kHIDUsage_KeyboardRightAlt = 0xE6, |
| kHIDUsage_KeyboardRightGUI = 0xE7, |
| }; |
| |
| // This function is privately defined in IOKit framework. |
| static uint32_t keyCodeForFunctionKey(NSString* key) { |
| // Compare the input string with the function-key names (i.e. "F1",...,"F24"). |
| for (int i = 1; i <= 12; ++i) { |
| if ([key isEqualToString:[NSString stringWithFormat:@"F%d", i]]) |
| return kHIDUsage_KeyboardF1 + i - 1; |
| } |
| for (int i = 13; i <= 24; ++i) { |
| if ([key isEqualToString:[NSString stringWithFormat:@"F%d", i]]) |
| return kHIDUsage_KeyboardF13 + i - 13; |
| } |
| return 0; |
| } |
| |
| // This function is privately defined in IOKit framework. |
| static inline uint32_t hidUsageCodeForCharacter(NSString* key) { |
| const int uppercaseAlphabeticOffset = 'A' - kHIDUsage_KeyboardA; |
| const int lowercaseAlphabeticOffset = 'a' - kHIDUsage_KeyboardA; |
| const int numericNonZeroOffset = '1' - kHIDUsage_Keyboard1; |
| if (key.length == 1) { |
| // Handle alphanumeric characters and basic symbols. |
| int keyCode = [key characterAtIndex:0]; |
| if (97 <= keyCode && keyCode <= 122) // Handle a-z. |
| return keyCode - lowercaseAlphabeticOffset; |
| |
| if (65 <= keyCode && keyCode <= 90) // Handle A-Z. |
| return keyCode - uppercaseAlphabeticOffset; |
| |
| if (49 <= keyCode && keyCode <= 57) // Handle 1-9. |
| return keyCode - numericNonZeroOffset; |
| |
| // Handle all other cases. |
| switch (keyCode) { |
| case '`': |
| case '~': |
| return kHIDUsage_KeyboardGraveAccentAndTilde; |
| case '!': |
| return kHIDUsage_Keyboard1; |
| case '@': |
| return kHIDUsage_Keyboard2; |
| case '#': |
| return kHIDUsage_Keyboard3; |
| case '$': |
| return kHIDUsage_Keyboard4; |
| case '%': |
| return kHIDUsage_Keyboard5; |
| case '^': |
| return kHIDUsage_Keyboard6; |
| case '&': |
| return kHIDUsage_Keyboard7; |
| case '*': |
| return kHIDUsage_Keyboard8; |
| case '(': |
| return kHIDUsage_Keyboard9; |
| case ')': |
| case '0': |
| return kHIDUsage_Keyboard0; |
| case '-': |
| case '_': |
| return kHIDUsage_KeyboardHyphen; |
| case '=': |
| case '+': |
| return kHIDUsage_KeyboardEqualSign; |
| case '\b': |
| return kHIDUsage_KeyboardDeleteOrBackspace; |
| case '\t': |
| return kHIDUsage_KeyboardTab; |
| case '[': |
| case '{': |
| return kHIDUsage_KeyboardOpenBracket; |
| case ']': |
| case '}': |
| return kHIDUsage_KeyboardCloseBracket; |
| case '\\': |
| case '|': |
| return kHIDUsage_KeyboardBackslash; |
| case ';': |
| case ':': |
| return kHIDUsage_KeyboardSemicolon; |
| case '\'': |
| case '"': |
| return kHIDUsage_KeyboardQuote; |
| case '\r': |
| case '\n': |
| return kHIDUsage_KeyboardReturnOrEnter; |
| case ',': |
| case '<': |
| return kHIDUsage_KeyboardComma; |
| case '.': |
| case '>': |
| return kHIDUsage_KeyboardPeriod; |
| case '/': |
| case '?': |
| return kHIDUsage_KeyboardSlash; |
| case ' ': |
| return kHIDUsage_KeyboardSpacebar; |
| } |
| } |
| |
| uint32_t keyCode = keyCodeForFunctionKey(key); |
| if (keyCode) |
| return keyCode; |
| |
| if ([key isEqualToString:@"capsLock"] || [key isEqualToString:@"capsLockKey"]) |
| return kHIDUsage_KeyboardCapsLock; |
| if ([key isEqualToString:@"pageUp"]) |
| return kHIDUsage_KeyboardPageUp; |
| if ([key isEqualToString:@"pageDown"]) |
| return kHIDUsage_KeyboardPageDown; |
| if ([key isEqualToString:@"home"]) |
| return kHIDUsage_KeyboardHome; |
| if ([key isEqualToString:@"insert"]) |
| return kHIDUsage_KeyboardInsert; |
| if ([key isEqualToString:@"end"]) |
| return kHIDUsage_KeyboardEnd; |
| if ([key isEqualToString:@"escape"]) |
| return kHIDUsage_KeyboardEscape; |
| if ([key isEqualToString:@"return"] || [key isEqualToString:@"enter"]) |
| return kHIDUsage_KeyboardReturnOrEnter; |
| if ([key isEqualToString:@"leftArrow"]) |
| return kHIDUsage_KeyboardLeftArrow; |
| if ([key isEqualToString:@"rightArrow"]) |
| return kHIDUsage_KeyboardRightArrow; |
| if ([key isEqualToString:@"upArrow"]) |
| return kHIDUsage_KeyboardUpArrow; |
| if ([key isEqualToString:@"downArrow"]) |
| return kHIDUsage_KeyboardDownArrow; |
| if ([key isEqualToString:@"delete"]) |
| return kHIDUsage_KeyboardDeleteOrBackspace; |
| if ([key isEqualToString:@"forwardDelete"]) |
| return kHIDUsage_KeyboardDeleteForward; |
| if ([key isEqualToString:@"leftCommand"] || [key isEqualToString:@"metaKey"]) |
| return kHIDUsage_KeyboardLeftGUI; |
| if ([key isEqualToString:@"rightCommand"]) |
| return kHIDUsage_KeyboardRightGUI; |
| if ([key isEqualToString:@"clear"]) // Num Lock / Clear |
| return kHIDUsage_KeypadNumLock; |
| if ([key isEqualToString:@"leftControl"] || [key isEqualToString:@"ctrlKey"]) |
| return kHIDUsage_KeyboardLeftControl; |
| if ([key isEqualToString:@"rightControl"]) |
| return kHIDUsage_KeyboardRightControl; |
| if ([key isEqualToString:@"leftShift"] || [key isEqualToString:@"shiftKey"]) |
| return kHIDUsage_KeyboardLeftShift; |
| if ([key isEqualToString:@"rightShift"]) |
| return kHIDUsage_KeyboardRightShift; |
| if ([key isEqualToString:@"leftAlt"] || [key isEqualToString:@"altKey"]) |
| return kHIDUsage_KeyboardLeftAlt; |
| if ([key isEqualToString:@"rightAlt"]) |
| return kHIDUsage_KeyboardRightAlt; |
| if ([key isEqualToString:@"numpadComma"]) |
| return kHIDUsage_KeypadComma; |
| |
| return 0; |
| } |
| |
| typedef NS_OPTIONS(NSInteger, BKSKeyModifierFlags) { |
| BKSKeyModifierShift = 1 << 17, |
| BKSKeyModifierControl = 1 << 18, |
| BKSKeyModifierAlternate = 1 << 19, |
| BKSKeyModifierCommand = 1 << 20, |
| }; |
| |
| @interface BKSHIDEventBaseAttributes : NSObject |
| @end |
| |
| @interface BKSHIDEventDigitizerAttributes : BKSHIDEventBaseAttributes |
| @property(nonatomic) BKSKeyModifierFlags activeModifiers; |
| @end |
| |
| // These are privately defined in IOKit framework. |
| enum { kHIDPage_KeyboardOrKeypad = 0x07, kHIDPage_VendorDefinedStart = 0xFF00 }; |
| |
| enum { |
| kIOHIDEventOptionNone = 0, |
| }; |
| |
| // From |
| // https://github.com/WebKit/WebKit/blob/32d692229b2aa815346811da6a8db51f260090fe/Source/WTF/wtf/cocoa/SoftLinking.h |
| #define SOFT_LINK_PRIVATE_FRAMEWORK(framework) \ |
| static void* framework##Library() { \ |
| static void* frameworkLibrary = ^{ \ |
| void* result = dlopen("/System/Library/PrivateFrameworks/" #framework \ |
| ".framework/" #framework, \ |
| RTLD_NOW); \ |
| return result; \ |
| }(); \ |
| return frameworkLibrary; \ |
| } |
| |
| #ifdef __cplusplus |
| #define WTF_EXTERN_C_BEGIN extern "C" { |
| #define WTF_EXTERN_C_END } |
| #else |
| #define WTF_EXTERN_C_BEGIN |
| #define WTF_EXTERN_C_END |
| #endif |
| |
| #define SOFT_LINK(framework, functionName, resultType, parameterDeclarations, \ |
| parameterNames) \ |
| WTF_EXTERN_C_BEGIN \ |
| resultType functionName parameterDeclarations; \ |
| WTF_EXTERN_C_END \ |
| static resultType init##functionName parameterDeclarations; \ |
| static resultType(*softLink##functionName) parameterDeclarations = \ |
| init##functionName; \ |
| \ |
| static resultType init##functionName parameterDeclarations { \ |
| softLink##functionName = (resultType(*) parameterDeclarations)dlsym( \ |
| framework##Library(), #functionName); \ |
| return softLink##functionName parameterNames; \ |
| } \ |
| \ |
| inline __attribute__((__always_inline__)) \ |
| resultType functionName parameterDeclarations { \ |
| return softLink##functionName parameterNames; \ |
| } |
| |
| // From |
| // https://github.com/WebKit/WebKit/blob/32d692229b2aa815346811da6a8db51f260090fe/Tools/WebKitTestRunner/ios/HIDEventGenerator.mm |
| SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices) |
| SOFT_LINK(BackBoardServices, |
| BKSHIDEventSetDigitizerInfo, |
| void, |
| (IOHIDEventRef digitizerEvent, |
| uint32_t contextID, |
| uint8_t systemGestureisPossible, |
| uint8_t isSystemGestureStateChangeEvent, |
| CFStringRef displayUUID, |
| CFTimeInterval initialTouchTimestamp, |
| float maxForce), |
| (digitizerEvent, |
| contextID, |
| systemGestureisPossible, |
| isSystemGestureStateChangeEvent, |
| displayUUID, |
| initialTouchTimestamp, |
| maxForce)) |
| SOFT_LINK(BackBoardServices, |
| BKSHIDEventGetDigitizerAttributes, |
| BKSHIDEventDigitizerAttributes*, |
| (IOHIDEventRef event), |
| (event)) |
| |
| #pragma mark - Private API to fake keyboard events. |
| |
| // Convenience wrapper for IOHIDEventCreateKeyboardEvent. |
| IOHIDEventRef CreateHIDKeyEvent(NSString* character, |
| uint64_t timestamp, |
| bool isKeyDown) { |
| return IOHIDEventCreateKeyboardEvent( |
| kCFAllocatorDefault, timestamp, kHIDPage_KeyboardOrKeypad, |
| hidUsageCodeForCharacter(character), isKeyDown, kIOHIDEventOptionNone); |
| } |
| |
| // A fake class that mirrors UIPhysicalKeyboardEvent private class' fields. |
| // This class is never used, but it allows to call UIPhysicalKeyboardEvent's API |
| // by casting an instance of UIPhysicalKeyboardEvent to PhysicalKeyboardEvent. |
| @interface PhysicalKeyboardEvent : UIEvent |
| + (id)_eventWithInput:(id)arg1 inputFlags:(int)arg2; |
| - (void)_setHIDEvent:(IOHIDEventRef)event keyboard:(void*)gsKeyboard; |
| // >=iOS17 only. |
| - (void)_setModifierFlags:(UIKeyModifierFlags)flags; |
| // <=iOS16 only. |
| @property(nonatomic) UIKeyModifierFlags _modifierFlags; |
| @end |
| |
| // Private API in UIKit. |
| @interface UIApplication () |
| - (void)_enqueueHIDEvent:(IOHIDEventRef)event; |
| - (void)handleKeyUIEvent:(id)event; |
| - (void)handleKeyHIDEvent:(id)event; |
| @end |
| |
| @interface UIWindow () |
| - (uint32_t)_contextId; |
| @end |
| |
| #pragma mark - Implementation |
| |
| using base::test::ios::kWaitForUIElementTimeout; |
| |
| namespace { |
| |
| // Delay between simulated keyboard press events. |
| const double kKeyPressDelay = 0.02; |
| |
| // Utility to describe modifier flags. Useful in debugging. |
| [[maybe_unused]] NSString* DescribeFlags(UIKeyModifierFlags flags); |
| NSString* DescribeFlags(UIKeyModifierFlags flags) { |
| NSMutableString* s = [[NSMutableString alloc] init]; |
| if (flags & UIKeyModifierAlphaShift) { |
| [s appendString:@"CapsLock+"]; |
| } |
| if (flags & UIKeyModifierShift) { |
| [s appendString:@"Shift+"]; |
| } |
| if (flags & UIKeyModifierControl) { |
| [s appendString:@"Ctrl+"]; |
| } |
| if (flags & UIKeyModifierAlternate) { |
| [s appendString:@"Alt+"]; |
| } |
| if (flags & UIKeyModifierCommand) { |
| [s appendString:@"Command+"]; |
| } |
| if (flags & UIKeyModifierNumericPad) { |
| [s appendString:@"NumPad+"]; |
| } |
| |
| return s; |
| } |
| |
| uint32_t GetContextId() { |
| for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) { |
| UIWindowScene* windowScene = (UIWindowScene*)scene; |
| for (UIWindow* window in windowScene.windows) { |
| if (window.isKeyWindow) { |
| return window._contextId; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| // Sends an individual keyboard press event. |
| void SendKBEventWithModifiers(UIKeyModifierFlags flags, NSString* input) { |
| // Fake up an event. |
| PhysicalKeyboardEvent* keyboardEvent = |
| [NSClassFromString(@"UIPhysicalKeyboardEvent") _eventWithInput:input |
| inputFlags:0]; |
| if (@available(iOS 17, *)) { |
| [keyboardEvent _setModifierFlags:flags]; |
| } else { |
| keyboardEvent._modifierFlags = flags; |
| } |
| IOHIDEventRef hidEvent = |
| CreateHIDKeyEvent(input, keyboardEvent.timestamp, true); |
| [keyboardEvent _setHIDEvent:hidEvent keyboard:0]; |
| [[UIApplication sharedApplication] handleKeyUIEvent:keyboardEvent]; |
| } |
| |
| // Simulate pressing a hardware keyboard key. |
| void PressKey(NSString* key) { |
| IOHIDEventRef event = CreateHIDKeyEvent(key, mach_absolute_time(), true); |
| uint32_t contextId = GetContextId(); |
| BKSHIDEventSetDigitizerInfo(event, contextId, false, false, NULL, 0, 0); |
| [[UIApplication sharedApplication] _enqueueHIDEvent:event]; |
| } |
| |
| // Simulate releasing a hardware keyboard key. |
| void ReleaseKey(NSString* key) { |
| IOHIDEventRef event = CreateHIDKeyEvent(key, mach_absolute_time(), false); |
| uint32_t contextId = GetContextId(); |
| BKSHIDEventSetDigitizerInfo(event, contextId, false, false, NULL, 0, 0); |
| [[UIApplication sharedApplication] _enqueueHIDEvent:event]; |
| } |
| |
| // Lifts the keypresses one by one. |
| // Once all keypresses are reversed, executes |completion| on the main thread. |
| void UnwindFakeKeyboardPressWithFlags(UIKeyModifierFlags flags, |
| NSString* input, |
| void (^completion)()) { |
| if (flags == 0 && input.length == 0) { |
| if (completion) { |
| completion(); |
| } |
| return; |
| } |
| |
| auto block = ^(NSTimer* timer) { |
| // First release all the non-modifier |
| // keys. |
| if (input.length > 0) { |
| NSString* remainingInput = [input substringFromIndex:1]; |
| |
| SendKBEventWithModifiers(flags, remainingInput); |
| UnwindFakeKeyboardPressWithFlags(flags, remainingInput, completion); |
| return; |
| } |
| |
| // Unwind the modifier keys. |
| for (int i = 16; i < 22; i++) { |
| UIKeyModifierFlags flag = 1 << i; |
| if (flags & flag) { |
| SendKBEventWithModifiers(flags & ~flag, input); |
| UnwindFakeKeyboardPressWithFlags(flags & ~flag, input, completion); |
| } |
| } |
| }; |
| [NSTimer scheduledTimerWithTimeInterval:kKeyPressDelay |
| repeats:false |
| block:block]; |
| } |
| |
| // Programmatically simulates pressing the keys one by one, starting with |
| // modifier keys and moving to input string through recursion, then calls |
| // unwindFakeKeyboardPressWithFlags to release the pressed keys in reverse |
| // order. |
| // Once all key downs and key ups are simulated, executes |completion| on the |
| // main thread. |
| void SimulatePhysicalKeyboardEventInternal(UIKeyModifierFlags flags, |
| NSString* input, |
| UIKeyModifierFlags previousFlags, |
| NSString* previousInput, |
| void (^completion)()) { |
| auto block = ^(NSTimer* timer) { |
| // First dial in all the modifier keys. |
| for (int i = 15; i < 25; i++) { |
| UIKeyModifierFlags flag = 1 << i; |
| if (flags & flag) { |
| SendKBEventWithModifiers(previousFlags ^ flag, previousInput); |
| SimulatePhysicalKeyboardEventInternal(flags & ~flag, input, |
| previousFlags ^ flag, |
| previousInput, completion); |
| return; |
| } |
| } |
| |
| // Now add the next input char. |
| if (input.length > 0) { |
| NSString* pressedKey = [input substringToIndex:1]; |
| NSString* remainingInput = [input substringFromIndex:1]; |
| NSString* alreadyPressedString = |
| [previousInput stringByAppendingString:pressedKey]; |
| |
| SendKBEventWithModifiers(previousFlags, alreadyPressedString); |
| SimulatePhysicalKeyboardEventInternal(flags, remainingInput, |
| previousFlags, alreadyPressedString, |
| completion); |
| } else { |
| // Time to unwind the presses. |
| UnwindFakeKeyboardPressWithFlags(previousFlags, previousInput, |
| completion); |
| } |
| }; |
| [NSTimer scheduledTimerWithTimeInterval:kKeyPressDelay |
| repeats:false |
| block:block]; |
| } |
| |
| } // namespace |
| |
| #pragma mark - Public |
| |
| namespace chrome_test_util { |
| |
| void SimulatePhysicalKeyboardEvent(UIKeyModifierFlags flags, NSString* input) { |
| // No modifier keys pressed. |
| if (flags == 0) { |
| if (hidUsageCodeForCharacter(input)) { |
| // Input is a keyboard key. |
| PressKey(input); |
| ReleaseKey(input); |
| return; |
| } |
| |
| // If there are no modifier keys and the input is not a keyboard key. Our |
| // intention is then to type the input. |
| for (NSUInteger i = 0; i < [input length]; i++) { |
| NSRange range = NSMakeRange(i, 1); |
| NSString* key = [input substringWithRange:range]; |
| PressKey(key); |
| ReleaseKey(key); |
| } |
| return; |
| } |
| |
| // Input with modifier keys. |
| __block BOOL keyPressesFinished = NO; |
| |
| SimulatePhysicalKeyboardEventInternal(flags, input, 0, @"", ^{ |
| keyPressesFinished = YES; |
| }); |
| |
| BOOL __unused result = |
| base::test::ios::WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^{ |
| return keyPressesFinished; |
| }); |
| } |
| |
| } // namespace chrome_test_util |