4
4
5
5
package io .flutter .embedding .android ;
6
6
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
-
13
7
import android .app .Activity ;
14
8
import android .content .Context ;
15
9
import android .content .ContextWrapper ;
20
14
import androidx .annotation .Nullable ;
21
15
import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
22
16
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
+ */
24
26
public class AndroidKeyProcessor {
25
27
private static final String TAG = "AndroidKeyProcessor" ;
26
28
27
29
@ NonNull private final KeyEventChannel keyEventChannel ;
28
30
@ 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 ;
34
33
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 ) {
36
38
this .keyEventChannel = keyEventChannel ;
37
39
this .textInputPlugin = textInputPlugin ;
38
- this .context = context ;
39
- this .keyEventChannel .setKeyProcessor ( this );
40
+ this .eventResponder = new EventResponder ( context ) ;
41
+ this .keyEventChannel .setEventResponseHandler ( eventResponder );
40
42
}
41
43
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
+ */
42
50
public boolean onKeyUp (@ NonNull KeyEvent keyEvent ) {
43
- if (dispatchingKeyEvent ) {
51
+ if (eventResponder . dispatchingKeyEvent ) {
44
52
// Don't handle it if it is from our own delayed event synthesis.
45
53
return false ;
46
54
}
47
55
48
56
Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
49
- KeyEventChannel .FlutterKeyEvent flutterEvent = new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
57
+ KeyEventChannel .FlutterKeyEvent flutterEvent =
58
+ new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
50
59
keyEventChannel .keyUp (flutterEvent );
51
- pendingEvents . put (flutterEvent .eventId , keyEvent );
60
+ eventResponder . addEvent (flutterEvent .eventId , keyEvent );
52
61
return true ;
53
62
}
54
63
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
+ */
55
70
public boolean onKeyDown (@ NonNull KeyEvent keyEvent ) {
56
- if (dispatchingKeyEvent ) {
71
+ if (eventResponder . dispatchingKeyEvent ) {
57
72
// Don't handle it if it is from our own delayed event synthesis.
58
73
return false ;
59
74
}
@@ -69,57 +84,13 @@ public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
69
84
}
70
85
71
86
Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
72
- KeyEventChannel .FlutterKeyEvent flutterEvent = new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
87
+ KeyEventChannel .FlutterKeyEvent flutterEvent =
88
+ new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
73
89
keyEventChannel .keyDown (flutterEvent );
74
- pendingEvents . put (flutterEvent .eventId , keyEvent );
90
+ eventResponder . addEvent (flutterEvent .eventId , keyEvent );
75
91
return true ;
76
92
}
77
93
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
-
123
94
/**
124
95
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
125
96
* Unicode combining character and returns the combination of these characters if a combination
@@ -165,7 +136,8 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
165
136
combiningCharacter = plainCodePoint ;
166
137
}
167
138
} 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.
169
141
if (combiningCharacter != 0 ) {
170
142
int combinedChar = KeyCharacterMap .getDeadChar (combiningCharacter , newCharacterCodePoint );
171
143
if (combinedChar > 0 ) {
@@ -177,4 +149,93 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
177
149
178
150
return complexCharacter ;
179
151
}
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
+ }
180
241
}
0 commit comments