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

Commit 579dee8

Browse files
committed
Converted to a deque, added comments
1 parent 44a834a commit 579dee8

File tree

3 files changed

+166
-88
lines changed

3 files changed

+166
-88
lines changed

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

+128-67
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44

55
package io.flutter.embedding.android;
66

7-
import java.util.Map;
8-
import java.util.List;
9-
import java.util.ArrayList;
10-
import java.util.HashMap;
11-
import java.util.MapEntry;
12-
137
import android.app.Activity;
148
import android.content.Context;
159
import android.content.ContextWrapper;
@@ -20,40 +14,61 @@
2014
import androidx.annotation.Nullable;
2115
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
2216
import io.flutter.plugin.editing.TextInputPlugin;
23-
17+
import java.util.AbstractMap.SimpleImmutableEntry;
18+
import java.util.ArrayDeque;
19+
import java.util.Deque;
20+
import java.util.Map.Entry;
21+
22+
/**
23+
* A class to process key events from Android, passing them to the framework as messages using
24+
* {@link KeyEventChannel}.
25+
*/
2426
public class AndroidKeyProcessor {
2527
private static final String TAG = "AndroidKeyProcessor";
2628

2729
@NonNull private final KeyEventChannel keyEventChannel;
2830
@NonNull private final TextInputPlugin textInputPlugin;
29-
@NonNull private final Context context;
30-
private int combiningCharacter;
31-
32-
private Map<Long, KeyEvent> pendingEvents = new HashMap<Long, KeyEvent>();
33-
private boolean dispatchingKeyEvent = false;
31+
@NonNull private int combiningCharacter;
32+
@NonNull private EventResponder eventResponder;
3433

35-
public AndroidKeyProcessor(@NonNull Context context, @NonNull KeyEventChannel keyEventChannel, @NonNull TextInputPlugin textInputPlugin) {
34+
public AndroidKeyProcessor(
35+
@NonNull Context context,
36+
@NonNull KeyEventChannel keyEventChannel,
37+
@NonNull TextInputPlugin textInputPlugin) {
3638
this.keyEventChannel = keyEventChannel;
3739
this.textInputPlugin = textInputPlugin;
38-
this.context = context;
39-
this.keyEventChannel.setKeyProcessor(this);
40+
this.eventResponder = new EventResponder(context);
41+
this.keyEventChannel.setEventResponseHandler(eventResponder);
4042
}
4143

44+
/**
45+
* Called when a key up event is received by the {@link FlutterView}.
46+
*
47+
* @param keyEvent the Android key event to respond to.
48+
* @return true if the key event was handled and should not be propagated.
49+
*/
4250
public boolean onKeyUp(@NonNull KeyEvent keyEvent) {
43-
if (dispatchingKeyEvent) {
51+
if (eventResponder.dispatchingKeyEvent) {
4452
// Don't handle it if it is from our own delayed event synthesis.
4553
return false;
4654
}
4755

4856
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
49-
KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
57+
KeyEventChannel.FlutterKeyEvent flutterEvent =
58+
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
5059
keyEventChannel.keyUp(flutterEvent);
51-
pendingEvents.put(flutterEvent.eventId, keyEvent);
60+
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
5261
return true;
5362
}
5463

64+
/**
65+
* Called when a key down event is received by the {@link FlutterView}.
66+
*
67+
* @param keyEvent the Android key event to respond to.
68+
* @return true if the key event was handled and should not be propagated.
69+
*/
5570
public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
56-
if (dispatchingKeyEvent) {
71+
if (eventResponder.dispatchingKeyEvent) {
5772
// Don't handle it if it is from our own delayed event synthesis.
5873
return false;
5974
}
@@ -69,57 +84,13 @@ public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
6984
}
7085

7186
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
72-
KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
87+
KeyEventChannel.FlutterKeyEvent flutterEvent =
88+
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
7389
keyEventChannel.keyDown(flutterEvent);
74-
pendingEvents.put(flutterEvent.eventId, keyEvent);
90+
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
7591
return true;
7692
}
7793

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-
12394
/**
12495
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
12596
* Unicode combining character and returns the combination of these characters if a combination
@@ -165,7 +136,8 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
165136
combiningCharacter = plainCodePoint;
166137
}
167138
} else {
168-
// The new character is a regular character. Apply combiningCharacter to it, if it exists.
139+
// The new character is a regular character. Apply combiningCharacter to it, if
140+
// it exists.
169141
if (combiningCharacter != 0) {
170142
int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
171143
if (combinedChar > 0) {
@@ -177,4 +149,93 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
177149

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

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,8 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
814814
this.flutterEngine.getTextInputChannel(),
815815
this.flutterEngine.getPlatformViewsController());
816816
androidKeyProcessor =
817-
new AndroidKeyProcessor(getContext(), this.flutterEngine.getKeyEventChannel(), textInputPlugin);
817+
new AndroidKeyProcessor(
818+
getContext(), this.flutterEngine.getKeyEventChannel(), textInputPlugin);
818819
androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
819820
accessibilityBridge =
820821
new AccessibilityBridge(

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

+36-20
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,63 @@
1010
import android.view.KeyEvent;
1111
import androidx.annotation.NonNull;
1212
import androidx.annotation.Nullable;
13-
import io.flutter.embedding.android.AndroidKeyProcessor;
1413
import io.flutter.embedding.engine.dart.DartExecutor;
1514
import io.flutter.plugin.common.BasicMessageChannel;
1615
import io.flutter.plugin.common.JSONMessageCodec;
1716
import java.util.HashMap;
1817
import java.util.Map;
19-
import org.json.JSONObject;
2018
import org.json.JSONException;
19+
import org.json.JSONObject;
2120

2221
/**
2322
* Event message channel for key events to/from the Flutter framework.
2423
*
25-
* <p>Sends key up/down events to the framework, and receives asynchronous messages
26-
* from the framework about whether or not the key was handled.
27-
*/
24+
* <p>Sends key up/down events to the framework, and receives asynchronous messages from the
25+
* framework about whether or not the key was handled.
26+
*/
2827
public class KeyEventChannel {
2928
private static final String TAG = "KeyEventChannel";
3029
private static long eventIdSerial = 0;
3130

3231
/**
33-
* Sets the key processor to be used to receive messages from the framework on
34-
* this channel.
32+
* Sets the event response handler to be used to receive key event response messages from the
33+
* framework on this channel.
3534
*/
36-
public void setKeyProcessor(AndroidKeyProcessor processor) {
37-
this.processor = processor;
35+
public void setEventResponseHandler(EventResponseHandler handler) {
36+
this.eventResponseHandler = handler;
37+
}
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 the framework.
46+
*
47+
* @param id the event id of the event to be marked as being handled by the framework. Must not
48+
* be null.
49+
*/
50+
public void onKeyEventHandled(@NonNull long id);
51+
52+
/**
53+
* Called whenever the framework responds that a given key event wasn't handled by the
54+
* framework.
55+
*
56+
* @param id the event id of the event to be marked as not being handled by the framework. Must
57+
* not be null.
58+
*/
59+
public void onKeyEventNotHandled(@NonNull long id);
3860
}
39-
private AndroidKeyProcessor processor;
4061

4162
private final BasicMessageChannel.MessageHandler<Object> messageHandler =
4263
new BasicMessageChannel.MessageHandler<Object>() {
4364
@Override
4465
public void onMessage(
4566
@Nullable Object message, @NonNull BasicMessageChannel.Reply<Object> reply) {
46-
Log.v(TAG, "Received key event channel message.");
47-
48-
// If there is no processor to respond to this message then we don't need to
49-
// parse it.
50-
if (processor == null) {
67+
// If there is no handler to respond to this message then we don't
68+
// need to parse it.
69+
if (eventResponseHandler == null) {
5170
return;
5271
}
5372

@@ -56,15 +75,12 @@ public void onMessage(
5675
final String type = annotatedEvent.getString("type");
5776
final JSONObject data = annotatedEvent.getJSONObject("data");
5877

59-
Log.v(TAG, "Received " + type + " message.");
6078
switch (type) {
6179
case "keyHandled":
62-
Log.w(TAG, "Handled key event " + data.getLong("eventId"));
63-
processor.onKeyEventHandled(data.getLong("eventId"));
80+
eventResponseHandler.onKeyEventHandled(data.getLong("eventId"));
6481
break;
6582
case "keyNotHandled":
66-
Log.w(TAG, "Did not handle key event " + data.getLong("eventId"));
67-
processor.onKeyEventNotHandled(data.getLong("eventId"));
83+
eventResponseHandler.onKeyEventNotHandled(data.getLong("eventId"));
6884
break;
6985
}
7086
} catch (JSONException e) {

0 commit comments

Comments
 (0)