11
11
import androidx .annotation .Nullable ;
12
12
import io .flutter .Log ;
13
13
import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
14
+ import io .flutter .embedding .engine .systemchannels .KeyEventChannel .FlutterKeyEvent ;
14
15
import io .flutter .plugin .editing .TextInputPlugin ;
15
16
import java .util .AbstractMap .SimpleImmutableEntry ;
16
17
import java .util .ArrayDeque ;
33
34
*/
34
35
public class AndroidKeyProcessor {
35
36
private static final String TAG = "AndroidKeyProcessor" ;
36
- private static long eventIdSerial = 0 ;
37
37
38
38
@ NonNull private final KeyEventChannel keyEventChannel ;
39
39
@ NonNull private final TextInputPlugin textInputPlugin ;
@@ -50,8 +50,8 @@ public class AndroidKeyProcessor {
50
50
* <p>It is possible that that in the middle of the async round trip, the focus chain could
51
51
* change, and instead of the native widget that was "next" when the event was fired getting the
52
52
* 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.
53
+ * shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input,
54
+ * and it may actually be desired behavior, but it is possible.
55
55
*
56
56
* @param view takes the activity to use for re-dispatching of events that were not handled by the
57
57
* framework.
@@ -88,31 +88,49 @@ public void destroy() {
88
88
* @return true if the key event should not be propagated to other Android components. Delayed
89
89
* synthesis events will return false, so that other components may handle them.
90
90
*/
91
- public boolean onKeyEvent (@ NonNull KeyEvent keyEvent ) {
92
- int action = keyEvent .getAction ();
93
- if (action != KeyEvent .ACTION_DOWN && action != KeyEvent .ACTION_UP ) {
91
+ public boolean onKeyEvent (@ NonNull KeyEvent event ) {
92
+ int action = event .getAction ();
93
+ if (action != event .ACTION_DOWN && action != event .ACTION_UP ) {
94
94
// There is theoretically a KeyEvent.ACTION_MULTIPLE, but theoretically
95
95
// that isn't sent by Android anymore, so this is just for protection in
96
96
// case the theory is wrong.
97
97
return false ;
98
98
}
99
- if (eventResponder .dispatchingKeyEvent ) {
100
- // Don't handle it if it is from our own delayed event dispatch.
99
+ long eventId = FlutterKeyEvent .computeEventId (event );
100
+ if (eventResponder .isHeadEvent (eventId )) {
101
+ // If the event is at the head of the queue of pending events we've seen,
102
+ // and has the same id, then we know that this is a re-dispatched event, and
103
+ // we shouldn't respond to it, but we should remove it from tracking now.
104
+ eventResponder .removePendingEvent (eventId );
101
105
return false ;
102
106
}
103
107
104
- Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
108
+ Character complexCharacter = applyCombiningCharacterToBaseCharacter (event .getUnicodeChar ());
105
109
KeyEventChannel .FlutterKeyEvent flutterEvent =
106
- new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter , eventIdSerial ++);
110
+ new KeyEventChannel .FlutterKeyEvent (event , complexCharacter );
111
+
112
+ eventResponder .addEvent (flutterEvent .eventId , event );
107
113
if (action == KeyEvent .ACTION_DOWN ) {
108
114
keyEventChannel .keyDown (flutterEvent );
109
115
} else {
110
116
keyEventChannel .keyUp (flutterEvent );
111
117
}
112
- eventResponder .addEvent (flutterEvent .eventId , keyEvent );
113
118
return true ;
114
119
}
115
120
121
+ /**
122
+ * Returns whether or not the given event is currently being processed by this key processor. This
123
+ * is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates
124
+ * from a hardware key event, or a soft keyboard editing event.
125
+ *
126
+ * @param event the event to check for being the current event.
127
+ * @return
128
+ */
129
+ public boolean isCurrentEvent (@ NonNull KeyEvent event ) {
130
+ long id = FlutterKeyEvent .computeEventId (event );
131
+ return eventResponder .isHeadEvent (id );
132
+ }
133
+
116
134
/**
117
135
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
118
136
* Unicode combining character and returns the combination of these characters if a combination
@@ -179,7 +197,6 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
179
197
final Deque <Entry <Long , KeyEvent >> pendingEvents = new ArrayDeque <Entry <Long , KeyEvent >>();
180
198
@ NonNull private final View view ;
181
199
@ NonNull private final TextInputPlugin textInputPlugin ;
182
- boolean dispatchingKeyEvent = false ;
183
200
184
201
public EventResponder (@ NonNull View view , @ NonNull TextInputPlugin textInputPlugin ) {
185
202
this .view = view ;
@@ -202,6 +219,25 @@ private KeyEvent removePendingEvent(long id) {
202
219
return pendingEvents .removeFirst ().getValue ();
203
220
}
204
221
222
+ private KeyEvent findPendingEvent (long id ) {
223
+ if (pendingEvents .size () == 0 ) {
224
+ throw new AssertionError (
225
+ "Event response received when no events are in the queue. Received id " + id );
226
+ }
227
+ if (pendingEvents .getFirst ().getKey () != id ) {
228
+ throw new AssertionError (
229
+ "Event response received out of order. Should have seen event "
230
+ + pendingEvents .getFirst ().getKey ()
231
+ + " first. Instead, received "
232
+ + id );
233
+ }
234
+ return pendingEvents .getFirst ().getValue ();
235
+ }
236
+
237
+ private boolean isHeadEvent (long id ) {
238
+ return pendingEvents .size () > 0 && pendingEvents .getFirst ().getKey () == id ;
239
+ }
240
+
205
241
/**
206
242
* Called whenever the framework responds that a given key event was handled by the framework.
207
243
*
@@ -222,18 +258,11 @@ public void onKeyEventHandled(long id) {
222
258
*/
223
259
@ Override
224
260
public void onKeyEventNotHandled (long id ) {
225
- dispatchKeyEvent (removePendingEvent (id ));
261
+ dispatchKeyEvent (findPendingEvent (id ), id );
226
262
}
227
263
228
264
/** Adds an Android key event with an id to the event responder to wait for a response. */
229
265
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
266
pendingEvents .addLast (new SimpleImmutableEntry <Long , KeyEvent >(id , event ));
238
267
if (pendingEvents .size () > MAX_PENDING_EVENTS ) {
239
268
Log .e (
@@ -250,27 +279,21 @@ public void addEvent(long id, @NonNull KeyEvent event) {
250
279
*
251
280
* @param event the event to be dispatched to the activity.
252
281
*/
253
- public void dispatchKeyEvent (KeyEvent event ) {
282
+ public void dispatchKeyEvent (KeyEvent event , long id ) {
254
283
// If the textInputPlugin is still valid and accepting text, then we'll try
255
284
// and send the key event to it, assuming that if the event can be sent,
256
285
// 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
- }
286
+ if (textInputPlugin .getInputMethodManager ().isAcceptingText ()
287
+ && textInputPlugin .getLastInputConnection () != null
288
+ && textInputPlugin .getLastInputConnection ().sendKeyEvent (event )) {
289
+ // The event was handled, so we can remove it from the queue.
290
+ removePendingEvent (id );
291
+ return ;
265
292
}
266
293
267
294
// Since the framework didn't handle it, dispatch the event again.
268
295
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
296
view .getRootView ().dispatchKeyEvent (event );
273
- dispatchingKeyEvent = false ;
274
297
}
275
298
}
276
299
}
0 commit comments