Skip to content

Commit b83eabc

Browse files
moffatmandkwingsmt
andauthored
[iOS] Fix duplicated keys when typing quickly on HW keyboard (flutter#29113)
* [iOS] Fix duplicated keys when typing quickly on HW keyboard * Address review (refactor constant) * Change message loop constant name * Apply suggestions from code review Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
1 parent 3ca67ac commit b83eabc

File tree

5 files changed

+113
-15
lines changed

5 files changed

+113
-15
lines changed

fml/platform/darwin/message_loop_darwin.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
namespace fml {
1717

1818
class MessageLoopDarwin : public MessageLoopImpl {
19+
public:
20+
// A custom CFRunLoop mode used when processing flutter messages,
21+
// so that the CFRunLoop can be run without being interrupted by UIKit,
22+
// while still being able to receive and be interrupted by framework messages.
23+
static CFStringRef kMessageLoopCFRunLoopMode;
24+
1925
private:
2026
std::atomic_bool running_;
2127
CFRef<CFRunLoopTimerRef> delayed_wake_timer_;

fml/platform/darwin/message_loop_darwin.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
static constexpr CFTimeInterval kDistantFuture = 1.0e10;
1515

16+
CFStringRef MessageLoopDarwin::kMessageLoopCFRunLoopMode = CFSTR("fmlMessageLoop");
17+
1618
MessageLoopDarwin::MessageLoopDarwin()
1719
: running_(false), loop_((CFRunLoopRef)CFRetain(CFRunLoopGetCurrent())) {
1820
FML_DCHECK(loop_ != nullptr);
@@ -29,11 +31,14 @@
2931
&timer_context /* context */));
3032
FML_DCHECK(delayed_wake_timer_ != nullptr);
3133
CFRunLoopAddTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes);
34+
// This mode will be used by FlutterKeyboardManager.
35+
CFRunLoopAddTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode);
3236
}
3337

3438
MessageLoopDarwin::~MessageLoopDarwin() {
3539
CFRunLoopTimerInvalidate(delayed_wake_timer_);
3640
CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes);
41+
CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode);
3742
}
3843

3944
void MessageLoopDarwin::Run() {

shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
66
#include "flutter/fml/memory/weak_ptr.h"
7+
#include "flutter/fml/platform/darwin/message_loop_darwin.h"
8+
9+
static constexpr CFTimeInterval kDistantFuture = 1.0e10;
710

811
@interface FlutterKeyboardManager ()
912

@@ -102,7 +105,10 @@ - (void)handlePress:(nonnull FlutterUIPressProxy*)press
102105
// framework. Once the completeCallback is called, this run loop will exit
103106
// and the main one will resume. The completeCallback MUST be called, or
104107
// the app will get stuck in this run loop indefinitely.
105-
CFRunLoopRun();
108+
//
109+
// We need to run in this mode so that UIKit doesn't give us new
110+
// events until we are done processing this one.
111+
CFRunLoopRunInMode(fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, kDistantFuture, NO);
106112
break;
107113
}
108114
case UIPressPhaseChanged:

shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#import <XCTest/XCTest.h>
99
#include <_types/_uint32_t.h>
1010

11+
#include "flutter/fml/platform/darwin/message_loop_darwin.h"
1112
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
1213
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
1314
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
@@ -59,7 +60,8 @@ - (bool)emptyNextResponder;
5960

6061
namespace {
6162

62-
typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback);
63+
typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
64+
API_AVAILABLE(ios(13.4));
6365
typedef BOOL (^BoolGetter)();
6466

6567
} // namespace
@@ -100,11 +102,14 @@ - (id)checkKeyDownEvent:(UIKeyboardHIDUsage)keyCode API_AVAILABLE(ios(13.4)) {
100102
OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder));
101103
OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]])
102104
.andDo((^(NSInvocation* invocation) {
105+
FlutterUIPressProxy* press;
103106
FlutterAsyncKeyCallback callback;
107+
[invocation getArgument:&press atIndex:2];
104108
[invocation getArgument:&callback atIndex:3];
105-
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
106-
callbackSetter(callback);
107-
});
109+
CFRunLoopPerformBlock(CFRunLoopGetCurrent(),
110+
fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() {
111+
callbackSetter(press, callback);
112+
});
108113
}));
109114
return mock;
110115
}
@@ -149,7 +154,8 @@ - (void)testSinglePrimaryResponder API_AVAILABLE(ios(13.4)) {
149154
FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
150155
__block BOOL primaryResponse = FALSE;
151156
__block int callbackCount = 0;
152-
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
157+
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
158+
FlutterAsyncKeyCallback callback) {
153159
callbackCount++;
154160
callback(primaryResponse);
155161
}]];
@@ -181,14 +187,16 @@ - (void)testDoublePrimaryResponder API_AVAILABLE(ios(13.4)) {
181187

182188
__block BOOL callback1Response = FALSE;
183189
__block int callback1Count = 0;
184-
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
190+
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
191+
FlutterAsyncKeyCallback callback) {
185192
callback1Count++;
186193
callback(callback1Response);
187194
}]];
188195

