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