Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions shell/platform/darwin/ios/framework/Headers/FlutterEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,14 @@ FLUTTER_EXPORT
*/
@property(nonatomic, readonly) FlutterBasicMessageChannel* settingsChannel;

/**
* The `FlutterBasicMessageChannel` used for communicating key events
* from physical keyboards
*
* Can be nil after `destroyContext` is called.
*/
@property(nonatomic, readonly) FlutterBasicMessageChannel* keyEventChannel;

/**
* The `NSURL` of the observatory for the service isolate.
*
Expand Down
10 changes: 10 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ @implementation FlutterEngine {
fml::scoped_nsobject<FlutterBasicMessageChannel> _lifecycleChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _systemChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _settingsChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _keyEventChannel;

int64_t _nextTextureId;

Expand Down Expand Up @@ -350,6 +351,9 @@ - (FlutterBasicMessageChannel*)systemChannel {
- (FlutterBasicMessageChannel*)settingsChannel {
return _settingsChannel.get();
}
- (FlutterBasicMessageChannel*)keyEventChannel {
return _keyEventChannel.get();
}

- (NSURL*)observatoryUrl {
return [_publisher.get() url];
Expand All @@ -364,6 +368,7 @@ - (void)resetChannels {
_lifecycleChannel.reset();
_systemChannel.reset();
_settingsChannel.reset();
_keyEventChannel.reset();
}

- (void)startProfiler:(NSString*)threadLabel {
Expand Down Expand Up @@ -436,6 +441,11 @@ - (void)setupChannels {
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMessageCodec sharedInstance]]);

_keyEventChannel.reset([[FlutterBasicMessageChannel alloc]
initWithName:@"flutter/keyevent"
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMessageCodec sharedInstance]]);

_textInputPlugin.reset([[FlutterTextInputPlugin alloc] init]);
_textInputPlugin.get().textInputDelegate = self;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,58 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification {
[self updateViewportMetrics];
}

- (void)dispatchPresses:(NSSet<UIPress*>*)presses API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
for (UIPress* press in presses) {
if (press.key == nil || press.phase == UIPressPhaseStationary ||
press.phase == UIPressPhaseChanged) {
continue;
}
NSMutableDictionary* keyMessage = [@{
@"keymap" : @"ios",
@"type" : @"unknown",
@"keyCode" : @(press.key.keyCode),
@"modifiers" : @(press.key.modifierFlags),
@"characters" : press.key.characters,
@"charactersIgnoringModifiers" : press.key.charactersIgnoringModifiers
} mutableCopy];

if (press.phase == UIPressPhaseBegan) {
keyMessage[@"type"] = @"keydown";
} else if (press.phase == UIPressPhaseEnded || press.phase == UIPressPhaseCancelled) {
keyMessage[@"type"] = @"keyup";
}

[[_engine.get() keyEventChannel] sendMessage:keyMessage];
}
}
}

- (void)pressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
if (@available(iOS 13.4, *)) {
[self dispatchPresses:presses];
}
}

- (void)pressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
if (@available(iOS 13.4, *)) {
[self dispatchPresses:presses];
}
}

- (void)pressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
if (@available(iOS 13.4, *)) {
[self dispatchPresses:presses];
}
}

- (void)pressesCancelled:(NSSet<UIPress*>*)presses
withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) {
if (@available(iOS 13.4, *)) {
[self dispatchPresses:presses];
}
}

#pragma mark - Orientation updates

- (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ - (UIAccessibilityContrast)accessibilityContrast;
@interface FlutterViewController (Tests)
- (void)surfaceUpdated:(BOOL)appeared;
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
- (void)dispatchPresses:(NSSet<UIPress*>*)presses;
@end

@implementation FlutterViewControllerTest
Expand Down Expand Up @@ -549,4 +550,150 @@ - (void)testNotifyLowMemory {
OCMVerify([engine notifyLowMemory]);
}

- (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
// noop
} else {
return;
}

id engine = OCMClassMock([FlutterEngine class]);

id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

id testSet = [self fakeUiPressSetForPhase:UIPressPhaseBegan
keyCode:UIKeyboardHIDUsageKeyboardA
modifierFlags:UIKeyModifierShift
characters:@"a"
charactersIgnoringModifiers:@"A"];

// Exercise behavior under test.
[vc dispatchPresses:testSet];

// Verify behavior.
OCMVerify([keyEventChannel
sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"keymap"] isEqualToString:@"ios"] &&
[message[@"type"] isEqualToString:@"keydown"] &&
[message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] &&
[message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] &&
[message[@"characters"] isEqualToString:@"a"] &&
[message[@"charactersIgnoringModifiers"] isEqualToString:@"A"];
}]]);

// Clean up mocks
[engine stopMocking];
[keyEventChannel stopMocking];
}

- (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
// noop
} else {
return;
}

id engine = OCMClassMock([FlutterEngine class]);

id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

id testSet = [self fakeUiPressSetForPhase:UIPressPhaseEnded
keyCode:UIKeyboardHIDUsageKeyboardA
modifierFlags:UIKeyModifierShift
characters:@"a"
charactersIgnoringModifiers:@"A"];

// Exercise behavior under test.
[vc dispatchPresses:testSet];

// Verify behavior.
OCMVerify([keyEventChannel
sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
return [message[@"keymap"] isEqualToString:@"ios"] &&
[message[@"type"] isEqualToString:@"keyup"] &&
[message[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]] &&
[message[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:131072]] &&
[message[@"characters"] isEqualToString:@"a"] &&
[message[@"charactersIgnoringModifiers"] isEqualToString:@"A"];
}]]);

// Clean up mocks
[engine stopMocking];
[keyEventChannel stopMocking];
}

- (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
// noop
} else {
return;
}

id engine = OCMClassMock([FlutterEngine class]);

id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
OCMStub([engine keyEventChannel]).andReturn(keyEventChannel);

FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];

id emptySet = [NSSet set];
id ignoredSet = [self fakeUiPressSetForPhase:UIPressPhaseStationary
keyCode:UIKeyboardHIDUsageKeyboardA
modifierFlags:UIKeyModifierShift
characters:@"a"
charactersIgnoringModifiers:@"A"];

id mockUiPress = OCMClassMock([UIPress class]);
OCMStub([mockUiPress phase]).andReturn(UIPressPhaseBegan);
id emptyKeySet = [NSSet setWithArray:@[ mockUiPress ]];
// Exercise behavior under test.
[vc dispatchPresses:emptySet];
[vc dispatchPresses:ignoredSet];
[vc dispatchPresses:emptyKeySet];

// Verify behavior.
OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);

// Clean up mocks
[engine stopMocking];
[keyEventChannel stopMocking];
}

- (NSSet<UIPress*>*)fakeUiPressSetForPhase:(UIPressPhase)phase
keyCode:(UIKeyboardHIDUsage)keyCode
modifierFlags:(UIKeyModifierFlags)modifierFlags
characters:(NSString*)characters
charactersIgnoringModifiers:(NSString*)charactersIgnoringModifiers
API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
// noop
} else {
return [NSSet set];
}
id mockUiPress = OCMClassMock([UIPress class]);
OCMStub([mockUiPress phase]).andReturn(phase);

id mockUiKey = OCMClassMock([UIKey class]);
OCMStub([mockUiKey keyCode]).andReturn(keyCode);
OCMStub([mockUiKey modifierFlags]).andReturn(modifierFlags);
OCMStub([mockUiKey characters]).andReturn(characters);
OCMStub([mockUiKey charactersIgnoringModifiers]).andReturn(charactersIgnoringModifiers);

OCMStub([mockUiPress key]).andReturn(mockUiKey);

return [NSSet setWithArray:@[ mockUiPress ]];
}

@end