Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 3eb72ea

Browse files
committed
Converted to a deque, added comments
1 parent 44a834a commit 3eb72ea

File tree

2 files changed

+188
-93
lines changed

2 files changed

+188
-93
lines changed

shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java

+158-82
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
package io.flutter.embedding.android;
66

77
import java.util.Map;
8+
import java.util.Map.Entry;
89
import java.util.List;
910
import java.util.ArrayList;
1011
import java.util.HashMap;
11-
import java.util.MapEntry;
12+
import java.util.Deque;
13+
import java.util.ArrayDeque;
14+
import java.util.AbstractMap.SimpleImmutableEntry;
1215

1316
import android.app.Activity;
1417
import android.content.Context;
@@ -21,48 +24,65 @@
2124
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
2225
import io.flutter.plugin.editing.TextInputPlugin;
2326

27+
/**
28+
* A class to process key events from Android, passing them to the framework as
29+
* messages using {@link KeyEventChannel}.
30+
*/
2431
public class AndroidKeyProcessor {
2532
private static final String TAG = "AndroidKeyProcessor";
2633

27-
@NonNull private final KeyEventChannel keyEventChannel;
28-
@NonNull private final TextInputPlugin textInputPlugin;
29-
@NonNull private final Context context;
34+
@NonNull
35+
private final KeyEventChannel keyEventChannel;
36+
@NonNull
37+
private final TextInputPlugin textInputPlugin;
38+
@NonNull
3039
private int combiningCharacter;
40+
@NonNull
41+
private EventResponder eventResponder;
3142

32-
private Map<Long, KeyEvent> pendingEvents = new HashMap<Long, KeyEvent>();
33-
private boolean dispatchingKeyEvent = false;
34-
35-
public AndroidKeyProcessor(@NonNull Context context, @NonNull KeyEventChannel keyEventChannel, @NonNull TextInputPlugin textInputPlugin) {
43+
public AndroidKeyProcessor(@NonNull Context context, @NonNull KeyEventChannel keyEventChannel,
44+
@NonNull TextInputPlugin textInputPlugin) {
3645
this.keyEventChannel = keyEventChannel;
3746
this.textInputPlugin = textInputPlugin;
38-
this.context = context;
39-
this.keyEventChannel.setKeyProcessor(this);
47+
this.eventResponder = new EventResponder(context);
48+
this.keyEventChannel.setEventResponseHandler(eventResponder);
4049
}
4150

51+
/**
52+
* Called when a key up event is received by the {@link FlutterView}.
53+
*
54+
* @param keyEvent the Android key event to respond to.
55+
* @return true if the key event was handled and should not be propagated.
56+
*/
4257
public boolean onKeyUp(@NonNull KeyEvent keyEvent) {
43-
if (dispatchingKeyEvent) {
58+
if (eventResponder.dispatchingKeyEvent) {
4459
// Don't handle it if it is from our own delayed event synthesis.
4560
return false;
4661
}
4762

4863
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
4964
KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
5065
keyEventChannel.keyUp(flutterEvent);
51-
pendingEvents.put(flutterEvent.eventId, keyEvent);
66+
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
5267
return true;
5368
}
5469

70+
/**
71+
* Called when a key down event is received by the {@link FlutterView}.
72+
*
73+
* @param keyEvent the Android key event to respond to.
74+
* @return true if the key event was handled and should not be propagated.
75+
*/
5576
public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
56-
if (dispatchingKeyEvent) {
77+
if (eventResponder.dispatchingKeyEvent) {
5778
// Don't handle it if it is from our own delayed event synthesis.
5879
return false;
5980
}
6081

6182
// If the textInputPlugin is still valid and accepting text, then we'll try
6283
// and send the key event to it, assuming that if the event can be sent, that
6384
// it has been handled.
64-
if (textInputPlugin.getLastInputConnection() != null
65-
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
85+
if (textInputPlugin.getLastInputConnection() != null && textInputPlugin.getInputMethodManager().isAcceptingText()) {
6686
if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) {
6787
return true;
6888
}
@@ -71,80 +91,43 @@ public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
7191
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
7292
KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
7393
keyEventChannel.keyDown(flutterEvent);
74-
pendingEvents.put(flutterEvent.eventId, keyEvent);
94+
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
7595
return true;
7696
}
7797

78-
public void onKeyEventHandled(@NonNull long id) {
79-
if (!pendingEvents.containsKey(id)) {
80-
Log.e(TAG, "Key with id " + id + " not found in pending key events list. " +
81-
"There are " + pendingEvents.size() + " pending events.");
82-
return;
83-
}
84-
// Since this event was already reported to Android as handled, we just
85-
// remove it from the map of pending events.
86-
pendingEvents.remove(id);
87-
Log.e(TAG, "Removing handled key with id " + id + " from pending key events list. " +
88-
"There are now " + pendingEvents.size() + " pending events.");
89-
}
90-
91-
public void onKeyEventNotHandled(@NonNull long id) {
92-
if (!pendingEvents.containsKey(id)) {
93-
Log.e(TAG, "Key with id " + id + " not found in pending key events list. " +
94-
"There are " + pendingEvents.size() + " pending events.");
95-
return;
96-
}
97-
// Since this event was NOT handled by the framework we now synthesize a
98-
// new, identical, key event to pass along.
99-
KeyEvent pendingEvent = pendingEvents.remove(id);
100-
Log.e(TAG, "Removing unhandled key with id " + id + " from pending key events list. " +
101-
"There are now " + pendingEvents.size() + " pending events.");
102-
Activity activity = getActivity(context);
103-
if (activity != null) {
104-
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
105-
// send it to the framework again.
106-
dispatchingKeyEvent = true;
107-
activity.dispatchKeyEvent(pendingEvent);
108-
dispatchingKeyEvent = false;
109-
}
110-
}
111-
112-
private Activity getActivity(Context context) {
113-
if (context instanceof Activity) {
114-
return (Activity) context;
115-
}
116-
if (context instanceof ContextWrapper) {
117-
// Recurse up chain of base contexts until we find an Activity.
118-
return getActivity(((ContextWrapper) context).getBaseContext());
119-
}
120-
return null;
121-
}
122-
12398
/**
124-
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
125-
* Unicode combining character and returns the combination of these characters if a combination
126-
* exists.
99+
* Applies the given Unicode character in {@code newCharacterCodePoint} to a
100+
* previously entered Unicode combining character and returns the combination of
101+
* these characters if a combination exists.
127102
*
128-
* <p>This method mutates {@link #combiningCharacter} over time to combine characters.
103+
* <p>
104+
* This method mutates {@link #combiningCharacter} over time to combine
105+
* characters.
129106
*
130-
* <p>One of the following things happens in this method:
107+
* <p>
108+
* One of the following things happens in this method:
131109
*
132110
* <ul>
133-
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
134-
* is not a combining character, then {@code newCharacterCodePoint} is returned.
135-
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
136-
* is a combining character, then {@code newCharacterCodePoint} is saved as the {@link
137-
* #combiningCharacter} and null is returned.
138-
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
139-
* also a combining character, then the {@code newCharacterCodePoint} is combined with the
140-
* existing {@link #combiningCharacter} and null is returned.
141-
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
142-
* not a combining character, then the {@link #combiningCharacter} is applied to the regular
143-
* {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link
144-
* #combiningCharacter} is cleared.
111+
* <li>If no previous {@link #combiningCharacter} exists and the
112+
* {@code newCharacterCodePoint} is not a combining character, then
113+
* {@code newCharacterCodePoint} is returned.
114+
* <li>If no previous {@link #combiningCharacter} exists and the
115+
* {@code newCharacterCodePoint} is a combining character, then
116+
* {@code newCharacterCodePoint} is saved as the {@link #combiningCharacter} and
117+
* null is returned.
118+
* <li>If a previous {@link #combiningCharacter} exists and the
119+
* {@code newCharacterCodePoint} is also a combining character, then the
120+
* {@code newCharacterCodePoint} is combined with the existing
121+
* {@link #combiningCharacter} and null is returned.
122+
* <li>If a previous {@link #combiningCharacter} exists and the
123+
* {@code newCharacterCodePoint} is not a combining character, then the
124+
* {@link #combiningCharacter} is applied to the regular
125+
* {@code newCharacterCodePoint} and the resulting complex character is
126+
* returned. The {@link #combiningCharacter} is cleared.
145127
* </ul>
146128
*
147-
* <p>The following reference explains the concept of a "combining character":
129+
* <p>
130+
* The following reference explains the concept of a "combining character":
148131
* https://en.wikipedia.org/wiki/Combining_character
149132
*/
150133
@Nullable
@@ -154,8 +137,7 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
154137
}
155138

156139
Character complexCharacter = (char) newCharacterCodePoint;
157-
boolean isNewCodePointACombiningCharacter =
158-
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
140+
boolean isNewCodePointACombiningCharacter = (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
159141
if (isNewCodePointACombiningCharacter) {
160142
// If a combining character was entered before, combine this one with that one.
161143
int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
@@ -165,7 +147,8 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
165147
combiningCharacter = plainCodePoint;
166148
}
167149
} else {
168-
// The new character is a regular character. Apply combiningCharacter to it, if it exists.
150+
// The new character is a regular character. Apply combiningCharacter to it, if
151+
// it exists.
169152
if (combiningCharacter != 0) {
170153
int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
171154
if (combinedChar > 0) {
@@ -177,4 +160,97 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
177160

178161
return complexCharacter;
179162
}
163+
164+
public static class EventResponder implements KeyEventChannel.EventResponseHandler {
165+
// The maximum number of pending events that are held before starting to
166+
// complain.
167+
private static final long MAX_PENDING_EVENTS = 1000;
168+
private final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
169+
@NonNull
170+
private final Context context;
171+
public boolean dispatchingKeyEvent = false;
172+
173+
public EventResponder(@NonNull Context context) {
174+
this.context = context;
175+
}
176+
177+
/**
178+
* Removes the pending event with the given id from the cache of pending events.
179+
*
180+
* @param id the id of the event to be removed.
181+
*/
182+
private KeyEvent removePendingEvent(@NonNull long id) {
183+
if (pendingEvents.getFirst().getKey() != id) {
184+
throw new AssertionError("Event response received out of order");
185+
}
186+
return pendingEvents.removeFirst().getValue();
187+
}
188+
189+
/**
190+
* Called whenever the framework responds that a given key event was handled by
191+
* the framework.
192+
*
193+
* @param id the event id of the event to be marked as being handled by the
194+
* framework. Must not be null.
195+
*/
196+
@Override
197+
public void onKeyEventHandled(@NonNull long id) {
198+
removePendingEvent(id);
199+
Log.v(TAG, "Removed handled key with id " + id + " from pending key events list. " + "There are now "
200+
+ pendingEvents.size() + " pending events.");
201+
}
202+
203+
/**
204+
* Called whenever the framework responds that a given key event wasn't handled
205+
* by the framework.
206+
*
207+
* @param id the event id of the event to be marked as not being handled by the
208+
* framework. Must not be null.
209+
*/
210+
@Override
211+
public void onKeyEventNotHandled(@NonNull long id) {
212+
KeyEvent pendingEvent = removePendingEvent(id);
213+
Log.v(TAG, "Removed unhandled key with id " + id + " from pending key events list. " + "There are now "
214+
+ pendingEvents.size() + " pending events.");
215+
216+
// Since the framework didn't handle it, dispatch the key again.
217+
Activity activity = getActivity(context);
218+
if (activity != null) {
219+
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
220+
// send it to the framework again.
221+
dispatchingKeyEvent = true;
222+
activity.dispatchKeyEvent(pendingEvent);
223+
dispatchingKeyEvent = false;
224+
}
225+
}
226+
227+
/**
228+
* Adds the given event with the given id to the event manager to wait for a
229+
* response.
230+
*/
231+
public void addEvent(long id, @NonNull KeyEvent event) {
232+
pendingEvents.addLast(new SimpleImmutableEntry<Long, KeyEvent>(id, event));
233+
if (pendingEvents.size() > MAX_PENDING_EVENTS) {
234+
Log.e(TAG, "There are " + pendingEvents.size() + " keyboard events "
235+
+ "that have not yet received a response. Are responses being sent?");
236+
}
237+
}
238+
239+
/**
240+
* Gets the nearest ancestor Activity for the given Context.
241+
*
242+
* @param context the context to look in for the activity.
243+
* @return null if no Activity found.
244+
*/
245+
private Activity getActivity(Context context) {
246+
if (context instanceof Activity) {
247+
return (Activity) context;
248+
}
249+
if (context instanceof ContextWrapper) {
250+
// Recurse up chain of base contexts until we find an Activity.
251+
return getActivity(((ContextWrapper) context).getBaseContext());
252+
}
253+
return null;
254+
}
255+
}
180256
}

shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java

+30-11
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,35 @@ public class KeyEventChannel {
3030
private static long eventIdSerial = 0;
3131

3232
/**
33-
* Sets the key processor to be used to receive messages from the framework on
34-
* this channel.
33+
* Sets the event response handler to be used to receive key event response
34+
* messages from the framework on this channel.
3535
*/
36-
public void setKeyProcessor(AndroidKeyProcessor processor) {
37-
this.processor = processor;
36+
public void setEventResponseHandler(EventResponseHandler handler) {
37+
this.eventResponseHandler = handler;
38+
}
39+
private EventResponseHandler eventResponseHandler;
40+
41+
/** A handler of incoming key handling messages. */
42+
public interface EventResponseHandler {
43+
44+
/**
45+
* Called whenever the framework responds that a given key event was handled by
46+
* the framework.
47+
*
48+
* @param id the event id of the event to be marked as being handled by the
49+
* framework. Must not be null.
50+
*/
51+
public void onKeyEventHandled(@NonNull long id);
52+
53+
/**
54+
* Called whenever the framework responds that a given key event wasn't handled
55+
* by the framework.
56+
*
57+
* @param id the event id of the event to be marked as not being handled by the
58+
* framework. Must not be null.
59+
*/
60+
public void onKeyEventNotHandled(@NonNull long id);
3861
}
39-
private AndroidKeyProcessor processor;
4062

4163
private final BasicMessageChannel.MessageHandler<Object> messageHandler =
4264
new BasicMessageChannel.MessageHandler<Object>() {
@@ -47,7 +69,7 @@ public void onMessage(
4769

4870
// If there is no processor to respond to this message then we don't need to
4971
// parse it.
50-
if (processor == null) {
72+
if (eventResponseHandler == null) {
5173
return;
5274
}
5375

@@ -56,15 +78,12 @@ public void onMessage(
5678
final String type = annotatedEvent.getString("type");
5779
final JSONObject data = annotatedEvent.getJSONObject("data");
5880

59-
Log.v(TAG, "Received " + type + " message.");
6081
switch (type) {
6182
case "keyHandled":
62-
Log.w(TAG, "Handled key event " + data.getLong("eventId"));
63-
processor.onKeyEventHandled(data.getLong("eventId"));
83+
eventResponseHandler.onKeyEventHandled(data.getLong("eventId"));
6484
break;
6585
case "keyNotHandled":
66-
Log.w(TAG, "Did not handle key event " + data.getLong("eventId"));
67-
processor.onKeyEventNotHandled(data.getLong("eventId"));
86+
eventResponseHandler.onKeyEventNotHandled(data.getLong("eventId"));
6887
break;
6988
}
7089
} catch (JSONException e) {

0 commit comments

Comments
 (0)