1212import io .flutter .Log ;
1313import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
1414import io .flutter .plugin .editing .TextInputPlugin ;
15- import java .util .AbstractMap .SimpleImmutableEntry ;
1615import java .util .ArrayDeque ;
1716import java .util .Deque ;
18- import java .util .Map .Entry ;
1917
2018/**
2119 * A class to process key events from Android, passing them to the framework as messages using
3331 */
3432public class AndroidKeyProcessor {
3533 private static final String TAG = "AndroidKeyProcessor" ;
36- private static long eventIdSerial = 0 ;
3734
3835 @ NonNull private final KeyEventChannel keyEventChannel ;
3936 @ NonNull private final TextInputPlugin textInputPlugin ;
@@ -50,8 +47,8 @@ public class AndroidKeyProcessor {
5047 * <p>It is possible that that in the middle of the async round trip, the focus chain could
5148 * change, and instead of the native widget that was "next" when the event was fired getting the
5249 * event, it may be the next widget when the event is synthesized that gets it. In practice, this
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.
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.
5552 *
5653 * @param view takes the activity to use for re-dispatching of events that were not handled by the
5754 * framework.
@@ -96,23 +93,39 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
9693 // case the theory is wrong.
9794 return false ;
9895 }
99- if (eventResponder .dispatchingKeyEvent ) {
100- // Don't handle it if it is from our own delayed event dispatch.
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 ();
101101 return false ;
102102 }
103103
104104 Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
105105 KeyEventChannel .FlutterKeyEvent flutterEvent =
106- new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter , eventIdSerial ++);
106+ new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
107+
108+ eventResponder .addEvent (keyEvent );
107109 if (action == KeyEvent .ACTION_DOWN ) {
108110 keyEventChannel .keyDown (flutterEvent );
109111 } else {
110112 keyEventChannel .keyUp (flutterEvent );
111113 }
112- eventResponder .addEvent (flutterEvent .eventId , keyEvent );
113114 return true ;
114115 }
115116
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+
116129 /**
117130 * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
118131 * Unicode combining character and returns the combination of these characters if a combination
@@ -176,65 +189,63 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
176189 // The maximum number of pending events that are held before starting to
177190 // complain.
178191 private static final long MAX_PENDING_EVENTS = 1000 ;
179- final Deque <Entry < Long , KeyEvent >> pendingEvents = new ArrayDeque <Entry < Long , KeyEvent > >();
192+ final Deque <KeyEvent > pendingEvents = new ArrayDeque <KeyEvent >();
180193 @ NonNull private final View view ;
181194 @ NonNull private final TextInputPlugin textInputPlugin ;
182- boolean dispatchingKeyEvent = false ;
183195
184196 public EventResponder (@ NonNull View view , @ NonNull TextInputPlugin textInputPlugin ) {
185197 this .view = view ;
186198 this .textInputPlugin = textInputPlugin ;
187199 }
188200
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 ) {
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 ) {
196212 throw new AssertionError (
197213 "Event response received out of order. Should have seen event "
198- + pendingEvents .getFirst (). getKey ()
214+ + pendingEvents .getFirst ()
199215 + " first. Instead, received "
200- + id );
216+ + event );
201217 }
202- return pendingEvents .removeFirst ().getValue ();
218+ return pendingEvents .getFirst ();
219+ }
220+
221+ private boolean isHeadEvent (KeyEvent event ) {
222+ return pendingEvents .size () > 0 && pendingEvents .getFirst () == event ;
203223 }
204224
205225 /**
206226 * Called whenever the framework responds that a given key event was handled by the framework.
207227 *
208- * @param id the event id of the event to be marked as being handled by the framework. Must not
209- * be null.
228+ * @param event the event to be marked as being handled by the framework. Must not be null.
210229 */
211230 @ Override
212- public void onKeyEventHandled (long id ) {
213- removePendingEvent ( id );
231+ public void onKeyEventHandled (KeyEvent event ) {
232+ removeHeadEvent ( );
214233 }
215234
216235 /**
217236 * Called whenever the framework responds that a given key event wasn't handled by the
218237 * framework.
219238 *
220- * @param id the event id of the event to be marked as not being handled by the framework. Must
221- * not be null.
239+ * @param event the event to be marked as not being handled by the framework. Must not be null.
222240 */
223241 @ Override
224- public void onKeyEventNotHandled (long id ) {
225- dispatchKeyEvent ( removePendingEvent ( id ));
242+ public void onKeyEventNotHandled (KeyEvent event ) {
243+ redispatchKeyEvent ( checkIsHeadEvent ( event ));
226244 }
227245
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 ));
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 );
238249 if (pendingEvents .size () > MAX_PENDING_EVENTS ) {
239250 Log .e (
240251 TAG ,
@@ -250,27 +261,21 @@ public void addEvent(long id, @NonNull KeyEvent event) {
250261 *
251262 * @param event the event to be dispatched to the activity.
252263 */
253- public void dispatchKeyEvent (KeyEvent event ) {
264+ private void redispatchKeyEvent (KeyEvent event ) {
254265 // If the textInputPlugin is still valid and accepting text, then we'll try
255266 // and send the key event to it, assuming that if the event can be sent,
256267 // that it has been handled.
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- }
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 ;
265274 }
266275
267276 // Since the framework didn't handle it, dispatch the event again.
268277 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 ;
272278 view .getRootView ().dispatchKeyEvent (event );
273- dispatchingKeyEvent = false ;
274279 }
275280 }
276281 }
0 commit comments