Flutter Engine
The Flutter Engine
KeyEmbedderResponder.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.InputDevice;
8import android.view.KeyEvent;
9import androidx.annotation.NonNull;
10import androidx.annotation.Nullable;
11import io.flutter.Log;
12import io.flutter.embedding.android.KeyboardMap.PressingGoal;
13import io.flutter.embedding.android.KeyboardMap.TogglingGoal;
14import io.flutter.plugin.common.BinaryMessenger;
15import java.util.ArrayList;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.Map;
19
20/**
21 * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending
22 * processed information in {@link KeyData}.
23 *
24 * <p>This class corresponds to the HardwareKeyboard API in the framework.
25 */
26public class KeyEmbedderResponder implements KeyboardManager.Responder {
27 private static final String TAG = "KeyEmbedderResponder";
28
29 // Maps KeyEvent's action and repeatCount to a KeyData type.
30 private static KeyData.Type getEventType(KeyEvent event) {
31 final boolean isRepeatEvent = event.getRepeatCount() > 0;
32 switch (event.getAction()) {
33 case KeyEvent.ACTION_DOWN:
34 return isRepeatEvent ? KeyData.Type.kRepeat : KeyData.Type.kDown;
35 case KeyEvent.ACTION_UP:
36 return KeyData.Type.kUp;
37 default:
38 throw new AssertionError("Unexpected event type");
39 }
40 }
41
42 // The messenger that is used to send Flutter key events to the framework.
43 //
44 // On `handleEvent`, Flutter events are marshalled into byte buffers in the format specified by
45 // `KeyData.toBytes`.
46 @NonNull private final BinaryMessenger messenger;
47 // The keys being pressed currently, mapped from physical keys to logical keys.
48 @NonNull private final HashMap<Long, Long> pressingRecords = new HashMap<>();
49 // Map from logical key to toggling goals.
50 //
51 // Besides immutable configuration, the toggling goals are also used to store the current enabling
52 // states in their `enabled` field.
53 @NonNull private final HashMap<Long, TogglingGoal> togglingGoals = new HashMap<>();
54
55 @NonNull
56 private final KeyboardManager.CharacterCombiner characterCombiner =
58
60 this.messenger = messenger;
61 for (final TogglingGoal goal : KeyboardMap.getTogglingGoals()) {
62 togglingGoals.put(goal.logicalKey, goal);
63 }
64 }
65
66 private static long keyOfPlane(long key, long plane) {
67 // Apply '& kValueMask' in case the key is a negative number before being converted to long.
68 return plane | (key & KeyboardMap.kValueMask);
69 }
70
71 // Get the physical key for this event.
72 //
73 // The returned value is never null.
74 private Long getPhysicalKey(@NonNull KeyEvent event) {
75 final long scancode = event.getScanCode();
76 // Scancode 0 can occur during emulation using `adb shell input keyevent`. Synthesize a physical
77 // key from the key code so that keys can be told apart.
78 if (scancode == 0) {
79 // The key code can't also be 0, since those events have been filtered.
80 return keyOfPlane(event.getKeyCode(), KeyboardMap.kAndroidPlane);
81 }
82 final Long byMapping = KeyboardMap.scanCodeToPhysical.get(scancode);
83 if (byMapping != null) {
84 return byMapping;
85 }
86 return keyOfPlane(event.getScanCode(), KeyboardMap.kAndroidPlane);
87 }
88
89 // Get the logical key for this event.
90 //
91 // The returned value is never null.
92 private Long getLogicalKey(@NonNull KeyEvent event) {
93 final Long byMapping = KeyboardMap.keyCodeToLogical.get((long) event.getKeyCode());
94 if (byMapping != null) {
95 return byMapping;
96 }
97 return keyOfPlane(event.getKeyCode(), KeyboardMap.kAndroidPlane);
98 }
99
100 // Update `pressingRecords`.
101 //
102 // If the key indicated by `physicalKey` is currently not pressed, then `logicalKey` must not be
103 // null and this key will be marked pressed.
104 //
105 // If the key indicated by `physicalKey` is currently pressed, then `logicalKey` must be null
106 // and this key will be marked released.
107 void updatePressingState(@NonNull Long physicalKey, @Nullable Long logicalKey) {
108 if (logicalKey != null) {
109 final Long previousValue = pressingRecords.put(physicalKey, logicalKey);
110 if (previousValue != null) {
111 throw new AssertionError("The key was not empty");
112 }
113 } else {
114 final Long previousValue = pressingRecords.remove(physicalKey);
115 if (previousValue == null) {
116 throw new AssertionError("The key was empty");
117 }
118 }
119 }
120
121 // Synchronize for a pressing modifier (such as Shift or Ctrl).
122 //
123 // A pressing modifier is defined by a `PressingGoal`, which consists of a mask to get the true
124 // state out of `KeyEvent.getMetaState`, and a list of keys. The synchronization process
125 // dispatches synthesized events so that the state of these keys matches the true state taking
126 // the current event in consideration.
127 //
128 // Events that should be synthesized before the main event are synthesized
129 // immediately, while events that should be synthesized after the main event are appended to
130 // `postSynchronize`.
131 //
132 // Although Android KeyEvent defined bitmasks for sided modifiers (SHIFT_LEFT_ON and
133 // SHIFT_RIGHT_ON),
134 // this function only uses the unsided modifiers (SHIFT_ON), due to the weird behaviors observed
135 // on ChromeOS, where right modifiers produce events with UNSIDED | LEFT_SIDE meta state bits.
137 PressingGoal goal,
138 boolean truePressed,
139 long eventLogicalKey,
140 long eventPhysicalKey,
141 KeyEvent event,
142 ArrayList<Runnable> postSynchronize) {
143 // During an incoming event, there might be a synthesized Flutter event for each key of each
144 // pressing goal, followed by an eventual main Flutter event.
145 //
146 // NowState ----------------> PreEventState --------------> -------------->TrueState
147 // PreSynchronize Event PostSynchronize
148 //
149 // The goal of the synchronization algorithm is to derive a pre-event state that can satisfy the
150 // true state (`truePressed`) after the event, and that requires as few synthesized events based
151 // on the current state (`nowStates`) as possible.
152 final boolean[] nowStates = new boolean[goal.keys.length];
153 final Boolean[] preEventStates = new Boolean[goal.keys.length];
154 boolean postEventAnyPressed = false;
155 // 1. Find the current states of all keys.
156 // 2. Derive the pre-event state of the event key (if applicable.)
157 for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) {
158 final KeyboardMap.KeyPair key = goal.keys[keyIdx];
159 nowStates[keyIdx] = pressingRecords.containsKey(key.physicalKey);
160 if (key.logicalKey == eventLogicalKey) {
161 switch (getEventType(event)) {
162 case kDown:
163 preEventStates[keyIdx] = false;
164 postEventAnyPressed = true;
165 if (!truePressed) {
166 postSynchronize.add(
167 () ->
168 synthesizeEvent(
169 false, key.logicalKey, eventPhysicalKey, event.getEventTime()));
170 }
171 break;
172 case kUp:
173 // Incoming event is an up. Although the previous state should be pressed, don't
174 // synthesize a down event even if it's not. The later code will handle such cases by
175 // skipping abrupt up events. Obviously don't synthesize up events either.
176 preEventStates[keyIdx] = nowStates[keyIdx];
177 break;
178 case kRepeat:
179 // Incoming event is repeat. The previous state can be either pressed or released. Don't
180 // synthesize a down event here, or there will be a down event *and* a repeat event,
181 // both of which have printable characters. Obviously don't synthesize up events either.
182 if (!truePressed) {
183 postSynchronize.add(
184 () ->
185 synthesizeEvent(
186 false, key.logicalKey, key.physicalKey, event.getEventTime()));
187 }
188 preEventStates[keyIdx] = nowStates[keyIdx];
189 postEventAnyPressed = true;
190 break;
191 }
192 } else {
193 postEventAnyPressed = postEventAnyPressed || nowStates[keyIdx];
194 }
195 }
196
197 // Fill the rest of the pre-event states to match the true state.
198 if (truePressed) {
199 // It is required that at least one key is pressed.
200 for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) {
201 if (preEventStates[keyIdx] != null) {
202 continue;
203 }
204 if (postEventAnyPressed) {
205 preEventStates[keyIdx] = nowStates[keyIdx];
206 } else {
207 preEventStates[keyIdx] = true;
208 postEventAnyPressed = true;
209 }
210 }
211 if (!postEventAnyPressed) {
212 preEventStates[0] = true;
213 }
214 } else {
215 for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) {
216 if (preEventStates[keyIdx] != null) {
217 continue;
218 }
219 preEventStates[keyIdx] = false;
220 }
221 }
222
223 // Dispatch synthesized events for state differences.
224 for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) {
225 if (nowStates[keyIdx] != preEventStates[keyIdx]) {
226 final KeyboardMap.KeyPair key = goal.keys[keyIdx];
227 synthesizeEvent(
228 preEventStates[keyIdx], key.logicalKey, key.physicalKey, event.getEventTime());
229 }
230 }
231 }
232
233 // Synchronize for a toggling modifier (such as CapsLock).
234 //
235 // A toggling modifier is defined by a `TogglingGoal`, which consists of a mask to get the true
236 // state out of `KeyEvent.getMetaState`, and a key. The synchronization process dispatches
237 // synthesized events so that the state of these keys matches the true state taking the current
238 // event in consideration.
239 //
240 // Although Android KeyEvent defined bitmasks for all "lock" modifiers and define them as the
241 // "lock" state, weird behaviors are observed on ChromeOS. First, ScrollLock and NumLock presses
242 // do not set metaState bits. Second, CapsLock key events set the CapsLock bit as if it is a
243 // pressing modifier (key down having state 1, key up having state 0), while other key events set
244 // the CapsLock bit correctly (locked having state 1, unlocked having state 0). Therefore this
245 // function only synchronizes the CapsLock state, and only does so during non-CapsLock key events.
247 TogglingGoal goal, boolean trueEnabled, long eventLogicalKey, KeyEvent event) {
248 if (goal.logicalKey == eventLogicalKey) {
249 // Don't synthesize for self events, because the self events have weird metaStates on
250 // ChromeOS.
251 return;
252 }
253 if (goal.enabled != trueEnabled) {
254 final boolean firstIsDown = !pressingRecords.containsKey(goal.physicalKey);
255 if (firstIsDown) {
256 goal.enabled = !goal.enabled;
257 }
258 synthesizeEvent(firstIsDown, goal.logicalKey, goal.physicalKey, event.getEventTime());
259 if (!firstIsDown) {
260 goal.enabled = !goal.enabled;
261 }
262 synthesizeEvent(!firstIsDown, goal.logicalKey, goal.physicalKey, event.getEventTime());
263 }
264 }
265
266 // Implements the core algorithm of `handleEvent`.
267 //
268 // Returns whether any events are dispatched.
269 private boolean handleEventImpl(
270 @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) {
271 // Events with no codes at all can not be recognized.
272 if (event.getScanCode() == 0 && event.getKeyCode() == 0) {
273 return false;
274 }
275 final Long physicalKey = getPhysicalKey(event);
276 final Long logicalKey = getLogicalKey(event);
277
278 final ArrayList<Runnable> postSynchronizeEvents = new ArrayList<>();
279 for (final PressingGoal goal : KeyboardMap.pressingGoals) {
281 goal,
282 (event.getMetaState() & goal.mask) != 0,
283 logicalKey,
284 physicalKey,
285 event,
286 postSynchronizeEvents);
287 }
288
289 for (final TogglingGoal goal : togglingGoals.values()) {
290 synchronizeTogglingKey(goal, (event.getMetaState() & goal.mask) != 0, logicalKey, event);
291 }
292
293 boolean isDownEvent;
294 switch (event.getAction()) {
295 case KeyEvent.ACTION_DOWN:
296 isDownEvent = true;
297 break;
298 case KeyEvent.ACTION_UP:
299 isDownEvent = false;
300 break;
301 default:
302 return false;
303 }
304
305 KeyData.Type type;
306 String character = null;
307 final Long lastLogicalRecord = pressingRecords.get(physicalKey);
308 if (isDownEvent) {
309 if (lastLogicalRecord == null) {
310 type = KeyData.Type.kDown;
311 } else {
312 // A key has been pressed that has the exact physical key as a currently
313 // pressed one.
314 if (event.getRepeatCount() > 0) {
315 type = KeyData.Type.kRepeat;
316 } else {
317 synthesizeEvent(false, lastLogicalRecord, physicalKey, event.getEventTime());
318 type = KeyData.Type.kDown;
319 }
320 }
321 final char complexChar =
322 characterCombiner.applyCombiningCharacterToBaseCharacter(event.getUnicodeChar());
323 if (complexChar != 0) {
324 character = "" + complexChar;
325 }
326 } else { // isDownEvent is false
327 if (lastLogicalRecord == null) {
328 // Ignore abrupt up events.
329 return false;
330 } else {
331 type = KeyData.Type.kUp;
332 }
333 }
334
335 if (type != KeyData.Type.kRepeat) {
336 updatePressingState(physicalKey, isDownEvent ? logicalKey : null);
337 }
338 if (type == KeyData.Type.kDown) {
339 final TogglingGoal maybeTogglingGoal = togglingGoals.get(logicalKey);
340 if (maybeTogglingGoal != null) {
341 maybeTogglingGoal.enabled = !maybeTogglingGoal.enabled;
342 }
343 }
344
345 final KeyData output = new KeyData();
346
347 switch (event.getSource()) {
348 default:
349 case InputDevice.SOURCE_KEYBOARD:
350 output.deviceType = KeyData.DeviceType.kKeyboard;
351 break;
352 case InputDevice.SOURCE_DPAD:
353 output.deviceType = KeyData.DeviceType.kDirectionalPad;
354 break;
355 case InputDevice.SOURCE_GAMEPAD:
356 output.deviceType = KeyData.DeviceType.kGamepad;
357 break;
358 case InputDevice.SOURCE_JOYSTICK:
359 output.deviceType = KeyData.DeviceType.kJoystick;
360 break;
361 case InputDevice.SOURCE_HDMI:
362 output.deviceType = KeyData.DeviceType.kHdmi;
363 break;
364 }
365
366 output.timestamp = event.getEventTime();
367 output.type = type;
368 output.logicalKey = logicalKey;
369 output.physicalKey = physicalKey;
370 output.character = character;
371 output.synthesized = false;
372 output.deviceType = KeyData.DeviceType.kKeyboard;
373
374 sendKeyEvent(output, onKeyEventHandledCallback);
375 for (final Runnable postSyncEvent : postSynchronizeEvents) {
376 postSyncEvent.run();
377 }
378 return true;
379 }
380
381 private void synthesizeEvent(boolean isDown, Long logicalKey, Long physicalKey, long timestamp) {
382 final KeyData output = new KeyData();
383 output.timestamp = timestamp;
384 output.type = isDown ? KeyData.Type.kDown : KeyData.Type.kUp;
385 output.logicalKey = logicalKey;
386 output.physicalKey = physicalKey;
387 output.character = null;
388 output.synthesized = true;
389 output.deviceType = KeyData.DeviceType.kKeyboard;
390 if (physicalKey != 0 && logicalKey != 0) {
391 updatePressingState(physicalKey, isDown ? logicalKey : null);
392 }
393 sendKeyEvent(output, null);
394 }
395
396 private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHandledCallback) {
397 final BinaryMessenger.BinaryReply handleMessageReply =
398 onKeyEventHandledCallback == null
399 ? null
400 : message -> {
401 Boolean handled = false;
402 if (message != null) {
403 message.rewind();
404 if (message.capacity() != 0) {
405 handled = message.get() != 0;
406 }
407 } else {
408 Log.w(TAG, "A null reply was received when sending a key event to the framework.");
409 }
410 onKeyEventHandledCallback.onKeyEventHandled(handled);
411 };
412
413 messenger.send(KeyData.CHANNEL, data.toBytes(), handleMessageReply);
414 }
415
416 /**
417 * Parses an Android key event, performs synchronization, and dispatches Flutter events through
418 * the messenger to the framework with the given callback.
419 *
420 * <p>At least one event will be dispatched. If there are no others, an empty event with 0
421 * physical key and 0 logical key will be synthesized.
422 *
423 * @param event The Android key event to be handled.
424 * @param onKeyEventHandledCallback the method to call when the framework has decided whether to
425 * handle this event. This callback will always be called once and only once. If there are no
426 * non-synthesized out of this event, this callback will be called during this method with
427 * true.
428 */
429 @Override
430 public void handleEvent(
431 @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) {
432 final boolean sentAny = handleEventImpl(event, onKeyEventHandledCallback);
433 if (!sentAny) {
434 synthesizeEvent(true, 0L, 0L, 0L);
435 onKeyEventHandledCallback.onKeyEventHandled(true);
436 }
437 }
438
439 /**
440 * Returns an unmodifiable view of the pressed state.
441 *
442 * @return A map whose keys are physical keyboard key IDs and values are the corresponding logical
443 * keyboard key IDs.
444 */
445 public Map<Long, Long> getPressedState() {
446 return Collections.unmodifiableMap(pressingRecords);
447 }
448}
GLenum type
void handleEvent( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback)
void synchronizePressingKey(PressingGoal goal, boolean truePressed, long eventLogicalKey, long eventPhysicalKey, KeyEvent event, ArrayList< Runnable > postSynchronize)
void synchronizeTogglingKey(TogglingGoal goal, boolean trueEnabled, long eventLogicalKey, KeyEvent event)
void updatePressingState(@NonNull Long physicalKey, @Nullable Long logicalKey)
static TogglingGoal[] getTogglingGoals()
@ kUp
Definition: embedder.h:973
@ kDown
Definition: embedder.h:980
FlKeyEvent * event
Win32Message message
void Log(const char *format,...) SK_PRINTF_LIKE(1
Definition: TestRunner.cpp:137
std::shared_ptr< const fml::Mapping > data
Definition: texture_gles.cc:63