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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.flutter.embedding.android.KeyboardMap.PressingGoal;
import io.flutter.embedding.android.KeyboardMap.TogglingGoal;
import io.flutter.plugin.common.BinaryMessenger;
import java.util.ArrayList;
import java.util.HashMap;

/**
Expand Down Expand Up @@ -120,17 +121,25 @@ void updatePressingState(@NonNull Long physicalKey, @Nullable Long logicalKey) {
// dispatches synthesized events so that the state of these keys matches the true state taking
// the current event in consideration.
//
// Events that should be synthesized before the main event are synthesized
// immediately, while events that should be syntehsized after the main event are appended to
// `postSynchronize`.
//
// Although Android KeyEvent defined bitmasks for sided modifiers (SHIFT_LEFT_ON and
// SHIFT_RIGHT_ON),
// this function only uses the unsided modifiers (SHIFT_ON), due to the weird behaviors observed
// on ChromeOS, where right modifiers produce events with UNSIDED | LEFT_SIDE meta state bits.
void synchronizePressingKey(
PressingGoal goal, boolean truePressed, long eventLogicalKey, KeyEvent event) {
PressingGoal goal,
boolean truePressed,
long eventLogicalKey,
KeyEvent event,
ArrayList<Runnable> postSynchronize) {
// During an incoming event, there might be a synthesized Flutter event for each key of each
// pressing goal, followed by an eventual main Flutter event.
//
// NowState ----------------> PreEventState --------------> TrueState
// Synchronization Event
// NowState ----------------> PreEventState --------------> -------------->TrueState
// PreSynchronize Event PostSynchronize
//
// The goal of the synchronization algorithm is to derive a pre-event state that can satisfy the
// true state (`truePressed`) after the event, and that requires as few synthesized events based
Expand All @@ -141,17 +150,18 @@ void synchronizePressingKey(
// 1. Find the current states of all keys.
// 2. Derive the pre-event state of the event key (if applicable.)
for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) {
final KeyboardMap.KeyPair key = goal.keys[keyIdx];
nowStates[keyIdx] = pressingRecords.containsKey(goal.keys[keyIdx].physicalKey);
if (goal.keys[keyIdx].logicalKey == eventLogicalKey) {
switch (getEventType(event)) {
case kDown:
preEventStates[keyIdx] = false;
postEventAnyPressed = true;
if (!truePressed) {
throw new AssertionError(
String.format(
"Unexpected metaState 0 for key 0x%x during an ACTION_down event.",
eventLogicalKey));
postSynchronize.add(
() ->
synthesizeEvent(
false, key.logicalKey, key.physicalKey, event.getEventTime()));
}
break;
case kUp:
Expand All @@ -165,10 +175,10 @@ void synchronizePressingKey(
// synthesize a down event here, or there will be a down event *and* a repeat event,
// both of which have printable characters. Obviously don't synthesize up events either.
if (!truePressed) {
throw new AssertionError(
String.format(
"Unexpected metaState 0 for key 0x%x during an ACTION_down repeat event.",
eventLogicalKey));
postSynchronize.add(
() ->
synthesizeEvent(
false, key.logicalKey, key.physicalKey, event.getEventTime()));
}
preEventStates[keyIdx] = nowStates[keyIdx];
postEventAnyPressed = true;
Expand Down Expand Up @@ -260,8 +270,10 @@ private boolean handleEventImpl(
final Long physicalKey = getPhysicalKey(event);
final Long logicalKey = getLogicalKey(event);

final ArrayList<Runnable> postSynchronizeEvents = new ArrayList<>();
for (final PressingGoal goal : KeyboardMap.pressingGoals) {
synchronizePressingKey(goal, (event.getMetaState() & goal.mask) != 0, logicalKey, event);
synchronizePressingKey(
goal, (event.getMetaState() & goal.mask) != 0, logicalKey, event, postSynchronizeEvents);
}

for (final TogglingGoal goal : togglingGoals.values()) {
Expand Down Expand Up @@ -329,6 +341,9 @@ private boolean handleEventImpl(
output.synthesized = false;

sendKeyEvent(output, onKeyEventHandledCallback);
for (final Runnable postSyncEvent : postSynchronizeEvents) {
postSyncEvent.run();
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,56 @@ public void synchronizeOtherModifiers() {
calls.clear();
}

// Regression test for https://github.com/flutter/flutter/issues/108124
@Test
public void synchronizeModifiersForConflictingMetaState() {
// Test if ShiftLeft can be correctly synchronized during down events of
// ShiftLeft that has 0 for its metaState.
final KeyboardTester tester = new KeyboardTester();
final ArrayList<CallRecord> calls = new ArrayList<>();
// Even though the event is for ShiftRight, we still set SHIFT | SHIFT_LEFT here.
// See the comment in synchronizePressingKey for the reason.
final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON;

tester.recordEmbedderCallsTo(calls);
tester.respondToTextInputWith(true); // Suppress redispatching

// Test: Down event when the current state is 0.
assertEquals(
true,
tester.keyboardManager.handleEvent(
new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0)));
assertEquals(calls.size(), 2);
assertEmbedderEventEquals(
calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false);
assertEmbedderEventEquals(
calls.get(1).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true);
calls.clear();

// A normal down event.
assertEquals(
true,
tester.keyboardManager.handleEvent(
new FakeKeyEvent(
ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON)));
assertEquals(calls.size(), 1);
assertEmbedderEventEquals(
calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false);
calls.clear();

// Test: Repeat event when the current state is 0.
assertEquals(
true,
tester.keyboardManager.handleEvent(
new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 1, '\0', 0)));
assertEquals(calls.size(), 2);
assertEmbedderEventEquals(
calls.get(0).keyData, Type.kRepeat, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false);
assertEmbedderEventEquals(
calls.get(1).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true);
calls.clear();
}

@Test
public void normalCapsLockEvents() {
final KeyboardTester tester = new KeyboardTester();
Expand Down