12
12
import io .flutter .Log ;
13
13
import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
14
14
import io .flutter .plugin .editing .TextInputPlugin ;
15
+ import java .util .AbstractMap .SimpleImmutableEntry ;
15
16
import java .util .ArrayDeque ;
16
17
import java .util .Deque ;
18
+ import java .util .Map .Entry ;
17
19
18
20
/**
19
21
* A class to process key events from Android, passing them to the framework as messages using
31
33
*/
32
34
public class AndroidKeyProcessor {
33
35
private static final String TAG = "AndroidKeyProcessor" ;
36
+ private static long eventIdSerial = 0 ;
34
37
35
38
@ NonNull private final KeyEventChannel keyEventChannel ;
36
39
@ NonNull private final TextInputPlugin textInputPlugin ;
@@ -47,8 +50,8 @@ public class AndroidKeyProcessor {
47
50
* <p>It is possible that that in the middle of the async round trip, the focus chain could
48
51
* change, and instead of the native widget that was "next" when the event was fired getting the
49
52
* 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.
52
55
*
53
56
* @param view takes the activity to use for re-dispatching of events that were not handled by the
54
57
* framework.
@@ -93,39 +96,23 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
93
96
// case the theory is wrong.
94
97
return false ;
95
98
}
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.
101
101
return false ;
102
102
}
103
103
104
104
Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
105
105
KeyEventChannel .FlutterKeyEvent flutterEvent =
106
- new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
107
-
108
- eventResponder .addEvent (keyEvent );
106
+ new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter , eventIdSerial ++);
109
107
if (action == KeyEvent .ACTION_DOWN ) {
110
108
keyEventChannel .keyDown (flutterEvent );
111
109
} else {
112
110
keyEventChannel .keyUp (flutterEvent );
113
111
}
112
+ eventResponder .addEvent (flutterEvent .eventId , keyEvent );
114
113
return true ;
115
114
}
116
115
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
-
129
116
/**
130
117
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
131
118
* Unicode combining character and returns the combination of these characters if a combination
@@ -189,63 +176,65 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
189
176
// The maximum number of pending events that are held before starting to
190
177
// complain.
191
178
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 > >();
193
180
@ NonNull private final View view ;
194
181
@ NonNull private final TextInputPlugin textInputPlugin ;
182
+ boolean dispatchingKeyEvent = false ;
195
183
196
184
public EventResponder (@ NonNull View view , @ NonNull TextInputPlugin textInputPlugin ) {
197
185
this .view = view ;
198
186
this .textInputPlugin = textInputPlugin ;
199
187
}
200
188
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 ) {
212
196
throw new AssertionError (
213
197
"Event response received out of order. Should have seen event "
214
- + pendingEvents .getFirst ()
198
+ + pendingEvents .getFirst (). getKey ()
215
199
+ " first. Instead, received "
216
- + event );
200
+ + id );
217
201
}
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 ();
223
203
}
224
204
225
205
/**
226
206
* Called whenever the framework responds that a given key event was handled by the framework.
227
207
*
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.
229
210
*/
230
211
@ Override
231
- public void onKeyEventHandled (KeyEvent event ) {
232
- removeHeadEvent ( );
212
+ public void onKeyEventHandled (long id ) {
213
+ removePendingEvent ( id );
233
214
}
234
215
235
216
/**
236
217
* Called whenever the framework responds that a given key event wasn't handled by the
237
218
* framework.
238
219
*
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.
240
222
*/
241
223
@ Override
242
- public void onKeyEventNotHandled (KeyEvent event ) {
243
- redispatchKeyEvent ( checkIsHeadEvent ( event ));
224
+ public void onKeyEventNotHandled (long id ) {
225
+ dispatchKeyEvent ( removePendingEvent ( id ));
244
226
}
245
227
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 ));
249
238
if (pendingEvents .size () > MAX_PENDING_EVENTS ) {
250
239
Log .e (
251
240
TAG ,
@@ -261,21 +250,27 @@ public void addEvent(@NonNull KeyEvent event) {
261
250
*
262
251
* @param event the event to be dispatched to the activity.
263
252
*/
264
- private void redispatchKeyEvent (KeyEvent event ) {
253
+ public void dispatchKeyEvent (KeyEvent event ) {
265
254
// If the textInputPlugin is still valid and accepting text, then we'll try
266
255
// and send the key event to it, assuming that if the event can be sent,
267
256
// 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
+ }
274
265
}
275
266
276
267
// Since the framework didn't handle it, dispatch the event again.
277
268
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 ;
278
272
view .getRootView ().dispatchKeyEvent (event );
273
+ dispatchingKeyEvent = false ;
279
274
}
280
275
}
281
276
}
0 commit comments