189196
__block BOOL callback2Response = FALSE;
190197
__block int callback2Count = 0;
191-
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
198+
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
199+
FlutterAsyncKeyCallback callback) {
192200
callback2Count++;
193201
callback(callback2Response);
194202
}]];
@@ -242,7 +250,8 @@ - (void)testSingleSecondaryResponder API_AVAILABLE(ios(13.4)) {
242250

243251
__block BOOL primaryResponse = FALSE;
244252
__block int callbackCount = 0;
245-
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
253+
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
254+
FlutterAsyncKeyCallback callback) {
246255
callbackCount++;
247256
callback(primaryResponse);
248257
}]];
@@ -291,4 +300,73 @@ - (void)testSingleSecondaryResponder API_AVAILABLE(ios(13.4)) {
291300
XCTAssertFalse(completeHandled);
292301
}
293302

303+
- (void)testEventsProcessedSequentially API_AVAILABLE(ios(13.4)) {
304+
constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50;
305+
constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51;
306+
FlutterUIPressProxy* event1 = keyDownEvent(keyId1);
307+
FlutterUIPressProxy* event2 = keyDownEvent(keyId2);
308+
__block FlutterAsyncKeyCallback key1Callback;
309+
__block FlutterAsyncKeyCallback key2Callback;
310+
__block bool key1Handled = true;
311+
__block bool key2Handled = true;
312+
313+
FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
314+
[manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
315+
FlutterAsyncKeyCallback callback) {
316+
if (press == event1) {
317+
key1Callback = callback;
318+
} else if (press == event2) {
319+
key2Callback = callback;
320+
}
321+
}]];
322+
323+
// Add both presses into the main CFRunLoop queue
324+
CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler(
325+
kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
326+
[manager handlePress:event1
327+
nextAction:^() {
328+
key1Handled = false;
329+
}];
330+
});
331+
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes);
332+
CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler(
333+
kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
334+
// key1 should be completely finished by now
335+
XCTAssertFalse(key1Handled);
336+
[manager handlePress:event2
337+
nextAction:^() {
338+
key2Handled = false;
339+
}];
340+
// End the nested CFRunLoop
341+
CFRunLoopStop(CFRunLoopGetCurrent());
342+
});
343+
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes);
344+
345+
// Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode
346+
// This allows them to interrupt the loop started within handlePress
347+
CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler(
348+
kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
349+
// No processing should be done on key2 yet
350+
XCTAssertTrue(key1Callback != nil);
351+
XCTAssertTrue(key2Callback == nil);
352+
key1Callback(false);
353+
});
354+
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2,
355+
fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
356+
CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler(
357+
kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
358+
// Both keys should be processed by now
359+
XCTAssertTrue(key1Callback != nil);
360+
XCTAssertTrue(key2Callback != nil);
361+
key2Callback(false);
362+
});
363+
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3,
364+
fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
365+
366+
// Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test
367+
CFRunLoopRun();
368+
XCTAssertFalse(key2Handled);
369+
XCTAssertFalse(key1Handled);
370+
}
371+
294372
@end

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#import <OCMock/OCMock.h>
66
#import <XCTest/XCTest.h>
77

8+
#include "flutter/fml/platform/darwin/message_loop_darwin.h"
89
#import "flutter/lib/ui/window/viewport_metrics.h"
910
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
1011
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
@@ -59,9 +60,10 @@ - (void)sendKeyEvent:(const FlutterKeyEvent&)event
5960
// NSAssert(callback != nullptr, @"Invalid callback");
6061
// Response is async, so we have to post it to the run loop instead of calling
6162
// it directly.
62-
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
63-
callback(true, userData);
64-
});
63+
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
64+
^() {
65+
callback(true, userData);
66+
});
6567
}
6668
@end
6769

@@ -763,9 +765,10 @@ - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback
763765
// Response is async, so we have to post it to the run loop instead of calling
764766
// it directly.
765767
self.messageSent = message;
766-
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
767-
callback(replyMessage);
768-
});
768+
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
769+
^() {
770+
callback(replyMessage);
771+
});
769772
}
770773

771774
- (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {

0 commit comments

Comments
 (0)