1212import io .flutter .Log ;
1313import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
1414import io .flutter .plugin .editing .TextInputPlugin ;
15+ import java .util .AbstractMap .SimpleImmutableEntry ;
1516import java .util .ArrayDeque ;
1617import java .util .Deque ;
18+ import java .util .Map .Entry ;
1719
1820/**
1921 * A class to process key events from Android, passing them to the framework as messages using
3133 */
3234public class AndroidKeyProcessor {
3335 private static final String TAG = "AndroidKeyProcessor" ;
36+ private static long eventIdSerial = 0 ;
3437
3538 @ NonNull private final KeyEventChannel keyEventChannel ;
3639 @ NonNull private final TextInputPlugin textInputPlugin ;
@@ -47,8 +50,8 @@ public class AndroidKeyProcessor {
4750 * <p>It is possible that that in the middle of the async round trip, the focus chain could
4851 * change, and instead of the native widget that was "next" when the event was fired getting the
4952 * event, it may be the next widget when the event is synthesized that gets it. In practice, this
50- * shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input,
51- * and it may actually be desired behavior, but it is possible.
53+ * shouldn't be a huge problem, as this is an unlikely occurance to happen without user input, and
54+ * it may actually be desired behavior, but it is possible.
5255 *
5356 * @param view takes the activity to use for re-dispatching of events that were not handled by the
5457 * framework.
@@ -93,39 +96,23 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
9396 // case the theory is wrong.
9497 return false ;
9598 }
96- if (eventResponder .isHeadEvent (keyEvent )) {
97- // If the keyEvent is at the head of the queue of pending events we've seen,
98- // and has the same id, then we know that this is a re-dispatched keyEvent, and
99- // we shouldn't respond to it, but we should remove it from tracking now.
100- eventResponder .removeHeadEvent ();
99+ if (eventResponder .dispatchingKeyEvent ) {
100+ // Don't handle it if it is from our own delayed event dispatch.
101101 return false ;
102102 }
103103
104104 Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
105105 KeyEventChannel .FlutterKeyEvent flutterEvent =
106- new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
107-
108- eventResponder .addEvent (keyEvent );
106+ new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter , eventIdSerial ++);
109107 if (action == KeyEvent .ACTION_DOWN ) {
110108 keyEventChannel .keyDown (flutterEvent );
111109 } else {
112110 keyEventChannel .keyUp (flutterEvent );
113111 }
112+ eventResponder .addEvent (flutterEvent .eventId , keyEvent );
114113 return true ;
115114 }
116115
117- /**
118- * Returns whether or not the given event is currently being processed by this key processor. This
119- * is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates
120- * from a hardware key event, or a soft keyboard editing event.
121- *
122- * @param event the event to check for being the current event.
123- * @return
124- */
125- public boolean isCurrentEvent (@ NonNull KeyEvent event ) {
126- return eventResponder .isHeadEvent (event );
127- }
128-
129116 /**
130117 * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
131118 * Unicode combining character and returns the combination of these characters if a combination
@@ -189,63 +176,65 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
189176 // The maximum number of pending events that are held before starting to
190177 // complain.
191178 private static final long MAX_PENDING_EVENTS = 1000 ;
192- final Deque <KeyEvent > pendingEvents = new ArrayDeque <KeyEvent >();
179+ final Deque <Entry < Long , KeyEvent >> pendingEvents = new ArrayDeque <Entry < Long , KeyEvent > >();
193180 @ NonNull private final View view ;
194181 @ NonNull private final TextInputPlugin textInputPlugin ;
182+ boolean dispatchingKeyEvent = false ;
195183
196184 public EventResponder (@ NonNull View view , @ NonNull TextInputPlugin textInputPlugin ) {
197185 this .view = view ;
198186 this .textInputPlugin = textInputPlugin ;
199187 }
200188
201- /** Removes the first pending event from the cache of pending events. */
202- private KeyEvent removeHeadEvent () {
203- return pendingEvents .removeFirst ();
204- }
205-
206- private KeyEvent checkIsHeadEvent (KeyEvent event ) {
207- if (pendingEvents .size () == 0 ) {
208- throw new AssertionError (
209- "Event response received when no events are in the queue. Received event " + event );
210- }
211- if (pendingEvents .getFirst () != event ) {
189+ /**
190+ * Removes the pending event with the given id from the cache of pending events.
191+ *
192+ * @param id the id of the event to be removed.
193+ */
194+ private KeyEvent removePendingEvent (long id ) {
195+ if (pendingEvents .getFirst ().getKey () != id ) {
212196 throw new AssertionError (
213197 "Event response received out of order. Should have seen event "
214- + pendingEvents .getFirst ()
198+ + pendingEvents .getFirst (). getKey ()
215199 + " first. Instead, received "
216- + event );
200+ + id );
217201 }
218- return pendingEvents .getFirst ();
219- }
220-
221- private boolean isHeadEvent (KeyEvent event ) {
222- return pendingEvents .size () > 0 && pendingEvents .getFirst () == event ;
202+ return pendingEvents .removeFirst ().getValue ();
223203 }
224204
225205 /**
226206 * Called whenever the framework responds that a given key event was handled by the framework.
227207 *
228- * @param event the event to be marked as being handled by the framework. Must not be null.
208+ * @param id the event id of the event to be marked as being handled by the framework. Must not
209+ * be null.
229210 */
230211 @ Override
231- public void onKeyEventHandled (KeyEvent event ) {
232- removeHeadEvent ( );
212+ public void onKeyEventHandled (long id ) {
213+ removePendingEvent ( id );
233214 }
234215
235216 /**
236217 * Called whenever the framework responds that a given key event wasn't handled by the
237218 * framework.
238219 *
239- * @param event the event to be marked as not being handled by the framework. Must not be null.
220+ * @param id the event id of the event to be marked as not being handled by the framework. Must
221+ * not be null.
240222 */
241223 @ Override
242- public void onKeyEventNotHandled (KeyEvent event ) {
243- redispatchKeyEvent ( checkIsHeadEvent ( event ));
224+ public void onKeyEventNotHandled (long id ) {
225+ dispatchKeyEvent ( removePendingEvent ( id ));
244226 }
245227
246- /** Adds an Android key event to the event responder to wait for a response. */
247- public void addEvent (@ NonNull KeyEvent event ) {
248- pendingEvents .addLast (event );
228+ /** Adds an Android key event with an id to the event responder to wait for a response. */
229+ public void addEvent (long id , @ NonNull KeyEvent event ) {
230+ if (pendingEvents .size () > 0 && pendingEvents .getFirst ().getKey () >= id ) {
231+ throw new AssertionError (
232+ "New events must have ids greater than the most recent pending event. New id "
233+ + id
234+ + " is less than or equal to the last event id of "
235+ + pendingEvents .getFirst ().getKey ());
236+ }
237+ pendingEvents .addLast (new SimpleImmutableEntry <Long , KeyEvent >(id , event ));
249238 if (pendingEvents .size () > MAX_PENDING_EVENTS ) {
250239 Log .e (
251240 TAG ,
@@ -261,21 +250,27 @@ public void addEvent(@NonNull KeyEvent event) {
261250 *
262251 * @param event the event to be dispatched to the activity.
263252 */
264- private void redispatchKeyEvent (KeyEvent event ) {
253+ public void dispatchKeyEvent (KeyEvent event ) {
265254 // If the textInputPlugin is still valid and accepting text, then we'll try
266255 // and send the key event to it, assuming that if the event can be sent,
267256 // that it has been handled.
268- if (textInputPlugin .getInputMethodManager ().isAcceptingText ()
269- && textInputPlugin .getLastInputConnection () != null
270- && textInputPlugin .getLastInputConnection ().sendKeyEvent (event )) {
271- // The event was handled, so we can remove it from the queue.
272- removeHeadEvent ();
273- return ;
257+ if (textInputPlugin .getLastInputConnection () != null
258+ && textInputPlugin .getInputMethodManager ().isAcceptingText ()) {
259+ dispatchingKeyEvent = true ;
260+ boolean handled = textInputPlugin .getLastInputConnection ().sendKeyEvent (event );
261+ dispatchingKeyEvent = false ;
262+ if (handled ) {
263+ return ;
264+ }
274265 }
275266
276267 // Since the framework didn't handle it, dispatch the event again.
277268 if (view != null ) {
269+ // Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
270+ // send it to the framework again.
271+ dispatchingKeyEvent = true ;
278272 view .getRootView ().dispatchKeyEvent (event );
273+ dispatchingKeyEvent = false ;
279274 }
280275 }
281276 }
0 commit comments