From 7224093f3c311448dca1933012677799a4056e80 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 21 Sep 2022 06:39:51 +0800 Subject: [PATCH] [CP] [Android] Fatal crash with `java.lang.AssertionError` when selecting text in `TextField` (#36288) * [Android] Synchronize incorrect metaState using post-synchronization (#34827) (cherry picked from commit a56d98ea78719e8ec54af8f3325740aadb953f1c) * [Android] Fix a crash related to zeroed scanCode (#35924) (cherry picked from commit fe5b84ae3f29b9bec8dd88a80e14e5c244b48963) Co-authored-by: Tong Mu Co-authored-by: Bruno Leroux --- .../android/KeyEmbedderResponder.java | 49 ++++++++---- .../android/KeyboardManagerTest.java | 75 +++++++++++++++++++ 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 989ee9997d6ba..2a5d5b1a87400 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -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; /** @@ -120,17 +121,26 @@ 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 synthesized 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, + long eventPhysicalKey, + KeyEvent event, + ArrayList 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 @@ -141,17 +151,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) { - nowStates[keyIdx] = pressingRecords.containsKey(goal.keys[keyIdx].physicalKey); - if (goal.keys[keyIdx].logicalKey == eventLogicalKey) { + final KeyboardMap.KeyPair key = goal.keys[keyIdx]; + nowStates[keyIdx] = pressingRecords.containsKey(key.physicalKey); + if (key.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, eventPhysicalKey, event.getEventTime())); } break; case kUp: @@ -165,10 +176,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; @@ -260,8 +271,15 @@ private boolean handleEventImpl( final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); + final ArrayList 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, + physicalKey, + event, + postSynchronizeEvents); } for (final TogglingGoal goal : togglingGoals.values()) { @@ -329,6 +347,9 @@ private boolean handleEventImpl( output.synthesized = false; sendKeyEvent(output, onKeyEventHandledCallback); + for (final Runnable postSyncEvent : postSynchronizeEvents) { + postSyncEvent.run(); + } return true; } diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index 9210b2edd698e..799ae861b2352 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -1346,6 +1346,81 @@ 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 have 0 for their metaState. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList 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(); + } + + // Regression test for https://github.com/flutter/flutter/issues/110640 + @Test + public void synchronizeModifiersForZeroedScanCode() { + // Test if ShiftLeft can be correctly synchronized during down events of + // ShiftLeft that have 0 for their metaState and 0 for their scanCode. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // Test: DOWN event when the current state is 0 and scanCode is 0. + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 0, KEYCODE_SHIFT_LEFT, 0, '\0', 0); + // Compute physicalKey in the same way as KeyboardManager.getPhysicalKey. + final Long physicalKey = KEYCODE_SHIFT_LEFT | KeyboardMap.kAndroidPlane; + + assertEquals(tester.keyboardManager.handleEvent(keyEvent), true); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, physicalKey, LOGICAL_SHIFT_LEFT, null, false); + assertEmbedderEventEquals( + calls.get(1).keyData, Type.kUp, physicalKey, LOGICAL_SHIFT_LEFT, null, true); + calls.clear(); + } + @Test public void normalCapsLockEvents() { final KeyboardTester tester = new KeyboardTester();