5
5
package io .flutter .embedding .android ;
6
6
7
7
import java .util .Map ;
8
+ import java .util .Map .Entry ;
8
9
import java .util .List ;
9
10
import java .util .ArrayList ;
10
11
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 ;
12
15
13
16
import android .app .Activity ;
14
17
import android .content .Context ;
21
24
import io .flutter .embedding .engine .systemchannels .KeyEventChannel ;
22
25
import io .flutter .plugin .editing .TextInputPlugin ;
23
26
27
+ /**
28
+ * A class to process key events from Android, passing them to the framework as
29
+ * messages using {@link KeyEventChannel}.
30
+ */
24
31
public class AndroidKeyProcessor {
25
32
private static final String TAG = "AndroidKeyProcessor" ;
26
33
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
30
39
private int combiningCharacter ;
40
+ @ NonNull
41
+ private EventResponder eventResponder ;
31
42
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 ) {
36
45
this .keyEventChannel = keyEventChannel ;
37
46
this .textInputPlugin = textInputPlugin ;
38
- this .context = context ;
39
- this .keyEventChannel .setKeyProcessor ( this );
47
+ this .eventResponder = new EventResponder ( context ) ;
48
+ this .keyEventChannel .setEventResponseHandler ( eventResponder );
40
49
}
41
50
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
+ */
42
57
public boolean onKeyUp (@ NonNull KeyEvent keyEvent ) {
43
- if (dispatchingKeyEvent ) {
58
+ if (eventResponder . dispatchingKeyEvent ) {
44
59
// Don't handle it if it is from our own delayed event synthesis.
45
60
return false ;
46
61
}
47
62
48
63
Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
49
64
KeyEventChannel .FlutterKeyEvent flutterEvent = new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
50
65
keyEventChannel .keyUp (flutterEvent );
51
- pendingEvents . put (flutterEvent .eventId , keyEvent );
66
+ eventResponder . addEvent (flutterEvent .eventId , keyEvent );
52
67
return true ;
53
68
}
54
69
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
+ */
55
76
public boolean onKeyDown (@ NonNull KeyEvent keyEvent ) {
56
- if (dispatchingKeyEvent ) {
77
+ if (eventResponder . dispatchingKeyEvent ) {
57
78
// Don't handle it if it is from our own delayed event synthesis.
58
79
return false ;
59
80
}
60
81
61
82
// If the textInputPlugin is still valid and accepting text, then we'll try
62
83
// and send the key event to it, assuming that if the event can be sent, that
63
84
// it has been handled.
64
- if (textInputPlugin .getLastInputConnection () != null
65
- && textInputPlugin .getInputMethodManager ().isAcceptingText ()) {
85
+ if (textInputPlugin .getLastInputConnection () != null && textInputPlugin .getInputMethodManager ().isAcceptingText ()) {
66
86
if (textInputPlugin .getLastInputConnection ().sendKeyEvent (keyEvent )) {
67
87
return true ;
68
88
}
@@ -71,80 +91,43 @@ public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
71
91
Character complexCharacter = applyCombiningCharacterToBaseCharacter (keyEvent .getUnicodeChar ());
72
92
KeyEventChannel .FlutterKeyEvent flutterEvent = new KeyEventChannel .FlutterKeyEvent (keyEvent , complexCharacter );
73
93
keyEventChannel .keyDown (flutterEvent );
74
- pendingEvents . put (flutterEvent .eventId , keyEvent );
94
+ eventResponder . addEvent (flutterEvent .eventId , keyEvent );
75
95
return true ;
76
96
}
77
97
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
98
/**
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.
127
102
*
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.
129
106
*
130
- * <p>One of the following things happens in this method:
107
+ * <p>
108
+ * One of the following things happens in this method:
131
109
*
132
110
* <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.
145
127
* </ul>
146
128
*
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":
148
131
* https://en.wikipedia.org/wiki/Combining_character
149
132
*/
150
133
@ Nullable
@@ -154,8 +137,7 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
154
137
}
155
138
156
139
Character complexCharacter = (char ) newCharacterCodePoint ;
157
- boolean isNewCodePointACombiningCharacter =
158
- (newCharacterCodePoint & KeyCharacterMap .COMBINING_ACCENT ) != 0 ;
140
+ boolean isNewCodePointACombiningCharacter = (newCharacterCodePoint & KeyCharacterMap .COMBINING_ACCENT ) != 0 ;
159
141
if (isNewCodePointACombiningCharacter ) {
160
142
// If a combining character was entered before, combine this one with that one.
161
143
int plainCodePoint = newCharacterCodePoint & KeyCharacterMap .COMBINING_ACCENT_MASK ;
@@ -165,7 +147,8 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
165
147
combiningCharacter = plainCodePoint ;
166
148
}
167
149
} 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.
169
152
if (combiningCharacter != 0 ) {
170
153
int combinedChar = KeyCharacterMap .getDeadChar (combiningCharacter , newCharacterCodePoint );
171
154
if (combinedChar > 0 ) {
@@ -177,4 +160,97 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
177
160
178
161
return complexCharacter ;
179
162
}
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
+ }
180
256
}
0 commit comments