From 8e212ddf55833ea0d035ecf5a02366bad3eb531a Mon Sep 17 00:00:00 2001 From: Tobias Kammerer Date: Tue, 1 Sep 2020 20:25:29 +0200 Subject: [PATCH 1/5] added keyevent support for iOS This is based on the UIKey introduction in 13.4 --- .../ios/framework/Headers/FlutterEngine.h | 8 ++++ .../ios/framework/Source/FlutterEngine.mm | 10 ++++ .../framework/Source/FlutterViewController.mm | 48 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 87b7753317e73..409ca99359bc4 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -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. * diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 13394a976c92c..59c2901d3138f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -77,6 +77,7 @@ @implementation FlutterEngine { fml::scoped_nsobject _lifecycleChannel; fml::scoped_nsobject _systemChannel; fml::scoped_nsobject _settingsChannel; + fml::scoped_nsobject _keyEventChannel; int64_t _nextTextureId; @@ -326,6 +327,9 @@ - (FlutterBasicMessageChannel*)systemChannel { - (FlutterBasicMessageChannel*)settingsChannel { return _settingsChannel.get(); } +- (FlutterBasicMessageChannel*)keyEventChannel { + return _keyEventChannel.get(); +} - (NSURL*)observatoryUrl { return [_publisher.get() url]; @@ -340,6 +344,7 @@ - (void)resetChannels { _lifecycleChannel.reset(); _systemChannel.reset(); _settingsChannel.reset(); + _keyEventChannel.reset(); } - (void)startProfiler:(NSString*)threadLabel { @@ -411,6 +416,11 @@ - (void)setupChannels { initWithName:@"flutter/settings" 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; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 6114281d8695b..a45ad7a5be8e9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -964,6 +964,54 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification { [self updateViewportMetrics]; } +- (void)dispatchPresses:(NSSet *)presses API_AVAILABLE(ios(9.0)) { + if (@available(iOS 9, *)) { + for (UIPress* press in presses) { + if (press.key == nil) { 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 *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesChanged:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesEnded:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { + if (@available(iOS 13.4, *)) { + [self dispatchPresses:presses]; + } +} + +- (void)pressesCancelled:(NSSet *)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 { From 068bb22de637914a0e8f8bb0233fe98aff68c8f6 Mon Sep 17 00:00:00 2001 From: Tobias Kammerer Date: Thu, 3 Sep 2020 15:43:21 +0200 Subject: [PATCH 2/5] formatting and version check fix --- .../ios/framework/Source/FlutterEngine.mm | 4 ++-- .../framework/Source/FlutterViewController.mm | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 59c2901d3138f..000c5aebb1836 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -416,9 +416,9 @@ - (void)setupChannels { initWithName:@"flutter/settings" binaryMessenger:self.binaryMessenger codec:[FlutterJSONMessageCodec sharedInstance]]); - + _keyEventChannel.reset([[FlutterBasicMessageChannel alloc] - initWithName:@"flutter/keyevent" + initWithName:@"flutter/keyevent" binaryMessenger:self.binaryMessenger codec:[FlutterJSONMessageCodec sharedInstance]]); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index a45ad7a5be8e9..51f27645d9f8d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -964,13 +964,15 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification { [self updateViewportMetrics]; } -- (void)dispatchPresses:(NSSet *)presses API_AVAILABLE(ios(9.0)) { - if (@available(iOS 9, *)) { +- (void)dispatchPresses:(NSSet*)presses API_AVAILABLE(ios(13.4)) { + if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - if (press.key == nil) { continue; } + if (press.key == nil) { + continue; + } NSMutableDictionary* keyMessage = [@{ @"keymap" : @"ios", - @"type": @"unknown", + @"type" : @"unknown", @"keyCode" : @(press.key.keyCode), @"modifiers" : @(press.key.modifierFlags), @"characters" : press.key.characters, @@ -988,25 +990,26 @@ - (void)dispatchPresses:(NSSet *)presses API_AVAILABLE(ios(9.0)) { } } -- (void)pressesBegan:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { +- (void)pressesBegan:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { [self dispatchPresses:presses]; } } -- (void)pressesChanged:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { +- (void)pressesChanged:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { [self dispatchPresses:presses]; } } -- (void)pressesEnded:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { +- (void)pressesEnded:(NSSet*)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { [self dispatchPresses:presses]; } } -- (void)pressesCancelled:(NSSet *)presses withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { +- (void)pressesCancelled:(NSSet*)presses + withEvent:(UIEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { [self dispatchPresses:presses]; } From 743c9e9b865a886e4453a2fd00fb0a6ffa3a4fd1 Mon Sep 17 00:00:00 2001 From: Tobias Kammerer Date: Wed, 9 Sep 2020 00:56:41 +0200 Subject: [PATCH 3/5] ignored unused press phases --- .../darwin/ios/framework/Source/FlutterViewController.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 51f27645d9f8d..d7515bc24fa43 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -967,7 +967,8 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification { - (void)dispatchPresses:(NSSet*)presses API_AVAILABLE(ios(13.4)) { if (@available(iOS 13.4, *)) { for (UIPress* press in presses) { - if (press.key == nil) { + if (press.key == nil || press.phase == UIPressPhaseStationary || + press.phase == UIPressPhaseChanged) { continue; } NSMutableDictionary* keyMessage = [@{ From 285b6c8130596a14e64505f8a80e18352e749084 Mon Sep 17 00:00:00 2001 From: Tobias Kammerer Date: Sat, 17 Oct 2020 21:41:07 +0200 Subject: [PATCH 4/5] added tests --- .../Source/FlutterViewControllerTest.mm | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index f15ea37666319..82d7d24dd5fbc 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -62,6 +62,7 @@ - (UIAccessibilityContrast)accessibilityContrast; @interface FlutterViewController (Tests) - (void)surfaceUpdated:(BOOL)appeared; - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; +- (void)dispatchPresses:(NSSet*)presses; @end @implementation FlutterViewControllerTest @@ -549,4 +550,140 @@ - (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 fakeUiPressSet: + UIPressPhaseBegan:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"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 fakeUiPressSet: + UIPressPhaseEnded:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"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 fakeUiPressSet: + UIPressPhaseStationary:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"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*)fakeUiPressSet:(UIPressPhase) + phase:(UIKeyboardHIDUsage)keyCode + :(UIKeyModifierFlags)modifierFlags + :(NSString*)characters + :(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 From c44c8c155f2a2229b591a02a6f03b38f821634bb Mon Sep 17 00:00:00 2001 From: Tobias Kammerer Date: Sun, 18 Oct 2020 00:29:24 +0200 Subject: [PATCH 5/5] fixed compile errors --- .../Source/FlutterViewControllerTest.mm | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 82d7d24dd5fbc..9f4348dd6812f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -566,8 +566,11 @@ - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) { nibName:nil bundle:nil]; - id testSet = [self fakeUiPressSet: - UIPressPhaseBegan:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"A"]; + id testSet = [self fakeUiPressSetForPhase:UIPressPhaseBegan + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; // Exercise behavior under test. [vc dispatchPresses:testSet]; @@ -604,8 +607,11 @@ - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) { nibName:nil bundle:nil]; - id testSet = [self fakeUiPressSet: - UIPressPhaseEnded:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"A"]; + id testSet = [self fakeUiPressSetForPhase:UIPressPhaseEnded + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; // Exercise behavior under test. [vc dispatchPresses:testSet]; @@ -643,8 +649,11 @@ - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) { bundle:nil]; id emptySet = [NSSet set]; - id ignoredSet = [self fakeUiPressSet: - UIPressPhaseStationary:UIKeyboardHIDUsageKeyboardA:UIKeyModifierShift:@"a":@"A"]; + id ignoredSet = [self fakeUiPressSetForPhase:UIPressPhaseStationary + keyCode:UIKeyboardHIDUsageKeyboardA + modifierFlags:UIKeyModifierShift + characters:@"a" + charactersIgnoringModifiers:@"A"]; id mockUiPress = OCMClassMock([UIPress class]); OCMStub([mockUiPress phase]).andReturn(UIPressPhaseBegan); @@ -662,11 +671,12 @@ - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) { [keyEventChannel stopMocking]; } -- (NSSet*)fakeUiPressSet:(UIPressPhase) - phase:(UIKeyboardHIDUsage)keyCode - :(UIKeyModifierFlags)modifierFlags - :(NSString*)characters - :(NSString*)charactersIgnoringModifiers API_AVAILABLE(ios(13.4)) { +- (NSSet*)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 {