Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
KeyboardManager.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.embedding.android;
6
7import android.view.KeyCharacterMap;
8import android.view.KeyEvent;
9import androidx.annotation.NonNull;
10import io.flutter.Log;
11import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
12import io.flutter.embedding.engine.systemchannels.KeyboardChannel;
13import io.flutter.plugin.common.BinaryMessenger;
14import io.flutter.plugin.editing.InputConnectionAdaptor;
15import io.flutter.plugin.editing.TextInputPlugin;
16import java.util.HashSet;
17import java.util.Map;
18
19/**
20 * Processes keyboard events and cooperate with {@link TextInputPlugin}.
21 *
22 * <p>Flutter uses asynchronous event handling to avoid blocking the UI thread, but Android requires
23 * that events are handled synchronously. So when the Android system sends new @{link KeyEvent} to
24 * Flutter, Flutter responds synchronously that the key has been handled so that it won't propagate
25 * to other components. It then uses "delayed event synthesis", where it sends the event to the
26 * framework, and if the framework responds that it has not handled the event, then this class
27 * synthesizes a new event to send to Android, without handling it this time.
28 *
29 * <p>Flutter processes an Android {@link KeyEvent} with several components, each can choose whether
30 * to handled the event, and only unhandled events can move to the next section.
31 *
32 * <ul>
33 * <li><b>Keyboard</b>: Dispatch to the {@link KeyboardManager.Responder}s simultaneously. After
34 * all responders have responded (asynchronously), the event is considered handled if any
35 * responders decide to handle.
36 * <li><b>Text input</b>: Events are sent to {@link TextInputPlugin}, processed synchronously with
37 * a result of whether it is handled.
38 * <li><b>"Redispatch"</b>: If there's no currently focused text field in {@link TextInputPlugin},
39 * or the text field does not handle the {@link KeyEvent} either, the {@link KeyEvent} will be
40 * sent back to the top of the activity's view hierachy, allowing it to be "redispatched". The
41 * {@link KeyboardManager} will remember this event and skip the identical event at the next
42 * encounter.
43 * </ul>
44 */
45public class KeyboardManager
46 implements InputConnectionAdaptor.KeyboardDelegate, KeyboardChannel.KeyboardMethodHandler {
47 private static final String TAG = "KeyboardManager";
48
49 /**
50 * Applies the given Unicode character from {@link KeyEvent#getUnicodeChar()} to a previously
51 * entered Unicode combining character and returns the combination of these characters if a
52 * combination exists.
53 *
54 * <p>This class is not used by {@link KeyboardManager}, but by its responders.
55 */
56 public static class CharacterCombiner {
57 private int combiningCharacter = 0;
58
60
61 /**
62 * This method mutates {@link #combiningCharacter} over time to combine characters.
63 *
64 * <p>One of the following things happens in this method:
65 *
66 * <ul>
67 * <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
68 * is not a combining character, then {@code newCharacterCodePoint} is returned.
69 * <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
70 * is a combining character, then {@code newCharacterCodePoint} is saved as the {@link
71 * #combiningCharacter} and null is returned.
72 * <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
73 * is also a combining character, then the {@code newCharacterCodePoint} is combined with
74 * the existing {@link #combiningCharacter} and null is returned.
75 * <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
76 * is not a combining character, then the {@link #combiningCharacter} is applied to the
77 * regular {@code newCharacterCodePoint} and the resulting complex character is returned.
78 * The {@link #combiningCharacter} is cleared.
79 * </ul>
80 *
81 * <p>The following reference explains the concept of a "combining character":
82 * https://en.wikipedia.org/wiki/Combining_character
83 */
84 Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) {
85 char complexCharacter = (char) newCharacterCodePoint;
86 boolean isNewCodePointACombiningCharacter =
87 (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
88 if (isNewCodePointACombiningCharacter) {
89 // If a combining character was entered before, combine this one with that one.
90 int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
91 if (combiningCharacter != 0) {
92 combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint);
93 } else {
94 combiningCharacter = plainCodePoint;
95 }
96 } else {
97 // The new character is a regular character. Apply combiningCharacter to it, if
98 // it exists.
99 if (combiningCharacter != 0) {
100 int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
101 if (combinedChar > 0) {
102 complexCharacter = (char) combinedChar;
103 }
104 combiningCharacter = 0;
105 }
106 }
107
108 return complexCharacter;
109 }
110 }
111
112 /**
113 * Construct a {@link KeyboardManager}.
114 *
115 * @param viewDelegate provides a set of interfaces that the keyboard manager needs to interact
116 * with other components and the platform, and is typically implements by {@link FlutterView}.
117 */
118 public KeyboardManager(@NonNull ViewDelegate viewDelegate) {
119 this.viewDelegate = viewDelegate;
120 this.responders =
121 new Responder[] {
122 new KeyEmbedderResponder(viewDelegate.getBinaryMessenger()),
123 new KeyChannelResponder(new KeyEventChannel(viewDelegate.getBinaryMessenger())),
124 };
125 final KeyboardChannel keyboardChannel = new KeyboardChannel(viewDelegate.getBinaryMessenger());
126 keyboardChannel.setKeyboardMethodHandler(this);
127 }
128
129 /**
130 * The interface for responding to a {@link KeyEvent} asynchronously.
131 *
132 * <p>Implementers of this interface should be owned by a {@link KeyboardManager}, in order to
133 * receive key events.
134 *
135 * <p>After receiving a {@link KeyEvent}, the {@link Responder} must call the supplied {@link
136 * OnKeyEventHandledCallback} exactly once, to inform the {@link KeyboardManager} whether it
137 * wishes to handle the {@link KeyEvent}. The {@link KeyEvent} will not be propagated to the
138 * {@link TextInputPlugin} or be redispatched to the view hierachy if any key responders answered
139 * yes.
140 *
141 * <p>If a {@link Responder} fails to call the {@link OnKeyEventHandledCallback} callback, the
142 * {@link KeyEvent} will never be sent to the {@link TextInputPlugin}, and the {@link
143 * KeyboardManager} class can't detect such errors as there is no timeout.
144 */
145 public interface Responder {
147 void onKeyEventHandled(boolean canHandleEvent);
148 }
149
150 /**
151 * Informs this {@link Responder} that a new {@link KeyEvent} needs processing.
152 *
153 * @param keyEvent the new {@link KeyEvent} this {@link Responder} may be interested in.
154 * @param onKeyEventHandledCallback the method to call when this {@link Responder} has decided
155 * whether to handle the {@link KeyEvent}.
156 */
158 @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback);
159 }
160
161 /**
162 * A set of interfaces that the {@link KeyboardManager} needs to interact with other components
163 * and the platform, and is typically implements by {@link FlutterView}.
164 */
165 public interface ViewDelegate {
166 /** Returns a {@link BinaryMessenger} to send platform messages with. */
167 public BinaryMessenger getBinaryMessenger();
168
169 /**
170 * Send a {@link KeyEvent} that is not handled by the keyboard responders to the text input
171 * system.
172 *
173 * @param keyEvent the {@link KeyEvent} that should be processed by the text input system. It
174 * must not be null.
175 * @return Whether the text input handles the key event.
176 */
177 public boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent);
178
179 /** Send a {@link KeyEvent} that is not handled by Flutter back to the platform. */
180 public void redispatch(@NonNull KeyEvent keyEvent);
181 }
182
183 private class PerEventCallbackBuilder {
184 private class Callback implements Responder.OnKeyEventHandledCallback {
185 boolean isCalled = false;
186
187 @Override
188 public void onKeyEventHandled(boolean canHandleEvent) {
189 if (isCalled) {
190 throw new IllegalStateException(
191 "The onKeyEventHandledCallback should be called exactly once.");
192 }
193 isCalled = true;
194 unrepliedCount -= 1;
195 isEventHandled |= canHandleEvent;
196 if (unrepliedCount == 0 && !isEventHandled) {
197 onUnhandled(keyEvent);
198 }
199 }
200 }
201
202 PerEventCallbackBuilder(@NonNull KeyEvent keyEvent) {
203 this.keyEvent = keyEvent;
204 }
205
206 final KeyEvent keyEvent;
207 int unrepliedCount = responders.length;
208 boolean isEventHandled = false;
209
210 public Responder.OnKeyEventHandledCallback buildCallback() {
211 return new Callback();
212 }
213 }
214
215 protected final Responder[] responders;
216 private final HashSet<KeyEvent> redispatchedEvents = new HashSet<>();
217 private final ViewDelegate viewDelegate;
218
219 @Override
220 public boolean handleEvent(@NonNull KeyEvent keyEvent) {
221 final boolean isRedispatchedEvent = redispatchedEvents.remove(keyEvent);
222 if (isRedispatchedEvent) {
223 return false;
224 }
225
226 if (responders.length > 0) {
227 final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent);
228 for (final Responder primaryResponder : responders) {
229 primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback());
230 }
231 } else {
232 onUnhandled(keyEvent);
233 }
234
235 return true;
236 }
237
238 public void destroy() {
239 final int remainingRedispatchCount = redispatchedEvents.size();
240 if (remainingRedispatchCount > 0) {
241 Log.w(
242 TAG,
243 "A KeyboardManager was destroyed with "
244 + String.valueOf(remainingRedispatchCount)
245 + " unhandled redispatch event(s).");
246 }
247 }
248
249 private void onUnhandled(@NonNull KeyEvent keyEvent) {
250 if (viewDelegate == null || viewDelegate.onTextInputKeyEvent(keyEvent)) {
251 return;
252 }
253
254 redispatchedEvents.add(keyEvent);
255 viewDelegate.redispatch(keyEvent);
256 if (redispatchedEvents.remove(keyEvent)) {
257 Log.w(TAG, "A redispatched key event was consumed before reaching KeyboardManager");
258 }
259 }
260
261 /**
262 * Returns an unmodifiable view of the pressed state.
263 *
264 * @return A map whose keys are physical keyboard key IDs and values are the corresponding logical
265 * keyboard key IDs.
266 */
267 public Map<Long, Long> getKeyboardState() {
269 return embedderResponder.getPressedState();
270 }
271}
static void w(@NonNull String tag, @NonNull String message)
Definition Log.java:76
Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint)
boolean handleEvent(@NonNull KeyEvent keyEvent)
KeyboardManager(@NonNull ViewDelegate viewDelegate)
void handleEvent( @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback)
boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent)
#define TAG()