Skip to content

Commit ee71e31

Browse files
authored
[Windows, Keyboard] Fix crash on Up-only IME event (flutter#33746)
* Impl and test * Format and some doc * Rewrite algorithm * Format * Simplify * Fix * Fix params
1 parent b22cdd4 commit ee71e31

File tree

3 files changed

+147
-63
lines changed

3 files changed

+147
-63
lines changed

shell/platform/windows/keyboard_key_embedder_handler.cc

Lines changed: 103 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -162,25 +162,60 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl(
162162
const uint64_t logical_key = GetLogicalKey(key, extended, scancode);
163163
assert(action == WM_KEYDOWN || action == WM_KEYUP ||
164164
action == WM_SYSKEYDOWN || action == WM_SYSKEYUP);
165-
const bool is_physical_down = action == WM_KEYDOWN || action == WM_SYSKEYDOWN;
166165

167166
auto last_logical_record_iter = pressingRecords_.find(physical_key);
168167
const bool had_record = last_logical_record_iter != pressingRecords_.end();
169168
const uint64_t last_logical_record =
170169
had_record ? last_logical_record_iter->second : 0;
171170

171+
// The logical key for the current "tap sequence".
172+
//
173+
// Key events are formed in tap sequences: down, repeats, up. The logical key
174+
// stays consistent throughout a tap sequence, which is this value.
175+
uint64_t sequence_logical_key =
176+
had_record ? last_logical_record : logical_key;
177+
178+
if (sequence_logical_key == VK_PROCESSKEY) {
179+
// VK_PROCESSKEY means that the key press is used by an IME. These key
180+
// presses are considered handled and not sent to Flutter. These events must
181+
// be filtered by result_logical_key because the key up event of such
182+
// presses uses the "original" logical key.
183+
callback(true);
184+
return;
185+
}
186+
187+
const bool is_event_down = action == WM_KEYDOWN || action == WM_SYSKEYDOWN;
188+
189+
UpdateLastSeenCritialKey(key, physical_key, sequence_logical_key);
190+
// Synchronize the toggled states of critical keys (such as whether CapsLocks
191+
// is enabled). Toggled states can only be changed upon a down event, so if
192+
// the recorded toggled state does not match the true state, this function
193+
// will synthesize (an up event if the key is recorded pressed, then) a down
194+
// event.
195+
//
196+
// After this function, all critical keys will have their toggled state
197+
// updated to the true state, while the critical keys whose toggled state have
198+
// been changed will be pressed regardless of their true pressed state.
199+
// Updating the pressed state will be done by SynchronizeCritialPressedStates.
200+
SynchronizeCritialToggledStates(key, is_event_down);
201+
// Synchronize the pressed states of critical keys (such as whether CapsLocks
202+
// is pressed).
203+
//
204+
// After this function, all critical keys except for the target key will have
205+
// their toggled state and pressed state matched with their true states. The
206+
// target key's pressed state will be updated immediately after this.
207+
SynchronizeCritialPressedStates(key, physical_key, is_event_down);
208+
172209
// The resulting event's `type`.
173210
FlutterKeyEventType type;
174-
// The resulting event's `logical_key`.
175-
uint64_t result_logical_key;
211+
character = UndeadChar(character);
212+
char character_bytes[kCharacterCacheSize];
176213
// What pressingRecords_[physical_key] should be after the KeyboardHookImpl
177214
// returns (0 if the entry should be removed).
178215
uint64_t eventual_logical_record;
179-
char character_bytes[kCharacterCacheSize];
180-
181-
character = UndeadChar(character);
216+
uint64_t result_logical_key;
182217

183-
if (is_physical_down) {
218+
if (is_event_down) {
184219
if (had_record) {
185220
if (was_down) {
186221
// A normal repeated key.
@@ -223,35 +258,6 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl(
223258
}
224259
}
225260

226-
if (result_logical_key == VK_PROCESSKEY) {
227-
// VK_PROCESSKEY means that the key press is used by an IME. These key
228-
// presses are considered handled and not sent to Flutter. These events must
229-
// be filtered by result_logical_key because the key up event of such
230-
// presses uses the "original" logical key.
231-
callback(true);
232-
return;
233-
}
234-
235-
UpdateLastSeenCritialKey(key, physical_key, result_logical_key);
236-
// Synchronize the toggled states of critical keys (such as whether CapsLocks
237-
// is enabled). Toggled states can only be changed upon a down event, so if
238-
// the recorded toggled state does not match the true state, this function
239-
// will synthesize (an up event if the key is recorded pressed, then) a down
240-
// event.
241-
//
242-
// After this function, all critical keys will have their toggled state
243-
// updated to the true state, while the critical keys whose toggled state have
244-
// been changed will be pressed regardless of their true pressed state.
245-
// Updating the pressed state will be done by SynchronizeCritialPressedStates.
246-
SynchronizeCritialToggledStates(key, type == kFlutterKeyEventTypeDown);
247-
// Synchronize the pressed states of critical keys (such as whether CapsLocks
248-
// is pressed).
249-
//
250-
// After this function, all critical keys except for the target key will have
251-
// their toggled state and pressed state matched with their true states. The
252-
// target key's pressed state will be updated immediately after this.
253-
SynchronizeCritialPressedStates(key, type != kFlutterKeyEventTypeRepeat);
254-
255261
if (eventual_logical_record != 0) {
256262
pressingRecords_[physical_key] = eventual_logical_record;
257263
} else {
@@ -333,8 +339,10 @@ void KeyboardKeyEmbedderHandler::UpdateLastSeenCritialKey(
333339
}
334340

335341
void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates(
336-
int this_virtual_key,
337-
bool is_down_event) {
342+
int event_virtual_key,
343+
bool is_event_down) {
344+
// NowState ----------------> PreEventState --------------> TrueState
345+
// Synchronization Event
338346
for (auto& kv : critical_keys_) {
339347
UINT virtual_key = kv.first;
340348
CriticalKey& key_info = kv.second;
@@ -346,21 +354,26 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates(
346354

347355
// Check toggling state first, because it might alter pressing state.
348356
if (key_info.check_toggled) {
357+
const bool target_is_pressed =
358+
pressingRecords_.find(key_info.physical_key) !=
359+
pressingRecords_.end();
349360
// The togglable keys observe a 4-phase cycle:
350361
//
351362
// Phase# 0 1 2 3
352363
// Event Down Up Down Up
353364
// Pressed 0 1 0 1
354365
// Toggled 0 1 1 0
355-
SHORT state = get_key_state_(virtual_key);
356-
bool should_toggled = state & kStateMaskToggled;
357-
if (virtual_key == this_virtual_key && is_down_event) {
358-
key_info.toggled_on = !key_info.toggled_on;
366+
const bool true_toggled = get_key_state_(virtual_key) & kStateMaskToggled;
367+
bool pre_event_toggled = true_toggled;
368+
// Check if the main event's key is the key being checked. If it's the
369+
// non-repeat down event, toggle the state.
370+
if (virtual_key == event_virtual_key && !target_is_pressed &&
371+
is_event_down) {
372+
pre_event_toggled = !pre_event_toggled;
359373
}
360-
if (key_info.toggled_on != should_toggled) {
374+
if (key_info.toggled_on != pre_event_toggled) {
361375
// If the key is pressed, release it first.
362-
if (pressingRecords_.find(key_info.physical_key) !=
363-
pressingRecords_.end()) {
376+
if (target_is_pressed) {
364377
SendEvent(SynthesizeSimpleEvent(
365378
kFlutterKeyEventTypeUp, key_info.physical_key,
366379
key_info.logical_key, empty_character),
@@ -373,14 +386,26 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates(
373386
key_info.logical_key, empty_character),
374387
nullptr, nullptr);
375388
}
376-
key_info.toggled_on = should_toggled;
389+
key_info.toggled_on = true_toggled;
377390
}
378391
}
379392
}
380393

381394
void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates(
382-
int this_virtual_key,
383-
bool pressed_state_will_change) {
395+
int event_virtual_key,
396+
int event_physical_key,
397+
bool is_event_down) {
398+
// During an incoming event, there might be a synthesized Flutter event for
399+
// each key of each pressing goal, followed by an eventual main Flutter
400+
// event.
401+
//
402+
// NowState ----------------> PreEventState --------------> TrueState
403+
// Synchronization Event
404+
//
405+
// The goal of the synchronization algorithm is to derive a pre-event state
406+
// that can satisfy the true state (`true_pressed`) after the event, and that
407+
// requires as few synthesized events based on the current state
408+
// (`now_pressed`) as possible.
384409
for (auto& kv : critical_keys_) {
385410
UINT virtual_key = kv.first;
386411
CriticalKey& key_info = kv.second;
@@ -390,25 +415,42 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates(
390415
}
391416
assert(key_info.logical_key != 0);
392417
if (key_info.check_pressed) {
393-
SHORT state = get_key_state_(virtual_key);
394-
auto recorded_pressed_iter = pressingRecords_.find(key_info.physical_key);
395-
bool recorded_pressed = recorded_pressed_iter != pressingRecords_.end();
396-
bool should_pressed = state & kStateMaskPressed;
397-
if (virtual_key == this_virtual_key && pressed_state_will_change) {
398-
should_pressed = !should_pressed;
418+
SHORT true_pressed = get_key_state_(virtual_key) & kStateMaskPressed;
419+
auto pressing_record_iter = pressingRecords_.find(key_info.physical_key);
420+
bool now_pressed = pressing_record_iter != pressingRecords_.end();
421+
bool pre_event_pressed = true_pressed;
422+
// Check if the main event is the key being checked to get the correct
423+
// target state.
424+
if (is_event_down) {
425+
// For down events, this key is the event key if they have the same
426+
// virtual key, because virtual key represents "functionality."
427+
if (virtual_key == event_virtual_key) {
428+
pre_event_pressed = false;
429+
}
430+
} else {
431+
// For up events, this key is the event key if they have the same
432+
// physical key, because it is necessary to ensure that the physical
433+
// key is correctly released.
434+
//
435+
// In that case, although the previous state should be pressed, don't
436+
// synthesize a down event even if it's not. The later code will handle
437+
// such cases by skipping abrupt up events. Obviously don't synthesize
438+
// up events either.
439+
if (event_physical_key == key_info.physical_key) {
440+
continue;
441+
}
399442
}
400-
if (recorded_pressed != should_pressed) {
401-
if (recorded_pressed) {
402-
pressingRecords_.erase(recorded_pressed_iter);
443+
if (now_pressed != pre_event_pressed) {
444+
if (now_pressed) {
445+
pressingRecords_.erase(pressing_record_iter);
403446
} else {
404447
pressingRecords_[key_info.physical_key] = key_info.logical_key;
405448
}
406449
const char* empty_character = "";
407450
SendEvent(
408-
SynthesizeSimpleEvent(recorded_pressed ? kFlutterKeyEventTypeUp
409-
: kFlutterKeyEventTypeDown,
410-
key_info.physical_key, key_info.logical_key,
411-
empty_character),
451+
SynthesizeSimpleEvent(
452+
now_pressed ? kFlutterKeyEventTypeUp : kFlutterKeyEventTypeDown,
453+
key_info.physical_key, key_info.logical_key, empty_character),
412454
nullptr, nullptr);
413455
}
414456
}

shell/platform/windows/keyboard_key_embedder_handler.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,13 @@ class KeyboardKeyEmbedderHandler
114114
uint64_t logical_key);
115115
// Check each key's state from |get_key_state_| and synthesize events
116116
// if their toggling states have been desynchronized.
117-
void SynchronizeCritialToggledStates(int virtual_key, bool is_down);
117+
void SynchronizeCritialToggledStates(int event_virtual_key,
118+
bool is_event_down);
118119
// Check each key's state from |get_key_state_| and synthesize events
119120
// if their pressing states have been desynchronized.
120-
void SynchronizeCritialPressedStates(int virtual_key, bool was_down);
121+
void SynchronizeCritialPressedStates(int event_virtual_key,
122+
int event_physical_key,
123+
bool is_event_down);
121124

122125
// Wraps perform_send_event_ with state tracking. Use this instead of
123126
// |perform_send_event_| to send events to the framework.

shell/platform/windows/keyboard_win32_unittests.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ constexpr uint64_t kScanCodeKeyO = 0x18;
4646
constexpr uint64_t kScanCodeKeyQ = 0x10;
4747
constexpr uint64_t kScanCodeKeyW = 0x11;
4848
constexpr uint64_t kScanCodeDigit1 = 0x02;
49+
constexpr uint64_t kScanCodeDigit2 = 0x03;
4950
constexpr uint64_t kScanCodeDigit6 = 0x07;
5051
// constexpr uint64_t kScanCodeNumpad1 = 0x4f;
5152
// constexpr uint64_t kScanCodeNumLock = 0x45;
@@ -1915,6 +1916,44 @@ TEST(KeyboardTest, ImeExtendedEventsAreIgnored) {
19151916
EXPECT_EQ(tester.RedispatchedMessageCountAndClear(), 0);
19161917
}
19171918

1919+
// Ensures that synthesization works correctly when a Shift key is pressed and
1920+
// (only) its up event is labeled as an IME event (VK_PROCESSKEY).
1921+
//
1922+
// Regression test for https://github.com/flutter/flutter/issues/104169. These
1923+
// are real messages recorded when pressing Shift-2 using Microsoft Pinyin IME
1924+
// on Win 10 Enterprise, which crashed the app before the fix.
1925+
TEST(KeyboardTest, UpOnlyImeEventsAreCorrectlyHandled) {
1926+
KeyboardTester tester;
1927+
tester.Responding(true);
1928+
1929+
// US Keyboard layout.
1930+
1931+
// Press CtrlRight in IME mode.
1932+
tester.InjectKeyboardChanges(std::vector<KeyboardChange>{
1933+
KeyStateChange{VK_LSHIFT, true, false},
1934+
WmKeyDownInfo{VK_SHIFT, kScanCodeShiftLeft, kNotExtended, kWasUp}.Build(
1935+
kWmResultZero),
1936+
WmKeyDownInfo{VK_PROCESSKEY, kScanCodeDigit2, kNotExtended, kWasUp}.Build(
1937+
kWmResultZero),
1938+
KeyStateChange{VK_LSHIFT, false, true},
1939+
WmKeyUpInfo{VK_PROCESSKEY, kScanCodeShiftLeft, kNotExtended}.Build(
1940+
kWmResultZero),
1941+
WmKeyUpInfo{'2', kScanCodeDigit2, kNotExtended, kWasUp}.Build(
1942+
kWmResultZero)});
1943+
1944+
EXPECT_EQ(key_calls.size(), 4);
1945+
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
1946+
kPhysicalShiftLeft, kLogicalShiftLeft, "",
1947+
kNotSynthesized);
1948+
EXPECT_CALL_IS_EVENT(key_calls[1], kFlutterKeyEventTypeDown, 0, 0, "",
1949+
kNotSynthesized);
1950+
EXPECT_CALL_IS_EVENT(key_calls[2], kFlutterKeyEventTypeUp, kPhysicalShiftLeft,
1951+
kLogicalShiftLeft, "", kNotSynthesized);
1952+
EXPECT_CALL_IS_EVENT(key_calls[3], kFlutterKeyEventTypeDown, 0, 0, "",
1953+
kNotSynthesized);
1954+
clear_key_calls();
1955+
}
1956+
19181957
TEST(KeyboardTest, DisorderlyRespondedEvents) {
19191958
KeyboardTester tester;
19201959

0 commit comments

Comments
 (0)