// 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