From 88684a58b40bf86a598502fd44ec3622d3ab392a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 31 May 2022 13:00:39 -0700 Subject: [PATCH] Reland No.2 "Hardware Keyboard: Android" (#33686) * Original code * Changes * Fix and test Better formatted tests --- ci/licenses_golden/licenses_flutter | 3 + lib/ui/key.dart | 2 +- lib/ui/window/key_data.h | 3 + lib/ui/window/key_data_packet.h | 3 + shell/platform/android/BUILD.gn | 3 + .../android/KeyChannelResponder.java | 62 +- .../io/flutter/embedding/android/KeyData.java | 140 ++ .../android/KeyEmbedderResponder.java | 387 +++++ .../embedding/android/KeyboardManager.java | 67 +- .../embedding/android/KeyboardMap.java | 619 ++++++++ .../android/KeyChannelResponderTest.java | 26 - .../android/KeyboardManagerTest.java | 1247 ++++++++++++++++- .../editing/InputConnectionAdaptorTest.java | 4 +- .../test/io/flutter/util/FakeKeyEvent.java | 15 +- .../test/io/flutter/util/KeyCodes.java | 733 ++++++++++ shell/platform/embedder/embedder.cc | 2 + 16 files changed, 3166 insertions(+), 150 deletions(-) create mode 100644 shell/platform/android/io/flutter/embedding/android/KeyData.java create mode 100644 shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java create mode 100644 shell/platform/android/io/flutter/embedding/android/KeyboardMap.java create mode 100644 shell/platform/android/test/io/flutter/util/KeyCodes.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index ba5950a4bd33e..d9ef3796d1be9 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1352,7 +1352,10 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyData.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java diff --git a/lib/ui/key.dart b/lib/ui/key.dart index f3ced0774ac2a..20b67dd2de098 100644 --- a/lib/ui/key.dart +++ b/lib/ui/key.dart @@ -146,7 +146,7 @@ class KeyData { String? _escapeCharacter() { if (character == null) { - return character ?? ''; + return ''; } switch (character!) { case '\n': diff --git a/lib/ui/window/key_data.h b/lib/ui/window/key_data.h index a7785da12507d..e3b3ea5d6459d 100644 --- a/lib/ui/window/key_data.h +++ b/lib/ui/window/key_data.h @@ -28,6 +28,9 @@ enum class KeyEventType : int64_t { // a different way in KeyDataPacket. // // This structure is unpacked by hooks.dart. +// +// Changes to this struct must also be made to +// io/flutter/embedding/android/KeyData.java. struct alignas(8) KeyData { // Timestamp in microseconds from an arbitrary and consistent start point uint64_t timestamp; diff --git a/lib/ui/window/key_data_packet.h b/lib/ui/window/key_data_packet.h index e8efdf17334df..c6d31707d6f5a 100644 --- a/lib/ui/window/key_data_packet.h +++ b/lib/ui/window/key_data_packet.h @@ -14,6 +14,9 @@ namespace flutter { // A byte stream representing a key event, to be sent to the framework. +// +// Changes to the marshalling format here must also be made to +// io/flutter/embedding/android/KeyData.java. class KeyDataPacket { public: // Build the key data packet by providing information. diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index f18bb672e72e5..8714664376422 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -185,7 +185,10 @@ android_java_sources = [ "io/flutter/embedding/android/FlutterTextureView.java", "io/flutter/embedding/android/FlutterView.java", "io/flutter/embedding/android/KeyChannelResponder.java", + "io/flutter/embedding/android/KeyData.java", + "io/flutter/embedding/android/KeyEmbedderResponder.java", "io/flutter/embedding/android/KeyboardManager.java", + "io/flutter/embedding/android/KeyboardMap.java", "io/flutter/embedding/android/MotionEventTracker.java", "io/flutter/embedding/android/RenderMode.java", "io/flutter/embedding/android/SplashScreen.java", diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java index 1a9dce748418a..c4ac5aa649b5e 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -4,7 +4,6 @@ package io.flutter.embedding.android; -import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; @@ -19,66 +18,15 @@ public class KeyChannelResponder implements KeyboardManager.Responder { private static final String TAG = "KeyChannelResponder"; @NonNull private final KeyEventChannel keyEventChannel; - private int combiningCharacter; + + @NonNull + private final KeyboardManager.CharacterCombiner characterCombiner = + new KeyboardManager.CharacterCombiner(); public KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) { this.keyEventChannel = keyEventChannel; } - /** - * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered - * Unicode combining character and returns the combination of these characters if a combination - * exists. - * - *

This method mutates {@link #combiningCharacter} over time to combine characters. - * - *

One of the following things happens in this method: - * - *

    - *
  • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} - * is not a combining character, then {@code newCharacterCodePoint} is returned. - *
  • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} - * is a combining character, then {@code newCharacterCodePoint} is saved as the {@link - * #combiningCharacter} and null is returned. - *
  • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is - * also a combining character, then the {@code newCharacterCodePoint} is combined with the - * existing {@link #combiningCharacter} and null is returned. - *
  • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is - * not a combining character, then the {@link #combiningCharacter} is applied to the regular - * {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link - * #combiningCharacter} is cleared. - *
- * - *

The following reference explains the concept of a "combining character": - * https://en.wikipedia.org/wiki/Combining_character - */ - Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { - char complexCharacter = (char) newCharacterCodePoint; - boolean isNewCodePointACombiningCharacter = - (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; - if (isNewCodePointACombiningCharacter) { - // If a combining character was entered before, combine this one with that one. - int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK; - if (combiningCharacter != 0) { - combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint); - } else { - combiningCharacter = plainCodePoint; - } - } else { - // The new character is a regular character. Apply combiningCharacter to it, if - // it exists. - if (combiningCharacter != 0) { - int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint); - if (combinedChar > 0) { - complexCharacter = (char) combinedChar; - } - combiningCharacter = 0; - } - } - - return complexCharacter; - } - @Override public void handleEvent( @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { @@ -92,7 +40,7 @@ public void handleEvent( } final Character complexCharacter = - applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); + characterCombiner.applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java new file mode 100644 index 0000000000000..eb8a41767c576 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -0,0 +1,140 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * The resulting Flutter key events generated by {@link KeyEmbedderResponder}, and are sent through + * the messenger after being marshalled with {@link #toBytes()}. + * + *

This class is the Java adaption of {@code KeyData} and {@code KeyDataPacket} in the C engine. + * Changes made to either side must also be made to the other. + * + *

Each {@link KeyData} corresponds to a {@code ui.KeyData} in the framework. + */ +public class KeyData { + private static final String TAG = "KeyData"; + + /** + * The channel that key data should be sent through. + * + *

Must be kept in sync with kFlutterKeyDataChannel in embedder.cc + */ + public static final String CHANNEL = "flutter/keydata"; + + // The number of fields except for `character`. + private static final int FIELD_COUNT = 5; + private static final int BYTES_PER_FIELD = 8; + + /** The action type of the key data. */ + public enum Type { + kDown(0), + kUp(1), + kRepeat(2); + + private long value; + + private Type(long value) { + this.value = value; + } + + public long getValue() { + return value; + } + + static Type fromLong(long value) { + switch ((int) value) { + case 0: + return kDown; + case 1: + return kUp; + case 2: + return kRepeat; + default: + throw new AssertionError("Unexpected Type value"); + } + } + } + + /** Creates an empty {@link KeyData}. */ + public KeyData() {} + + /** + * Unmarshal fields from a buffer. + * + *

For the binary format, see {@code lib/ui/window/key_data_packet.h}. + */ + public KeyData(@NonNull ByteBuffer buffer) { + final long charSize = buffer.getLong(); + this.timestamp = buffer.getLong(); + this.type = Type.fromLong(buffer.getLong()); + this.physicalKey = buffer.getLong(); + this.logicalKey = buffer.getLong(); + this.synthesized = buffer.getLong() != 0; + + if (buffer.remaining() != charSize) { + throw new AssertionError( + String.format( + "Unexpected char length: charSize is %d while buffer has position %d, capacity %d, limit %d", + charSize, buffer.position(), buffer.capacity(), buffer.limit())); + } + this.character = null; + if (charSize != 0) { + final byte[] strBytes = new byte[(int) charSize]; + buffer.get(strBytes, 0, (int) charSize); + try { + this.character = new String(strBytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("UTF-8 unsupported"); + } + } + } + + long timestamp; + Type type; + long physicalKey; + long logicalKey; + boolean synthesized; + + /** The character of this key data encoded in UTF-8. */ + @Nullable String character; + + /** + * Marshal the key data to a new byte buffer. + * + *

For the binary format, see {@code lib/ui/window/key_data_packet.h}. + * + * @return the marshalled bytes. + */ + ByteBuffer toBytes() { + byte[] charBytes; + try { + charBytes = character == null ? null : character.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("UTF-8 not supported"); + } + final int charSize = charBytes == null ? 0 : charBytes.length; + final ByteBuffer packet = + ByteBuffer.allocateDirect((1 + FIELD_COUNT) * BYTES_PER_FIELD + charSize); + packet.order(ByteOrder.LITTLE_ENDIAN); + + packet.putLong(charSize); + packet.putLong(timestamp); + packet.putLong(type.getValue()); + packet.putLong(physicalKey); + packet.putLong(logicalKey); + packet.putLong(synthesized ? 1L : 0L); + if (charBytes != null) { + packet.put(charBytes); + } + + return packet; + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java new file mode 100644 index 0000000000000..734c8775faadc --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -0,0 +1,387 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import android.view.KeyEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.android.KeyboardMap.PressingGoal; +import io.flutter.embedding.android.KeyboardMap.TogglingGoal; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.HashMap; + +/** + * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending + * processed information in {@link KeyData}. + * + *

This class corresponds to the HardwareKeyboard API in the framework. + */ +public class KeyEmbedderResponder implements KeyboardManager.Responder { + private static final String TAG = "KeyEmbedderResponder"; + + // Maps KeyEvent's action and repeatCount to a KeyData type. + private static KeyData.Type getEventType(KeyEvent event) { + final boolean isRepeatEvent = event.getRepeatCount() > 0; + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + return isRepeatEvent ? KeyData.Type.kRepeat : KeyData.Type.kDown; + case KeyEvent.ACTION_UP: + return KeyData.Type.kUp; + default: + throw new AssertionError("Unexpected event type"); + } + } + + // The messenger that is used to send Flutter key events to the framework. + // + // On `handleEvent`, Flutter events are marshalled into byte buffers in the format specified by + // `KeyData.toBytes`. + @NonNull private final BinaryMessenger messenger; + // The keys being pressed currently, mapped from physical keys to logical keys. + @NonNull private final HashMap pressingRecords = new HashMap<>(); + // Map from logical key to toggling goals. + // + // Besides immutable configuration, the toggling goals are also used to store the current enabling + // states in their `enabled` field. + @NonNull private final HashMap togglingGoals = new HashMap<>(); + + @NonNull + private final KeyboardManager.CharacterCombiner characterCombiner = + new KeyboardManager.CharacterCombiner(); + + public KeyEmbedderResponder(BinaryMessenger messenger) { + this.messenger = messenger; + for (final TogglingGoal goal : KeyboardMap.getTogglingGoals()) { + togglingGoals.put(goal.logicalKey, goal); + } + } + + private static long keyOfPlane(long key, long plane) { + // Apply '& kValueMask' in case the key is a negative number before being converted to long. + return plane | (key & KeyboardMap.kValueMask); + } + + // Get the physical key for this event. + // + // The returned value is never null. + private Long getPhysicalKey(@NonNull KeyEvent event) { + final long scancode = event.getScanCode(); + // Scancode 0 can occur during emulation using `adb shell input keyevent`. Synthesize a physical + // key from the key code so that keys can be told apart. + if (scancode == 0) { + // The key code can't also be 0, since those events have been filtered. + return keyOfPlane(event.getKeyCode(), KeyboardMap.kLogicalPlane); + } + final Long byMapping = KeyboardMap.scanCodeToPhysical.get(scancode); + if (byMapping != null) { + return byMapping; + } + return keyOfPlane(event.getScanCode(), KeyboardMap.kAndroidPlane); + } + + // Get the logical key for this event. + // + // The returned value is never null. + private Long getLogicalKey(@NonNull KeyEvent event) { + final Long byMapping = KeyboardMap.keyCodeToLogical.get((long) event.getKeyCode()); + if (byMapping != null) { + return byMapping; + } + return keyOfPlane(event.getKeyCode(), KeyboardMap.kAndroidPlane); + } + + // Update `pressingRecords`. + // + // If the key indicated by `physicalKey` is currently not pressed, then `logicalKey` must not be + // null and this key will be marked pressed. + // + // If the key indicated by `physicalKey` is currently pressed, then `logicalKey` must be null + // and this key will be marked released. + void updatePressingState(@NonNull Long physicalKey, @Nullable Long logicalKey) { + if (logicalKey != null) { + final Long previousValue = pressingRecords.put(physicalKey, logicalKey); + if (previousValue != null) { + throw new AssertionError("The key was not empty"); + } + } else { + final Long previousValue = pressingRecords.remove(physicalKey); + if (previousValue == null) { + throw new AssertionError("The key was empty"); + } + } + } + + // Synchronize for a pressing modifier (such as Shift or Ctrl). + // + // A pressing modifier is defined by a `PressingGoal`, which consists of a mask to get the true + // state out of `KeyEvent.getMetaState`, and a list of keys. The synchronization process + // dispatches synthesized events so that the state of these keys matches the true state taking + // the current event in consideration. + // + // Although Android KeyEvent defined bitmasks for sided modifiers (SHIFT_LEFT_ON and + // SHIFT_RIGHT_ON), + // this function only uses the unsided modifiers (SHIFT_ON), due to the weird behaviors observed + // on ChromeOS, where right modifiers produce events with UNSIDED | LEFT_SIDE meta state bits. + void synchronizePressingKey( + PressingGoal goal, boolean truePressed, long eventLogicalKey, KeyEvent event) { + // During an incoming event, there might be a synthesized Flutter event for each key of each + // pressing goal, followed by an eventual main Flutter event. + // + // NowState ----------------> PreEventState --------------> TrueState + // Synchronization Event + // + // The goal of the synchronization algorithm is to derive a pre-event state that can satisfy the + // true state (`truePressed`) after the event, and that requires as few synthesized events based + // on the current state (`nowStates`) as possible. + final boolean[] nowStates = new boolean[goal.keys.length]; + final Boolean[] preEventStates = new Boolean[goal.keys.length]; + boolean postEventAnyPressed = false; + // 1. Find the current states of all keys. + // 2. Derive the pre-event state of the event key (if applicable.) + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + nowStates[keyIdx] = pressingRecords.containsKey(goal.keys[keyIdx].physicalKey); + if (goal.keys[keyIdx].logicalKey == eventLogicalKey) { + switch (getEventType(event)) { + case kDown: + preEventStates[keyIdx] = false; + postEventAnyPressed = true; + if (!truePressed) { + throw new AssertionError( + String.format( + "Unexpected metaState 0 for key 0x%x during an ACTION_down event.", + eventLogicalKey)); + } + break; + case kUp: + // Incoming event is an up. Although the previous state should be pressed, don't + // synthesize a down event even if it's not. The later code will handle such cases by + // skipping abrupt up events. Obviously don't synthesize up events either. + preEventStates[keyIdx] = nowStates[keyIdx]; + break; + case kRepeat: + // Incoming event is repeat. The previous state can be either pressed or released. Don't + // synthesize a down event here, or there will be a down event *and* a repeat event, + // both of which have printable characters. Obviously don't synthesize up events either. + if (!truePressed) { + throw new AssertionError( + String.format( + "Unexpected metaState 0 for key 0x%x during an ACTION_down repeat event.", + eventLogicalKey)); + } + preEventStates[keyIdx] = nowStates[keyIdx]; + postEventAnyPressed = true; + break; + } + } else { + postEventAnyPressed = postEventAnyPressed || nowStates[keyIdx]; + } + } + + // Fill the rest of the pre-event states to match the true state. + if (truePressed) { + // It is required that at least one key is pressed. + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (preEventStates[keyIdx] != null) { + continue; + } + if (postEventAnyPressed) { + preEventStates[keyIdx] = nowStates[keyIdx]; + } else { + preEventStates[keyIdx] = true; + postEventAnyPressed = true; + } + } + if (!postEventAnyPressed) { + preEventStates[0] = true; + } + } else { + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (preEventStates[keyIdx] != null) { + continue; + } + preEventStates[keyIdx] = false; + } + } + + // Dispatch synthesized events for state differences. + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (nowStates[keyIdx] != preEventStates[keyIdx]) { + final KeyboardMap.KeyPair key = goal.keys[keyIdx]; + synthesizeEvent( + preEventStates[keyIdx], key.logicalKey, key.physicalKey, event.getEventTime()); + } + } + } + + // Synchronize for a toggling modifier (such as CapsLock). + // + // A toggling modifier is defined by a `TogglingGoal`, which consists of a mask to get the true + // state out of `KeyEvent.getMetaState`, and a key. The synchronization process dispatches + // synthesized events so that the state of these keys matches the true state taking the current + // event in consideration. + // + // Although Android KeyEvent defined bitmasks for all "lock" modifiers and define them as the + // "lock" state, weird behaviors are observed on ChromeOS. First, ScrollLock and NumLock presses + // do not set metaState bits. Second, CapsLock key events set the CapsLock bit as if it is a + // pressing modifier (key down having state 1, key up having state 0), while other key events set + // the CapsLock bit correctly (locked having state 1, unlocked having state 0). Therefore this + // function only synchronizes the CapsLock state, and only does so during non-CapsLock key events. + void synchronizeTogglingKey( + TogglingGoal goal, boolean trueEnabled, long eventLogicalKey, KeyEvent event) { + if (goal.logicalKey == eventLogicalKey) { + // Don't synthesize for self events, because the self events have weird metaStates on + // ChromeOS. + return; + } + if (goal.enabled != trueEnabled) { + final boolean firstIsDown = !pressingRecords.containsKey(goal.physicalKey); + if (firstIsDown) { + goal.enabled = !goal.enabled; + } + synthesizeEvent(firstIsDown, goal.logicalKey, goal.physicalKey, event.getEventTime()); + if (!firstIsDown) { + goal.enabled = !goal.enabled; + } + synthesizeEvent(!firstIsDown, goal.logicalKey, goal.physicalKey, event.getEventTime()); + } + } + + // Implements the core algorithm of `handleEvent`. + // + // Returns whether any events are dispatched. + private boolean handleEventImpl( + @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + // Events with no codes at all can not be recognized. + if (event.getScanCode() == 0 && event.getKeyCode() == 0) { + return false; + } + final Long physicalKey = getPhysicalKey(event); + final Long logicalKey = getLogicalKey(event); + + for (final PressingGoal goal : KeyboardMap.pressingGoals) { + synchronizePressingKey(goal, (event.getMetaState() & goal.mask) != 0, logicalKey, event); + } + + for (final TogglingGoal goal : togglingGoals.values()) { + synchronizeTogglingKey(goal, (event.getMetaState() & goal.mask) != 0, logicalKey, event); + } + + boolean isDownEvent; + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + isDownEvent = true; + break; + case KeyEvent.ACTION_UP: + isDownEvent = false; + break; + default: + return false; + } + + KeyData.Type type; + String character = null; + final Long lastLogicalRecord = pressingRecords.get(physicalKey); + if (isDownEvent) { + if (lastLogicalRecord == null) { + type = KeyData.Type.kDown; + } else { + // A key has been pressed that has the exact physical key as a currently + // pressed one. + if (event.getRepeatCount() > 0) { + type = KeyData.Type.kRepeat; + } else { + synthesizeEvent(false, lastLogicalRecord, physicalKey, event.getEventTime()); + type = KeyData.Type.kDown; + } + } + final char complexChar = + characterCombiner.applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); + if (complexChar != 0) { + character = "" + complexChar; + } + } else { // isDownEvent is false + if (lastLogicalRecord == null) { + // Ignore abrupt up events. + return false; + } else { + type = KeyData.Type.kUp; + } + } + + if (type != KeyData.Type.kRepeat) { + updatePressingState(physicalKey, isDownEvent ? logicalKey : null); + } + if (type == KeyData.Type.kDown) { + final TogglingGoal maybeTogglingGoal = togglingGoals.get(logicalKey); + if (maybeTogglingGoal != null) { + maybeTogglingGoal.enabled = !maybeTogglingGoal.enabled; + } + } + + final KeyData output = new KeyData(); + output.timestamp = event.getEventTime(); + output.type = type; + output.logicalKey = logicalKey; + output.physicalKey = physicalKey; + output.character = character; + output.synthesized = false; + + sendKeyEvent(output, onKeyEventHandledCallback); + return true; + } + + private void synthesizeEvent(boolean isDown, Long logicalKey, Long physicalKey, long timestamp) { + final KeyData output = new KeyData(); + output.timestamp = timestamp; + output.type = isDown ? KeyData.Type.kDown : KeyData.Type.kUp; + output.logicalKey = logicalKey; + output.physicalKey = physicalKey; + output.character = null; + output.synthesized = true; + if (physicalKey != 0 && logicalKey != 0) { + updatePressingState(physicalKey, isDown ? logicalKey : null); + } + sendKeyEvent(output, null); + } + + private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHandledCallback) { + final BinaryMessenger.BinaryReply handleMessageReply = + onKeyEventHandledCallback == null + ? null + : message -> { + Boolean handled = false; + message.rewind(); + if (message.capacity() != 0) { + handled = message.get() != 0; + } + onKeyEventHandledCallback.onKeyEventHandled(handled); + }; + + messenger.send(KeyData.CHANNEL, data.toBytes(), handleMessageReply); + } + + /** + * Parses an Android key event, performs synchronization, and dispatches Flutter events through + * the messenger to the framework with the given callback. + * + *

At least one event will be dispatched. If there are no others, an empty event with 0 + * physical key and 0 logical key will be synthesized. + * + * @param event The Android key event to be handled. + * @param onKeyEventHandledCallback the method to call when the framework has decided whether to + * handle this event. This callback will always be called once and only once. If there are no + * non-synthesized out of this event, this callback will be called during this method with + * true. + */ + @Override + public void handleEvent( + @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + final boolean sentAny = handleEventImpl(event, onKeyEventHandledCallback); + if (!sentAny) { + synthesizeEvent(true, 0L, 0L, 0L); + onKeyEventHandledCallback.onKeyEventHandled(true); + } + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 38bbb7c985d8f..76e344a4e5d26 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -4,6 +4,7 @@ package io.flutter.embedding.android; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.Log; @@ -42,6 +43,69 @@ public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate { private static final String TAG = "KeyboardManager"; + /** + * Applies the given Unicode character from {@link KeyEvent#getUnicodeChar()} to a previously + * entered Unicode combining character and returns the combination of these characters if a + * combination exists. + * + *

This class is not used by {@link KeyboardManager}, but by its responders. + */ + public static class CharacterCombiner { + private int combiningCharacter = 0; + + public CharacterCombiner() {} + + /** + * This method mutates {@link #combiningCharacter} over time to combine characters. + * + *

One of the following things happens in this method: + * + *

    + *
  • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is not a combining character, then {@code newCharacterCodePoint} is returned. + *
  • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is a combining character, then {@code newCharacterCodePoint} is saved as the {@link + * #combiningCharacter} and null is returned. + *
  • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is also a combining character, then the {@code newCharacterCodePoint} is combined with + * the existing {@link #combiningCharacter} and null is returned. + *
  • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is not a combining character, then the {@link #combiningCharacter} is applied to the + * regular {@code newCharacterCodePoint} and the resulting complex character is returned. + * The {@link #combiningCharacter} is cleared. + *
+ * + *

The following reference explains the concept of a "combining character": + * https://en.wikipedia.org/wiki/Combining_character + */ + Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { + char complexCharacter = (char) newCharacterCodePoint; + boolean isNewCodePointACombiningCharacter = + (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; + if (isNewCodePointACombiningCharacter) { + // If a combining character was entered before, combine this one with that one. + int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK; + if (combiningCharacter != 0) { + combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint); + } else { + combiningCharacter = plainCodePoint; + } + } else { + // The new character is a regular character. Apply combiningCharacter to it, if + // it exists. + if (combiningCharacter != 0) { + int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint); + if (combinedChar > 0) { + complexCharacter = (char) combinedChar; + } + combiningCharacter = 0; + } + } + + return complexCharacter; + } + } + /** * Construct a {@link KeyboardManager}. * @@ -51,7 +115,8 @@ public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate public KeyboardManager(@NonNull ViewDelegate viewDelegate) { this.viewDelegate = viewDelegate; this.responders = - new KeyChannelResponder[] { + new Responder[] { + new KeyEmbedderResponder(viewDelegate.getBinaryMessenger()), new KeyChannelResponder(new KeyEventChannel(viewDelegate.getBinaryMessenger())), }; } diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java new file mode 100644 index 0000000000000..b0efe1d5e0ab5 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -0,0 +1,619 @@ +package io.flutter.embedding.android; + +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// This file is generated by flutter/flutter@dev/tools/gen_keycodes/bin/gen_keycodes.dart and +// should not be edited directly. +// +// Edit the template dev/tools/gen_keycodes/data/android_keyboard_map_java.tmpl instead. +// See dev/tools/gen_keycodes/README.md for more information. + +import android.view.KeyEvent; +import java.util.HashMap; + +/** Static information used by {@link KeyEmbedderResponder}. */ +public class KeyboardMap { + /** A physicalKey-logicalKey pair used to define mappings. */ + public static class KeyPair { + public KeyPair(long physicalKey, long logicalKey) { + this.physicalKey = physicalKey; + this.logicalKey = logicalKey; + } + + public long physicalKey; + public long logicalKey; + } + + /** + * An immutable configuration item that defines how to synchronize pressing modifiers (such as + * Shift or Ctrl), so that the {@link KeyEmbedderResponder} must synthesize events until the + * combined pressing state of {@link keys} matches the true meta state masked by {@link mask}. + */ + public static class PressingGoal { + public PressingGoal(int mask, KeyPair[] keys) { + this.mask = mask; + this.keys = keys; + } + + public final int mask; + public final KeyPair[] keys; + } + + /** + * A configuration item that defines how to synchronize toggling modifiers (such as CapsLock), so + * that the {@link KeyEmbedderResponder} must synthesize events until the enabling state of the + * key matches the true meta state masked by {@link #mask}. + * + *

The objects of this class are mutable. The {@link #enabled} field will be used to store the + * current enabling state. + */ + public static class TogglingGoal { + public TogglingGoal(int mask, long physicalKey, long logicalKey) { + this.mask = mask; + this.physicalKey = physicalKey; + this.logicalKey = logicalKey; + } + + public final int mask; + public final long physicalKey; + public final long logicalKey; + /** + * Used by {@link KeyEmbedderResponder} to store the current enabling state of this modifier. + * + *

Initialized as false. + */ + public boolean enabled = false; + } + + /** Maps from Android scan codes {@link KeyEvent#getScanCode()} to Flutter physical keys. */ + public static final HashMap scanCodeToPhysical = + new HashMap() { + private static final long serialVersionUID = 1L; + + { + put(0x00000001d0L, 0x0000000012L); // fn + put(0x00000000cdL, 0x0000000014L); // suspend + put(0x000000008eL, 0x0000010082L); // sleep + put(0x000000008fL, 0x0000010083L); // wakeUp + put(0x0000000100L, 0x000005ff01L); // gameButton1 + put(0x0000000120L, 0x000005ff01L); // gameButton1 + put(0x0000000101L, 0x000005ff02L); // gameButton2 + put(0x0000000121L, 0x000005ff02L); // gameButton2 + put(0x0000000102L, 0x000005ff03L); // gameButton3 + put(0x0000000122L, 0x000005ff03L); // gameButton3 + put(0x0000000103L, 0x000005ff04L); // gameButton4 + put(0x0000000123L, 0x000005ff04L); // gameButton4 + put(0x0000000104L, 0x000005ff05L); // gameButton5 + put(0x0000000124L, 0x000005ff05L); // gameButton5 + put(0x0000000105L, 0x000005ff06L); // gameButton6 + put(0x0000000125L, 0x000005ff06L); // gameButton6 + put(0x0000000106L, 0x000005ff07L); // gameButton7 + put(0x0000000126L, 0x000005ff07L); // gameButton7 + put(0x0000000107L, 0x000005ff08L); // gameButton8 + put(0x0000000127L, 0x000005ff08L); // gameButton8 + put(0x0000000108L, 0x000005ff09L); // gameButton9 + put(0x0000000128L, 0x000005ff09L); // gameButton9 + put(0x0000000109L, 0x000005ff0aL); // gameButton10 + put(0x0000000129L, 0x000005ff0aL); // gameButton10 + put(0x000000010aL, 0x000005ff0bL); // gameButton11 + put(0x000000012aL, 0x000005ff0bL); // gameButton11 + put(0x000000010bL, 0x000005ff0cL); // gameButton12 + put(0x000000012bL, 0x000005ff0cL); // gameButton12 + put(0x000000010cL, 0x000005ff0dL); // gameButton13 + put(0x000000012cL, 0x000005ff0dL); // gameButton13 + put(0x000000010dL, 0x000005ff0eL); // gameButton14 + put(0x000000012dL, 0x000005ff0eL); // gameButton14 + put(0x000000010eL, 0x000005ff0fL); // gameButton15 + put(0x000000012eL, 0x000005ff0fL); // gameButton15 + put(0x000000010fL, 0x000005ff10L); // gameButton16 + put(0x000000012fL, 0x000005ff10L); // gameButton16 + put(0x0000000130L, 0x000005ff11L); // gameButtonA + put(0x0000000131L, 0x000005ff12L); // gameButtonB + put(0x0000000132L, 0x000005ff13L); // gameButtonC + put(0x0000000136L, 0x000005ff14L); // gameButtonLeft1 + put(0x0000000138L, 0x000005ff15L); // gameButtonLeft2 + put(0x000000013cL, 0x000005ff16L); // gameButtonMode + put(0x0000000137L, 0x000005ff17L); // gameButtonRight1 + put(0x0000000139L, 0x000005ff18L); // gameButtonRight2 + put(0x000000013aL, 0x000005ff19L); // gameButtonSelect + put(0x000000013bL, 0x000005ff1aL); // gameButtonStart + put(0x000000013dL, 0x000005ff1bL); // gameButtonThumbLeft + put(0x000000013eL, 0x000005ff1cL); // gameButtonThumbRight + put(0x0000000133L, 0x000005ff1dL); // gameButtonX + put(0x0000000134L, 0x000005ff1eL); // gameButtonY + put(0x0000000135L, 0x000005ff1fL); // gameButtonZ + put(0x000000001eL, 0x0000070004L); // keyA + put(0x0000000030L, 0x0000070005L); // keyB + put(0x000000002eL, 0x0000070006L); // keyC + put(0x0000000020L, 0x0000070007L); // keyD + put(0x0000000012L, 0x0000070008L); // keyE + put(0x0000000021L, 0x0000070009L); // keyF + put(0x0000000022L, 0x000007000aL); // keyG + put(0x0000000023L, 0x000007000bL); // keyH + put(0x0000000017L, 0x000007000cL); // keyI + put(0x0000000024L, 0x000007000dL); // keyJ + put(0x0000000025L, 0x000007000eL); // keyK + put(0x0000000026L, 0x000007000fL); // keyL + put(0x0000000032L, 0x0000070010L); // keyM + put(0x0000000031L, 0x0000070011L); // keyN + put(0x0000000018L, 0x0000070012L); // keyO + put(0x0000000019L, 0x0000070013L); // keyP + put(0x0000000010L, 0x0000070014L); // keyQ + put(0x0000000013L, 0x0000070015L); // keyR + put(0x000000001fL, 0x0000070016L); // keyS + put(0x0000000014L, 0x0000070017L); // keyT + put(0x0000000016L, 0x0000070018L); // keyU + put(0x000000002fL, 0x0000070019L); // keyV + put(0x0000000011L, 0x000007001aL); // keyW + put(0x000000002dL, 0x000007001bL); // keyX + put(0x0000000015L, 0x000007001cL); // keyY + put(0x000000002cL, 0x000007001dL); // keyZ + put(0x0000000002L, 0x000007001eL); // digit1 + put(0x0000000003L, 0x000007001fL); // digit2 + put(0x0000000004L, 0x0000070020L); // digit3 + put(0x0000000005L, 0x0000070021L); // digit4 + put(0x0000000006L, 0x0000070022L); // digit5 + put(0x0000000007L, 0x0000070023L); // digit6 + put(0x0000000008L, 0x0000070024L); // digit7 + put(0x0000000009L, 0x0000070025L); // digit8 + put(0x000000000aL, 0x0000070026L); // digit9 + put(0x000000000bL, 0x0000070027L); // digit0 + put(0x000000001cL, 0x0000070028L); // enter + put(0x0000000001L, 0x0000070029L); // escape + put(0x000000000eL, 0x000007002aL); // backspace + put(0x000000000fL, 0x000007002bL); // tab + put(0x0000000039L, 0x000007002cL); // space + put(0x000000000cL, 0x000007002dL); // minus + put(0x000000000dL, 0x000007002eL); // equal + put(0x000000001aL, 0x000007002fL); // bracketLeft + put(0x000000001bL, 0x0000070030L); // bracketRight + put(0x000000002bL, 0x0000070031L); // backslash + put(0x0000000056L, 0x0000070031L); // backslash + put(0x0000000027L, 0x0000070033L); // semicolon + put(0x0000000028L, 0x0000070034L); // quote + put(0x0000000029L, 0x0000070035L); // backquote + put(0x0000000033L, 0x0000070036L); // comma + put(0x0000000034L, 0x0000070037L); // period + put(0x0000000035L, 0x0000070038L); // slash + put(0x000000003aL, 0x0000070039L); // capsLock + put(0x000000003bL, 0x000007003aL); // f1 + put(0x000000003cL, 0x000007003bL); // f2 + put(0x000000003dL, 0x000007003cL); // f3 + put(0x000000003eL, 0x000007003dL); // f4 + put(0x000000003fL, 0x000007003eL); // f5 + put(0x0000000040L, 0x000007003fL); // f6 + put(0x0000000041L, 0x0000070040L); // f7 + put(0x0000000042L, 0x0000070041L); // f8 + put(0x0000000043L, 0x0000070042L); // f9 + put(0x0000000044L, 0x0000070043L); // f10 + put(0x0000000057L, 0x0000070044L); // f11 + put(0x0000000058L, 0x0000070045L); // f12 + put(0x0000000063L, 0x0000070046L); // printScreen + put(0x0000000046L, 0x0000070047L); // scrollLock + put(0x0000000077L, 0x0000070048L); // pause + put(0x000000019bL, 0x0000070048L); // pause + put(0x000000006eL, 0x0000070049L); // insert + put(0x0000000066L, 0x000007004aL); // home + put(0x0000000068L, 0x000007004bL); // pageUp + put(0x00000000b1L, 0x000007004bL); // pageUp + put(0x000000006fL, 0x000007004cL); // delete + put(0x000000006bL, 0x000007004dL); // end + put(0x000000006dL, 0x000007004eL); // pageDown + put(0x00000000b2L, 0x000007004eL); // pageDown + put(0x000000006aL, 0x000007004fL); // arrowRight + put(0x0000000069L, 0x0000070050L); // arrowLeft + put(0x000000006cL, 0x0000070051L); // arrowDown + put(0x0000000067L, 0x0000070052L); // arrowUp + put(0x0000000045L, 0x0000070053L); // numLock + put(0x0000000062L, 0x0000070054L); // numpadDivide + put(0x0000000037L, 0x0000070055L); // numpadMultiply + put(0x000000004aL, 0x0000070056L); // numpadSubtract + put(0x000000004eL, 0x0000070057L); // numpadAdd + put(0x0000000060L, 0x0000070058L); // numpadEnter + put(0x000000004fL, 0x0000070059L); // numpad1 + put(0x0000000050L, 0x000007005aL); // numpad2 + put(0x0000000051L, 0x000007005bL); // numpad3 + put(0x000000004bL, 0x000007005cL); // numpad4 + put(0x000000004cL, 0x000007005dL); // numpad5 + put(0x000000004dL, 0x000007005eL); // numpad6 + put(0x0000000047L, 0x000007005fL); // numpad7 + put(0x0000000048L, 0x0000070060L); // numpad8 + put(0x0000000049L, 0x0000070061L); // numpad9 + put(0x0000000052L, 0x0000070062L); // numpad0 + put(0x0000000053L, 0x0000070063L); // numpadDecimal + put(0x000000007fL, 0x0000070065L); // contextMenu + put(0x000000008bL, 0x0000070065L); // contextMenu + put(0x0000000074L, 0x0000070066L); // power + put(0x0000000098L, 0x0000070066L); // power + put(0x0000000075L, 0x0000070067L); // numpadEqual + put(0x00000000b7L, 0x0000070068L); // f13 + put(0x00000000b8L, 0x0000070069L); // f14 + put(0x00000000b9L, 0x000007006aL); // f15 + put(0x00000000baL, 0x000007006bL); // f16 + put(0x00000000bbL, 0x000007006cL); // f17 + put(0x00000000bcL, 0x000007006dL); // f18 + put(0x00000000bdL, 0x000007006eL); // f19 + put(0x00000000beL, 0x000007006fL); // f20 + put(0x00000000bfL, 0x0000070070L); // f21 + put(0x00000000c0L, 0x0000070071L); // f22 + put(0x00000000c1L, 0x0000070072L); // f23 + put(0x00000000c2L, 0x0000070073L); // f24 + put(0x0000000086L, 0x0000070074L); // open + put(0x000000008aL, 0x0000070075L); // help + put(0x0000000161L, 0x0000070077L); // select + put(0x0000000081L, 0x0000070079L); // again + put(0x0000000083L, 0x000007007aL); // undo + put(0x0000000089L, 0x000007007bL); // cut + put(0x0000000085L, 0x000007007cL); // copy + put(0x0000000087L, 0x000007007dL); // paste + put(0x0000000088L, 0x000007007eL); // find + put(0x0000000071L, 0x000007007fL); // audioVolumeMute + put(0x0000000073L, 0x0000070080L); // audioVolumeUp + put(0x0000000072L, 0x0000070081L); // audioVolumeDown + put(0x000000005fL, 0x0000070085L); // numpadComma + put(0x0000000079L, 0x0000070085L); // numpadComma + put(0x0000000059L, 0x0000070087L); // intlRo + put(0x000000007cL, 0x0000070089L); // intlYen + put(0x000000005cL, 0x000007008aL); // convert + put(0x000000005eL, 0x000007008bL); // nonConvert + put(0x000000005aL, 0x0000070092L); // lang3 + put(0x000000005bL, 0x0000070093L); // lang4 + put(0x0000000082L, 0x00000700a3L); // props + put(0x00000000b3L, 0x00000700b6L); // numpadParenLeft + put(0x00000000b4L, 0x00000700b7L); // numpadParenRight + put(0x000000001dL, 0x00000700e0L); // controlLeft + put(0x000000002aL, 0x00000700e1L); // shiftLeft + put(0x0000000038L, 0x00000700e2L); // altLeft + put(0x000000007dL, 0x00000700e3L); // metaLeft + put(0x0000000061L, 0x00000700e4L); // controlRight + put(0x0000000036L, 0x00000700e5L); // shiftRight + put(0x0000000064L, 0x00000700e6L); // altRight + put(0x000000007eL, 0x00000700e7L); // metaRight + put(0x0000000166L, 0x00000c0060L); // info + put(0x0000000172L, 0x00000c0061L); // closedCaptionToggle + put(0x00000000e1L, 0x00000c006fL); // brightnessUp + put(0x00000000e0L, 0x00000c0070L); // brightnessDown + put(0x0000000195L, 0x00000c0083L); // mediaLast + put(0x00000000aeL, 0x00000c0094L); // exit + put(0x0000000192L, 0x00000c009cL); // channelUp + put(0x0000000193L, 0x00000c009dL); // channelDown + put(0x00000000c8L, 0x00000c00b0L); // mediaPlay + put(0x00000000cfL, 0x00000c00b0L); // mediaPlay + put(0x00000000c9L, 0x00000c00b1L); // mediaPause + put(0x00000000a7L, 0x00000c00b2L); // mediaRecord + put(0x00000000d0L, 0x00000c00b3L); // mediaFastForward + put(0x00000000a8L, 0x00000c00b4L); // mediaRewind + put(0x00000000a3L, 0x00000c00b5L); // mediaTrackNext + put(0x00000000a5L, 0x00000c00b6L); // mediaTrackPrevious + put(0x0000000080L, 0x00000c00b7L); // mediaStop + put(0x00000000a6L, 0x00000c00b7L); // mediaStop + put(0x00000000a1L, 0x00000c00b8L); // eject + put(0x00000000a2L, 0x00000c00b8L); // eject + put(0x00000000a4L, 0x00000c00cdL); // mediaPlayPause + put(0x00000000d1L, 0x00000c00e5L); // bassBoost + put(0x000000009bL, 0x00000c018aL); // launchMail + put(0x00000000d7L, 0x00000c018aL); // launchMail + put(0x00000001adL, 0x00000c018dL); // launchContacts + put(0x000000018dL, 0x00000c018eL); // launchCalendar + put(0x0000000247L, 0x00000c01cbL); // launchAssistant + put(0x00000000a0L, 0x00000c0203L); // close + put(0x00000000ceL, 0x00000c0203L); // close + put(0x00000000d2L, 0x00000c0208L); // print + put(0x00000000d9L, 0x00000c0221L); // browserSearch + put(0x000000009fL, 0x00000c0225L); // browserForward + put(0x000000009cL, 0x00000c022aL); // browserFavorites + put(0x00000000b6L, 0x00000c0279L); // redo + } + }; + + /** Maps from Android key codes {@link KeyEvent#getKeyCode()} to Flutter logical keys. */ + public static final HashMap keyCodeToLogical = + new HashMap() { + private static final long serialVersionUID = 1L; + + { + put(0x000000003eL, 0x0000000020L); // space + put(0x000000004bL, 0x0000000022L); // quote + put(0x0000000012L, 0x0000000023L); // numberSign + put(0x0000000011L, 0x000000002aL); // asterisk + put(0x0000000051L, 0x000000002bL); // add + put(0x0000000037L, 0x000000002cL); // comma + put(0x0000000045L, 0x000000002dL); // minus + put(0x0000000038L, 0x000000002eL); // period + put(0x000000004cL, 0x000000002fL); // slash + put(0x0000000007L, 0x0000000030L); // digit0 + put(0x0000000008L, 0x0000000031L); // digit1 + put(0x0000000009L, 0x0000000032L); // digit2 + put(0x000000000aL, 0x0000000033L); // digit3 + put(0x000000000bL, 0x0000000034L); // digit4 + put(0x000000000cL, 0x0000000035L); // digit5 + put(0x000000000dL, 0x0000000036L); // digit6 + put(0x000000000eL, 0x0000000037L); // digit7 + put(0x000000000fL, 0x0000000038L); // digit8 + put(0x0000000010L, 0x0000000039L); // digit9 + put(0x000000004aL, 0x000000003bL); // semicolon + put(0x0000000046L, 0x000000003dL); // equal + put(0x000000004dL, 0x0000000040L); // at + put(0x0000000047L, 0x000000005bL); // bracketLeft + put(0x0000000049L, 0x000000005cL); // backslash + put(0x0000000048L, 0x000000005dL); // bracketRight + put(0x0000000044L, 0x0000000060L); // backquote + put(0x000000001dL, 0x0000000061L); // keyA + put(0x000000001eL, 0x0000000062L); // keyB + put(0x000000001fL, 0x0000000063L); // keyC + put(0x0000000020L, 0x0000000064L); // keyD + put(0x0000000021L, 0x0000000065L); // keyE + put(0x0000000022L, 0x0000000066L); // keyF + put(0x0000000023L, 0x0000000067L); // keyG + put(0x0000000024L, 0x0000000068L); // keyH + put(0x0000000025L, 0x0000000069L); // keyI + put(0x0000000026L, 0x000000006aL); // keyJ + put(0x0000000027L, 0x000000006bL); // keyK + put(0x0000000028L, 0x000000006cL); // keyL + put(0x0000000029L, 0x000000006dL); // keyM + put(0x000000002aL, 0x000000006eL); // keyN + put(0x000000002bL, 0x000000006fL); // keyO + put(0x000000002cL, 0x0000000070L); // keyP + put(0x000000002dL, 0x0000000071L); // keyQ + put(0x000000002eL, 0x0000000072L); // keyR + put(0x000000002fL, 0x0000000073L); // keyS + put(0x0000000030L, 0x0000000074L); // keyT + put(0x0000000031L, 0x0000000075L); // keyU + put(0x0000000032L, 0x0000000076L); // keyV + put(0x0000000033L, 0x0000000077L); // keyW + put(0x0000000034L, 0x0000000078L); // keyX + put(0x0000000035L, 0x0000000079L); // keyY + put(0x0000000036L, 0x000000007aL); // keyZ + put(0x0000000043L, 0x0100000008L); // backspace + put(0x000000003dL, 0x0100000009L); // tab + put(0x0000000042L, 0x010000000dL); // enter + put(0x000000006fL, 0x010000001bL); // escape + put(0x0000000070L, 0x010000007fL); // delete + put(0x0000000073L, 0x0100000104L); // capsLock + put(0x0000000077L, 0x0100000106L); // fn + put(0x000000008fL, 0x010000010aL); // numLock + put(0x0000000074L, 0x010000010cL); // scrollLock + put(0x000000003fL, 0x010000010fL); // symbol + put(0x0000000014L, 0x0100000301L); // arrowDown + put(0x0000000015L, 0x0100000302L); // arrowLeft + put(0x0000000016L, 0x0100000303L); // arrowRight + put(0x0000000013L, 0x0100000304L); // arrowUp + put(0x000000007bL, 0x0100000305L); // end + put(0x000000007aL, 0x0100000306L); // home + put(0x000000005dL, 0x0100000307L); // pageDown + put(0x000000005cL, 0x0100000308L); // pageUp + put(0x000000001cL, 0x0100000401L); // clear + put(0x0000000116L, 0x0100000402L); // copy + put(0x0000000115L, 0x0100000404L); // cut + put(0x000000007cL, 0x0100000407L); // insert + put(0x0000000117L, 0x0100000408L); // paste + put(0x0000000052L, 0x0100000505L); // contextMenu + put(0x0000000103L, 0x0100000508L); // help + put(0x0000000079L, 0x0100000509L); // pause + put(0x0000000017L, 0x010000050cL); // select + put(0x00000000a8L, 0x010000050dL); // zoomIn + put(0x00000000a9L, 0x010000050eL); // zoomOut + put(0x00000000dcL, 0x0100000601L); // brightnessDown + put(0x00000000ddL, 0x0100000602L); // brightnessUp + put(0x000000001bL, 0x0100000603L); // camera + put(0x0000000081L, 0x0100000604L); // eject + put(0x000000001aL, 0x0100000606L); // power + put(0x0000000078L, 0x0100000608L); // printScreen + put(0x00000000e0L, 0x010000060bL); // wakeUp + put(0x00000000d6L, 0x0100000705L); // convert + put(0x00000000ccL, 0x0100000709L); // groupNext + put(0x000000005fL, 0x010000070bL); // modeChange + put(0x00000000d5L, 0x010000070dL); // nonConvert + put(0x00000000d4L, 0x0100000714L); // eisu + put(0x00000000d7L, 0x0100000717L); // hiraganaKatakana + put(0x00000000daL, 0x0100000719L); // kanjiMode + put(0x00000000d3L, 0x010000071dL); // zenkakuHankaku + put(0x0000000083L, 0x0100000801L); // f1 + put(0x0000000084L, 0x0100000802L); // f2 + put(0x0000000085L, 0x0100000803L); // f3 + put(0x0000000086L, 0x0100000804L); // f4 + put(0x0000000087L, 0x0100000805L); // f5 + put(0x0000000088L, 0x0100000806L); // f6 + put(0x0000000089L, 0x0100000807L); // f7 + put(0x000000008aL, 0x0100000808L); // f8 + put(0x000000008bL, 0x0100000809L); // f9 + put(0x000000008cL, 0x010000080aL); // f10 + put(0x000000008dL, 0x010000080bL); // f11 + put(0x000000008eL, 0x010000080cL); // f12 + put(0x0000000080L, 0x0100000a01L); // close + put(0x0000000055L, 0x0100000a05L); // mediaPlayPause + put(0x0000000056L, 0x0100000a07L); // mediaStop + put(0x0000000057L, 0x0100000a08L); // mediaTrackNext + put(0x0000000058L, 0x0100000a09L); // mediaTrackPrevious + put(0x0000000019L, 0x0100000a0fL); // audioVolumeDown + put(0x0000000018L, 0x0100000a10L); // audioVolumeUp + put(0x00000000a4L, 0x0100000a11L); // audioVolumeMute + put(0x00000000d0L, 0x0100000b02L); // launchCalendar + put(0x0000000041L, 0x0100000b03L); // launchMail + put(0x00000000d1L, 0x0100000b05L); // launchMusicPlayer + put(0x0000000040L, 0x0100000b09L); // launchWebBrowser + put(0x00000000cfL, 0x0100000b0cL); // launchContacts + put(0x00000000dbL, 0x0100000b0eL); // launchAssistant + put(0x00000000aeL, 0x0100000c02L); // browserFavorites + put(0x000000007dL, 0x0100000c03L); // browserForward + put(0x0000000054L, 0x0100000c06L); // browserSearch + put(0x00000000b6L, 0x0100000d08L); // avrInput + put(0x00000000b5L, 0x0100000d09L); // avrPower + put(0x00000000a7L, 0x0100000d0aL); // channelDown + put(0x00000000a6L, 0x0100000d0bL); // channelUp + put(0x00000000b7L, 0x0100000d0cL); // colorF0Red + put(0x00000000b8L, 0x0100000d0dL); // colorF1Green + put(0x00000000b9L, 0x0100000d0eL); // colorF2Yellow + put(0x00000000baL, 0x0100000d0fL); // colorF3Blue + put(0x00000000afL, 0x0100000d12L); // closedCaptionToggle + put(0x00000000acL, 0x0100000d22L); // guide + put(0x00000000a5L, 0x0100000d25L); // info + put(0x000000005aL, 0x0100000d2cL); // mediaFastForward + put(0x00000000e5L, 0x0100000d2dL); // mediaLast + put(0x000000007fL, 0x0100000d2eL); // mediaPause + put(0x000000007eL, 0x0100000d2fL); // mediaPlay + put(0x0000000082L, 0x0100000d30L); // mediaRecord + put(0x0000000059L, 0x0100000d31L); // mediaRewind + put(0x00000000b0L, 0x0100000d43L); // settings + put(0x00000000b4L, 0x0100000d45L); // stbInput + put(0x00000000b3L, 0x0100000d46L); // stbPower + put(0x00000000e9L, 0x0100000d48L); // teletext + put(0x00000000aaL, 0x0100000d49L); // tv + put(0x00000000b2L, 0x0100000d4aL); // tvInput + put(0x00000000b1L, 0x0100000d4bL); // tvPower + put(0x00000000ffL, 0x0100000d4eL); // zoomToggle + put(0x00000000adL, 0x0100000d4fL); // dvr + put(0x00000000deL, 0x0100000d50L); // mediaAudioTrack + put(0x0000000111L, 0x0100000d51L); // mediaSkipBackward + put(0x0000000110L, 0x0100000d52L); // mediaSkipForward + put(0x0000000113L, 0x0100000d53L); // mediaStepBackward + put(0x0000000112L, 0x0100000d54L); // mediaStepForward + put(0x00000000e2L, 0x0100000d55L); // mediaTopMenu + put(0x0000000106L, 0x0100000d56L); // navigateIn + put(0x0000000105L, 0x0100000d57L); // navigateNext + put(0x0000000107L, 0x0100000d58L); // navigateOut + put(0x0000000104L, 0x0100000d59L); // navigatePrevious + put(0x00000000e1L, 0x0100000d5aL); // pairing + put(0x000000005bL, 0x0100000e09L); // microphoneVolumeMute + put(0x00000000bbL, 0x0100001001L); // appSwitch + put(0x0000000005L, 0x0100001002L); // call + put(0x0000000050L, 0x0100001003L); // cameraFocus + put(0x0000000006L, 0x0100001004L); // endCall + put(0x0000000004L, 0x0100001005L); // goBack + put(0x0000000003L, 0x0100001006L); // goHome + put(0x000000004fL, 0x0100001007L); // headsetHook + put(0x0000000053L, 0x0100001009L); // notification + put(0x00000000cdL, 0x010000100aL); // mannerMode + put(0x00000000ceL, 0x0100001101L); // tv3DMode + put(0x00000000f2L, 0x0100001102L); // tvAntennaCable + put(0x00000000fcL, 0x0100001103L); // tvAudioDescription + put(0x00000000feL, 0x0100001104L); // tvAudioDescriptionMixDown + put(0x00000000fdL, 0x0100001105L); // tvAudioDescriptionMixUp + put(0x0000000100L, 0x0100001106L); // tvContentsMenu + put(0x00000000e6L, 0x0100001107L); // tvDataService + put(0x00000000f9L, 0x0100001108L); // tvInputComponent1 + put(0x00000000faL, 0x0100001109L); // tvInputComponent2 + put(0x00000000f7L, 0x010000110aL); // tvInputComposite1 + put(0x00000000f8L, 0x010000110bL); // tvInputComposite2 + put(0x00000000f3L, 0x010000110cL); // tvInputHDMI1 + put(0x00000000f4L, 0x010000110dL); // tvInputHDMI2 + put(0x00000000f5L, 0x010000110eL); // tvInputHDMI3 + put(0x00000000f6L, 0x010000110fL); // tvInputHDMI4 + put(0x00000000fbL, 0x0100001110L); // tvInputVGA1 + put(0x00000000f1L, 0x0100001112L); // tvNetwork + put(0x00000000eaL, 0x0100001113L); // tvNumberEntry + put(0x00000000e8L, 0x0100001114L); // tvRadioService + put(0x00000000edL, 0x0100001115L); // tvSatellite + put(0x00000000eeL, 0x0100001116L); // tvSatelliteBS + put(0x00000000efL, 0x0100001117L); // tvSatelliteCS + put(0x00000000f0L, 0x0100001118L); // tvSatelliteToggle + put(0x00000000ebL, 0x0100001119L); // tvTerrestrialAnalog + put(0x00000000ecL, 0x010000111aL); // tvTerrestrialDigital + put(0x0000000102L, 0x010000111bL); // tvTimer + put(0x00000000dfL, 0x0200000002L); // sleep + put(0x00000000d9L, 0x0200000021L); // intlRo + put(0x00000000d8L, 0x0200000022L); // intlYen + put(0x0000000071L, 0x0200000100L); // controlLeft + put(0x0000000072L, 0x0200000101L); // controlRight + put(0x000000003bL, 0x0200000102L); // shiftLeft + put(0x000000003cL, 0x0200000103L); // shiftRight + put(0x0000000039L, 0x0200000104L); // altLeft + put(0x000000003aL, 0x0200000105L); // altRight + put(0x0000000075L, 0x0200000106L); // metaLeft + put(0x0000000076L, 0x0200000107L); // metaRight + put(0x00000000a0L, 0x020000020dL); // numpadEnter + put(0x00000000a2L, 0x0200000228L); // numpadParenLeft + put(0x00000000a3L, 0x0200000229L); // numpadParenRight + put(0x000000009bL, 0x020000022aL); // numpadMultiply + put(0x000000009dL, 0x020000022bL); // numpadAdd + put(0x000000009fL, 0x020000022cL); // numpadComma + put(0x000000009cL, 0x020000022dL); // numpadSubtract + put(0x000000009eL, 0x020000022eL); // numpadDecimal + put(0x000000009aL, 0x020000022fL); // numpadDivide + put(0x0000000090L, 0x0200000230L); // numpad0 + put(0x0000000091L, 0x0200000231L); // numpad1 + put(0x0000000092L, 0x0200000232L); // numpad2 + put(0x0000000093L, 0x0200000233L); // numpad3 + put(0x0000000094L, 0x0200000234L); // numpad4 + put(0x0000000095L, 0x0200000235L); // numpad5 + put(0x0000000096L, 0x0200000236L); // numpad6 + put(0x0000000097L, 0x0200000237L); // numpad7 + put(0x0000000098L, 0x0200000238L); // numpad8 + put(0x0000000099L, 0x0200000239L); // numpad9 + put(0x00000000a1L, 0x020000023dL); // numpadEqual + put(0x00000000bcL, 0x0200000301L); // gameButton1 + put(0x00000000bdL, 0x0200000302L); // gameButton2 + put(0x00000000beL, 0x0200000303L); // gameButton3 + put(0x00000000bfL, 0x0200000304L); // gameButton4 + put(0x00000000c0L, 0x0200000305L); // gameButton5 + put(0x00000000c1L, 0x0200000306L); // gameButton6 + put(0x00000000c2L, 0x0200000307L); // gameButton7 + put(0x00000000c3L, 0x0200000308L); // gameButton8 + put(0x00000000c4L, 0x0200000309L); // gameButton9 + put(0x00000000c5L, 0x020000030aL); // gameButton10 + put(0x00000000c6L, 0x020000030bL); // gameButton11 + put(0x00000000c7L, 0x020000030cL); // gameButton12 + put(0x00000000c8L, 0x020000030dL); // gameButton13 + put(0x00000000c9L, 0x020000030eL); // gameButton14 + put(0x00000000caL, 0x020000030fL); // gameButton15 + put(0x00000000cbL, 0x0200000310L); // gameButton16 + put(0x0000000060L, 0x0200000311L); // gameButtonA + put(0x0000000061L, 0x0200000312L); // gameButtonB + put(0x0000000062L, 0x0200000313L); // gameButtonC + put(0x0000000066L, 0x0200000314L); // gameButtonLeft1 + put(0x0000000068L, 0x0200000315L); // gameButtonLeft2 + put(0x000000006eL, 0x0200000316L); // gameButtonMode + put(0x0000000067L, 0x0200000317L); // gameButtonRight1 + put(0x0000000069L, 0x0200000318L); // gameButtonRight2 + put(0x000000006dL, 0x0200000319L); // gameButtonSelect + put(0x000000006cL, 0x020000031aL); // gameButtonStart + put(0x000000006aL, 0x020000031bL); // gameButtonThumbLeft + put(0x000000006bL, 0x020000031cL); // gameButtonThumbRight + put(0x0000000063L, 0x020000031dL); // gameButtonX + put(0x0000000064L, 0x020000031eL); // gameButtonY + put(0x0000000065L, 0x020000031fL); // gameButtonZ + } + }; + + public static final PressingGoal[] pressingGoals = + new PressingGoal[] { + new PressingGoal( + KeyEvent.META_CTRL_ON, + new KeyPair[] { + new KeyPair(0x000700e0L, 0x0200000100L), // ControlLeft + new KeyPair(0x000700e4L, 0x0200000101L), // ControlRight + }), + new PressingGoal( + KeyEvent.META_SHIFT_ON, + new KeyPair[] { + new KeyPair(0x000700e1L, 0x0200000102L), // ShiftLeft + new KeyPair(0x000700e5L, 0x0200000103L), // ShiftRight + }), + new PressingGoal( + KeyEvent.META_ALT_ON, + new KeyPair[] { + new KeyPair(0x000700e2L, 0x0200000104L), // AltLeft + new KeyPair(0x000700e6L, 0x0200000105L), // AltRight + }), + }; + + /** + * A list of toggling modifiers that must be synchronized on each key event. + * + *

The list is not a static variable but constructed by a function, because {@link + * TogglingGoal} is mutable. + */ + public static TogglingGoal[] getTogglingGoals() { + return new TogglingGoal[] { + new TogglingGoal(KeyEvent.META_CAPS_LOCK_ON, 0x00070039L, 0x0100000104L), + }; + } + + public static final long kValueMask = 0x000ffffffffL; + public static final long kUnicodePlane = 0x00000000000L; + public static final long kLogicalPlane = 0x00300000000L; + public static final long kAndroidPlane = 0x01100000000L; +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java index ed3c22acb1baf..80379a21343b2 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java @@ -5,7 +5,6 @@ import static org.mockito.Mockito.doAnswer; import android.annotation.TargetApi; -import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; @@ -23,8 +22,6 @@ @TargetApi(28) public class KeyChannelResponderTest { - private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; - @Mock KeyEventChannel keyEventChannel; KeyChannelResponder channelResponder; @@ -55,27 +52,4 @@ public void primaryResponderTest() { }); assertEquals(completionCallbackInvocationCounter[0], 1); } - - @Test - public void basicCombingCharactersTest() { - assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); - assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); - assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B')); - assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B')); - assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); - assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); - - assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); - - assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); - // The 0 input should remove the combining state. - assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); - - assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); - assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); - } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index b909ce707adaa..4a4b82dba80f8 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -1,14 +1,20 @@ package io.flutter.embedding.android; +import static android.view.KeyEvent.*; +import static io.flutter.embedding.android.KeyData.Type; +import static io.flutter.util.KeyCodes.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -18,8 +24,11 @@ import io.flutter.util.FakeKeyEvent; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; +import junit.framework.Assert; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; @@ -33,6 +42,25 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class KeyboardManagerTest { + public static final int SCAN_KEY_A = 0x1e; + public static final int SCAN_DIGIT1 = 0x2; + public static final int SCAN_SHIFT_LEFT = 0x2a; + public static final int SCAN_SHIFT_RIGHT = 0x36; + public static final int SCAN_CONTROL_LEFT = 0x1d; + public static final int SCAN_CONTROL_RIGHT = 0x61; + public static final int SCAN_ALT_LEFT = 0x38; + public static final int SCAN_ALT_RIGHT = 0x64; + public static final int SCAN_ARROW_LEFT = 0x69; + public static final int SCAN_ARROW_RIGHT = 0x6a; + public static final int SCAN_CAPS_LOCK = 0x3a; + + public static final boolean DOWN_EVENT = true; + public static final boolean UP_EVENT = false; + public static final boolean SHIFT_LEFT_EVENT = true; + public static final boolean SHIFT_RIGHT_EVENT = false; + + private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; + /** * Records a message that {@link KeyboardManager} sends to outside. * @@ -40,14 +68,20 @@ public class KeyboardManagerTest { * types will have different fields filled, leaving others empty. */ static class CallRecord { - static enum Type { + enum Kind { /** * The channel responder sent a message through the key event channel. * - *

This call record will have a non-null {@link channelData}, with an optional {@link + *

This call record will have a non-null {@link channelObject}, with an optional {@link * reply}. */ kChannel, + /** + * The embedder responder sent a message through the key data channel. + * + *

This call record will have a non-null {@link keyData}, with an optional {@link reply}. + */ + kEmbedder, } /** @@ -57,7 +91,7 @@ static enum Type { */ private CallRecord() {} - Type type; + Kind kind; /** * The callback given by the keyboard manager. @@ -67,34 +101,29 @@ private CallRecord() {} * continue processing the key event. */ public Consumer reply; - /** The data for a call record of type {@link Type.kChannel}. */ - public ChannelCallData channelData; + /** The data for a call record of kind {@link Kind.kChannel}. */ + public JSONObject channelObject; + /** The data for a call record of kind {@link Kind.kEmbedder}. */ + public KeyData keyData; - /** Construct a call record of type {@link Type.kChannel}. */ + /** Construct a call record of kind {@link Kind.kChannel}. */ static CallRecord channelCall( - @NonNull ChannelCallData channelData, @Nullable Consumer reply) { + @NonNull JSONObject channelObject, @Nullable Consumer reply) { final CallRecord record = new CallRecord(); - record.type = Type.kChannel; - record.channelData = channelData; + record.kind = Kind.kChannel; + record.channelObject = channelObject; record.reply = reply; return record; } - } - /** - * The data for a {@link CallRecord} of a channel message sent by the channel responder to the - * framework. - */ - static class ChannelCallData { - ChannelCallData(@NonNull String channel, @NonNull JSONObject message) { - this.channel = channel; - this.message = message; + /** Construct a call record of kind {@link Kind.kEmbedder}. */ + static CallRecord embedderCall(@NonNull KeyData keyData, @Nullable Consumer reply) { + final CallRecord record = new CallRecord(); + record.kind = Kind.kEmbedder; + record.keyData = keyData; + record.reply = reply; + return record; } - - /** The channel that the message is sent on. */ - public String channel; - /** The parsed JSON message object. */ - public JSONObject message; } /** @@ -102,7 +131,7 @@ static class ChannelCallData { * * @param handled whether the event is handled. */ - static ByteBuffer buildResponseMessage(boolean handled) { + static ByteBuffer buildJsonResponse(boolean handled) { JSONObject body = new JSONObject(); try { body.put("handled", handled); @@ -114,6 +143,19 @@ static ByteBuffer buildResponseMessage(boolean handled) { return binaryReply; } + /** + * Build a response to an embedder message sent by the embedder responder. + * + * @param handled whether the event is handled. + */ + static ByteBuffer buildBinaryResponse(boolean handled) { + byte[] body = new byte[1]; + body[0] = (byte) (handled ? 1 : 0); + final ByteBuffer binaryReply = ByteBuffer.wrap(body); + binaryReply.rewind(); + return binaryReply; + } + /** * Used to configure how to process a channel message. * @@ -122,18 +164,29 @@ static ByteBuffer buildResponseMessage(boolean handled) { * reply callback, which should be called to mock the reply from the framework. */ @FunctionalInterface - static interface ChannelCallHandler extends BiConsumer> {} + static interface ChannelCallHandler extends BiConsumer> {} + + /** + * Used to configure how to process an embedder message. + * + *

When the embedder responder sends a key data, this functional interface will be invoked. Its + * first argument will be the detailed data. The second argument will be a nullable reply + * callback, which should be called to mock the reply from the framework. + */ + @FunctionalInterface + static interface EmbedderCallHandler extends BiConsumer> {} static class KeyboardTester { public KeyboardTester() { respondToChannelCallsWith(false); + respondToEmbedderCallsWith(false); respondToTextInputWith(false); BinaryMessenger mockMessenger = mock(BinaryMessenger.class); - doAnswer(invocation -> onChannelMessage(invocation)) + doAnswer(invocation -> onMessengerMessage(invocation)) .when(mockMessenger) - .send(any(String.class), any(ByteBuffer.class)); - doAnswer(invocation -> onChannelMessage(invocation)) + .send(any(String.class), any(ByteBuffer.class), eq(null)); + doAnswer(invocation -> onMessengerMessage(invocation)) .when(mockMessenger) .send(any(String.class), any(ByteBuffer.class), any(BinaryMessenger.BinaryReply.class)); @@ -161,7 +214,7 @@ public KeyboardTester() { /** Set channel calls to respond immediately with the given response. */ public void respondToChannelCallsWith(boolean handled) { channelHandler = - (ChannelCallData data, Consumer reply) -> { + (JSONObject data, Consumer reply) -> { if (reply != null) { reply.accept(handled); } @@ -169,38 +222,68 @@ public void respondToChannelCallsWith(boolean handled) { } /** - * Record embedder calls to the given storage. + * Record channel calls to the given storage. * *

They are not responded to until the stored callbacks are manually called. */ public void recordChannelCallsTo(@NonNull ArrayList storage) { channelHandler = - (ChannelCallData data, Consumer reply) -> { + (JSONObject data, Consumer reply) -> { storage.add(CallRecord.channelCall(data, reply)); }; } + /** Set embedder calls to respond immediately with the given response. */ + public void respondToEmbedderCallsWith(boolean handled) { + embedderHandler = + (KeyData keyData, Consumer reply) -> { + if (reply != null) { + reply.accept(handled); + } + }; + } + + /** + * Record embedder calls to the given storage. + * + *

They are not responded to until the stored callbacks are manually called. + */ + public void recordEmbedderCallsTo(@NonNull ArrayList storage) { + embedderHandler = + (KeyData keyData, Consumer reply) -> + storage.add(CallRecord.embedderCall(keyData, reply)); + } + /** Set text calls to respond with the given response. */ public void respondToTextInputWith(boolean response) { textInputResult = response; } private ChannelCallHandler channelHandler; + private EmbedderCallHandler embedderHandler; private Boolean textInputResult; - private Object onChannelMessage(@NonNull InvocationOnMock invocation) { + private Object onMessengerMessage(@NonNull InvocationOnMock invocation) { final String channel = invocation.getArgument(0); final ByteBuffer buffer = invocation.getArgument(1); buffer.rewind(); - final JSONObject jsonObject = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(buffer); + final BinaryMessenger.BinaryReply reply = invocation.getArgument(2); - final Consumer jsonReply = - reply == null - ? null - : handled -> { - reply.reply(buildResponseMessage(handled)); - }; - channelHandler.accept(new ChannelCallData(channel, jsonObject), jsonReply); + if (channel == "flutter/keyevent") { + // Parse a channel call. + final JSONObject jsonObject = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(buffer); + final Consumer jsonReply = + reply == null ? null : handled -> reply.reply(buildJsonResponse(handled)); + channelHandler.accept(jsonObject, jsonReply); + } else if (channel == "flutter/keydata") { + // Parse an embedder call. + final KeyData keyData = new KeyData(buffer); + final Consumer booleanReply = + reply == null ? null : handled -> reply.reply(buildBinaryResponse(handled)); + embedderHandler.accept(keyData, booleanReply); + } else { + assertTrue(false); + } return null; } } @@ -215,9 +298,7 @@ private Object onChannelMessage(@NonNull InvocationOnMock invocation) { * @param keyCode the key code. */ static void assertChannelEventEquals( - @NonNull ChannelCallData data, @NonNull String type, @NonNull Integer keyCode) { - final JSONObject message = data.message; - assertEquals(data.channel, "flutter/keyevent"); + @NonNull JSONObject message, @NonNull String type, @NonNull Integer keyCode) { try { assertEquals(type, message.get("type")); assertEquals("android", message.get("keymap")); @@ -227,6 +308,120 @@ static void assertChannelEventEquals( } } + /** Assert that the embedder call is an event that matches the given data. */ + static void assertEmbedderEventEquals( + @NonNull KeyData data, + Type type, + long physicalKey, + long logicalKey, + String character, + boolean synthesized) { + assertEquals(type, data.type); + assertEquals(physicalKey, data.physicalKey); + assertEquals(logicalKey, data.logicalKey); + assertEquals(character, data.character); + assertEquals(synthesized, data.synthesized); + } + + static void verifyEmbedderEvents(List receivedCalls, KeyData[] expectedData) { + assertEquals(receivedCalls.size(), expectedData.length); + for (int idx = 0; idx < receivedCalls.size(); idx += 1) { + final KeyData data = expectedData[idx]; + assertEmbedderEventEquals( + receivedCalls.get(idx).keyData, + data.type, + data.physicalKey, + data.logicalKey, + data.character, + data.synthesized); + } + } + + static KeyData buildKeyData( + Type type, + long physicalKey, + long logicalKey, + @Nullable String characters, + boolean synthesized) { + final KeyData result = new KeyData(); + result.physicalKey = physicalKey; + result.logicalKey = logicalKey; + result.timestamp = 0x0; + result.type = type; + result.character = characters; + result.synthesized = synthesized; + return result; + } + + /** + * Start a new tester, generate a ShiftRight event under the specified condition, and return the + * output events for that event. + * + * @param preEventLeftPressed Whether ShiftLeft was recorded as pressed before the event. + * @param preEventRightPressed Whether ShiftRight was recorded as pressed before the event. + * @param rightEventIsDown Whether the dispatched event is a key down of key up of ShiftRight. + * @param truePressed Whether Shift is pressed as shown in the metaState of the event. + * @return + */ + public static List testShiftRightEvent( + boolean preEventLeftPressed, + boolean preEventRightPressed, + boolean rightEventIsDown, + boolean truePressed) { + final ArrayList calls = new ArrayList<>(); + // Even though the event is for ShiftRight, we still set SHIFT | SHIFT_LEFT here. + // See the comment in synchronizePressingKey for the reason. + final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON; + + final KeyboardTester tester = new KeyboardTester(); + tester.respondToTextInputWith(true); // Suppress redispatching + if (preEventLeftPressed) { + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON)); + } + if (preEventRightPressed) { + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_RIGHT, KEYCODE_SHIFT_RIGHT, 0, '\0', SHIFT_LEFT_ON)); + } + tester.recordEmbedderCallsTo(calls); + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + rightEventIsDown ? ACTION_DOWN : ACTION_UP, + SCAN_SHIFT_RIGHT, + KEYCODE_SHIFT_RIGHT, + 0, + '\0', + truePressed ? SHIFT_LEFT_ON : 0)); + return calls.stream() + .filter(data -> data.keyData.physicalKey != 0) + .collect(Collectors.toList()); + } + + public static KeyData buildShiftKeyData(boolean isLeft, boolean isDown, boolean isSynthesized) { + final KeyData data = new KeyData(); + data.type = isDown ? Type.kDown : Type.kUp; + data.physicalKey = isLeft ? PHYSICAL_SHIFT_LEFT : PHYSICAL_SHIFT_RIGHT; + data.logicalKey = isLeft ? LOGICAL_SHIFT_LEFT : LOGICAL_SHIFT_RIGHT; + data.synthesized = isSynthesized; + return data; + } + + /** + * Print each byte of the given buffer as a hex (such as "0a" for 0x0a), and return the + * concatenated string. + * + *

Used to compare binary content in byte buffers. + */ + static String printBufferBytes(@NonNull ByteBuffer buffer) { + final String[] results = new String[buffer.capacity()]; + for (int byteIdx = 0; byteIdx < buffer.capacity(); byteIdx += 1) { + results[byteIdx] = String.format("%02x", buffer.get(byteIdx)); + } + return String.join("", results); + } + @Before public void setUp() { MockitoAnnotations.openMocks(this); @@ -234,11 +429,91 @@ public void setUp() { // Tests start + @Test + public void serializeAndDeserializeKeyData() { + // Test data1: Non-empty character, synthesized. + final KeyData data1 = new KeyData(); + data1.physicalKey = 0x0a; + data1.logicalKey = 0x0b; + data1.timestamp = 0x0c; + data1.type = Type.kRepeat; + data1.character = "A"; + data1.synthesized = true; + + final ByteBuffer data1Buffer = data1.toBytes(); + + assertEquals( + "" + + "0100000000000000" + + "0c00000000000000" + + "0200000000000000" + + "0a00000000000000" + + "0b00000000000000" + + "0100000000000000" + + "41", + printBufferBytes(data1Buffer)); + // `position` is considered as the message size. + assertEquals(49, data1Buffer.position()); + + data1Buffer.rewind(); + final KeyData data1Loaded = new KeyData(data1Buffer); + assertEquals(data1Loaded.timestamp, data1.timestamp); + + // Test data2: Empty character, not synthesized. + final KeyData data2 = new KeyData(); + data2.physicalKey = 0xaaaabbbbccccl; + data2.logicalKey = 0x666677778888l; + data2.timestamp = 0x333344445555l; + data2.type = Type.kUp; + data2.character = null; + data2.synthesized = false; + + final ByteBuffer data2Buffer = data2.toBytes(); + + assertEquals( + "" + + "0000000000000000" + + "5555444433330000" + + "0100000000000000" + + "ccccbbbbaaaa0000" + + "8888777766660000" + + "0000000000000000", + printBufferBytes(data2Buffer)); + + data2Buffer.rewind(); + final KeyData data2Loaded = new KeyData(data2Buffer); + assertEquals(data2Loaded.timestamp, data2.timestamp); + } + + @Test + public void basicCombingCharactersTest() { + final KeyboardManager.CharacterCombiner combiner = new KeyboardManager.CharacterCombiner(); + Assert.assertEquals(0, (int) combiner.applyCombiningCharacterToBaseCharacter(0)); + Assert.assertEquals('A', (int) combiner.applyCombiningCharacterToBaseCharacter('A')); + Assert.assertEquals('B', (int) combiner.applyCombiningCharacterToBaseCharacter('B')); + Assert.assertEquals('B', (int) combiner.applyCombiningCharacterToBaseCharacter('B')); + Assert.assertEquals(0, (int) combiner.applyCombiningCharacterToBaseCharacter(0)); + Assert.assertEquals(0, (int) combiner.applyCombiningCharacterToBaseCharacter(0)); + + Assert.assertEquals('`', (int) combiner.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + Assert.assertEquals('`', (int) combiner.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + Assert.assertEquals('À', (int) combiner.applyCombiningCharacterToBaseCharacter('A')); + + Assert.assertEquals('`', (int) combiner.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + Assert.assertEquals(0, (int) combiner.applyCombiningCharacterToBaseCharacter(0)); + // The 0 input should remove the combining state. + Assert.assertEquals('A', (int) combiner.applyCombiningCharacterToBaseCharacter('A')); + + Assert.assertEquals(0, (int) combiner.applyCombiningCharacterToBaseCharacter(0)); + Assert.assertEquals('`', (int) combiner.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + Assert.assertEquals('À', (int) combiner.applyCombiningCharacterToBaseCharacter('A')); + } + @Test public void respondsTrueWhenHandlingNewEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - final ArrayList calls = new ArrayList(); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -246,7 +521,7 @@ public void respondsTrueWhenHandlingNewEvents() { assertEquals(true, result); assertEquals(calls.size(), 1); - assertChannelEventEquals(calls.get(0).channelData, "keydown", 65); + assertChannelEventEquals(calls.get(0).channelObject, "keydown", 65); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -255,10 +530,10 @@ public void respondsTrueWhenHandlingNewEvents() { } @Test - public void primaryRespondersHaveTheHighestPrecedence() { + public void channelReponderHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - final ArrayList calls = new ArrayList(); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -266,7 +541,34 @@ public void primaryRespondersHaveTheHighestPrecedence() { assertEquals(true, result); assertEquals(calls.size(), 1); - assertChannelEventEquals(calls.get(0).channelData, "keydown", 65); + assertChannelEventEquals(calls.get(0).channelObject, "keydown", 65); + + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(tester.mockView, times(0)).onTextInputKeyEvent(any(KeyEvent.class)); + verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); + + // If a primary responder handles the key event the propagation stops. + assertNotNull(calls.get(0).reply); + calls.get(0).reply.accept(true); + verify(tester.mockView, times(0)).onTextInputKeyEvent(any(KeyEvent.class)); + verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); + } + + @Test + public void embedderReponderHandlesEvents() { + final KeyboardTester tester = new KeyboardTester(); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + + final boolean result = tester.keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -281,10 +583,41 @@ public void primaryRespondersHaveTheHighestPrecedence() { } @Test - public void textInputPluginHasTheSecondHighestPrecedence() { + public void bothRespondersHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - final ArrayList calls = new ArrayList(); + final ArrayList calls = new ArrayList<>(); + + tester.recordChannelCallsTo(calls); + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); + + final boolean result = + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0)); + + assertEquals(true, result); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); + assertChannelEventEquals(calls.get(1).channelObject, "keydown", KEYCODE_A); + + verify(tester.mockView, times(0)).onTextInputKeyEvent(any(KeyEvent.class)); + verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); + + calls.get(0).reply.accept(true); + verify(tester.mockView, times(0)).onTextInputKeyEvent(any(KeyEvent.class)); + verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); + + calls.get(1).reply.accept(true); + verify(tester.mockView, times(0)).onTextInputKeyEvent(any(KeyEvent.class)); + verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); + } + + @Test + public void textInputHandlesEventsIfNoRespondersDo() { + final KeyboardTester tester = new KeyboardTester(); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -292,7 +625,7 @@ public void textInputPluginHasTheSecondHighestPrecedence() { assertEquals(true, result); assertEquals(calls.size(), 1); - assertChannelEventEquals(calls.get(0).channelData, "keydown", 65); + assertChannelEventEquals(calls.get(0).channelObject, "keydown", 65); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -311,10 +644,10 @@ public void textInputPluginHasTheSecondHighestPrecedence() { } @Test - public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { + public void redispatchEventsIfTextInputDoesntHandle() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - final ArrayList calls = new ArrayList(); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -322,7 +655,7 @@ public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { assertEquals(true, result); assertEquals(calls.size(), 1); - assertChannelEventEquals(calls.get(0).channelData, "keydown", 65); + assertChannelEventEquals(calls.get(0).channelObject, "keydown", 65); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -338,18 +671,18 @@ public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { } @Test - public void respondsFalseWhenHandlingRedispatchedEvents() { + public void redispatchedEventsAreCorrectlySkipped() { final KeyboardTester tester = new KeyboardTester(); - final ArrayList calls = new ArrayList(); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); final boolean result = tester.keyboardManager.handleEvent(keyEvent); assertEquals(true, result); assertEquals(calls.size(), 1); - assertChannelEventEquals(calls.get(0).channelData, "keydown", 65); + assertChannelEventEquals(calls.get(0).channelObject, "keydown", 65); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -366,4 +699,794 @@ public void respondsFalseWhenHandlingRedispatchedEvents() { // It's redispatched to the keyboard manager, but no eventual key calls. assertEquals(calls.size(), 1); } + + @Test + public void tapLowerA() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false), + }); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 1, 'a', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kRepeat, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false), + }); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false), + }); + calls.clear(); + } + + @Test + public void tapUpperA() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // ShiftLeft + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false), + }); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "A", false), + }); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false), + }); + calls.clear(); + + // ShiftLeft + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false), + }); + calls.clear(); + } + + @Test + public void eventsWithForgedCodes() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // Zero scan code. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, 0, KEYCODE_ENTER, 0, '\n', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, 0x300000042L, LOGICAL_ENTER, "\n", false), + }); + calls.clear(); + + // Zero scan code and zero key code. + assertEquals( + true, tester.keyboardManager.handleEvent(new FakeKeyEvent(ACTION_DOWN, 0, 0, 0, '\0', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, 0, 0, null, true), + }); + calls.clear(); + + // Unrecognized scan code. (Fictional test) + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, 0xDEADBEEF, 0, 0, '\0', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, 0x11DEADBEEFL, 0x1100000000L, null, false), + }); + calls.clear(); + + // Zero key code. (Fictional test; I have yet to find a real case.) + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_ARROW_LEFT, 0, 0, '\0', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_ARROW_LEFT, 0x1100000000L, null, false), + }); + calls.clear(); + + // Unrecognized key code. (Fictional test) + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_ARROW_RIGHT, 0xDEADBEEF, 0, '\0', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_ARROW_RIGHT, 0x11DEADBEEFL, null, false), + }); + calls.clear(); + } + + @Test + public void duplicateDownEventsArePrecededBySynthesizedUpEvents() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false), + }); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, true); + assertEmbedderEventEquals( + calls.get(1).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); + calls.clear(); + } + + @Test + public void abruptUpEventsAreIgnored() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, 0l, 0l, null, true), + }); + calls.clear(); + } + + @Test + public void modifierKeys() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // ShiftLeft + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false), + }); + calls.clear(); + + // ShiftRight + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_RIGHT, KEYCODE_SHIFT_RIGHT, 0, '\0', 0x41)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_SHIFT_RIGHT, LOGICAL_SHIFT_RIGHT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_RIGHT, KEYCODE_SHIFT_RIGHT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_SHIFT_RIGHT, LOGICAL_SHIFT_RIGHT, null, false), + }); + calls.clear(); + + // ControlLeft + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_CONTROL_LEFT, KEYCODE_CTRL_LEFT, 0, '\0', 0x3000)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_CONTROL_LEFT, KEYCODE_CTRL_LEFT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, false), + }); + calls.clear(); + + // ControlRight + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_CONTROL_RIGHT, KEYCODE_CTRL_RIGHT, 0, '\0', 0x3000)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_CONTROL_RIGHT, LOGICAL_CONTROL_RIGHT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_CONTROL_RIGHT, KEYCODE_CTRL_RIGHT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_CONTROL_RIGHT, LOGICAL_CONTROL_RIGHT, null, false), + }); + calls.clear(); + + // AltLeft + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_LEFT, KEYCODE_ALT_LEFT, 0, '\0', 0x12)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ALT_LEFT, KEYCODE_ALT_LEFT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, false), + }); + calls.clear(); + + // AltRight + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_RIGHT, KEYCODE_ALT_RIGHT, 0, '\0', 0x12)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ALT_RIGHT, KEYCODE_ALT_RIGHT, 0, '\0', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false), + }); + calls.clear(); + } + + @Test + public void nonUsKeys() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // French 1 + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_DIGIT1, KEYCODE_1, 0, '1', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, "1", false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '1', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, null, false), + }); + calls.clear(); + + // French Shift-1 + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41)); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_DIGIT1, KEYCODE_1, 0, '&', 0x41)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, "&", false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '&', 0x41)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, null, false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0)); + calls.clear(); + + // Russian lowerA + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, '\u0444', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "ф", false), + }); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, '\u0444', 0)); + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData(Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false), + }); + calls.clear(); + } + + @Test + public void synchronizeShiftLeftDuringForeignKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ArrowLeft. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON; + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', SHIFT_LEFT_ON))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true); + calls.clear(); + } + + @Test + public void synchronizeShiftLeftDuringSelfKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ShiftLeft. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON; + // All 6 cases (3 types x 2 states) are arranged in the following order so that the starting + // states for each case are the desired states. + + // Repeat event when current state is 0. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 1, '\0', SHIFT_LEFT_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + calls.clear(); + + // Down event when the current state is 1. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true); + assertEmbedderEventEquals( + calls.get(1).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + calls.clear(); + + // Up event when the current state is 1. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + calls.clear(); + + // Up event when the current state is 0. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals(calls.get(0).keyData, Type.kDown, 0l, 0l, null, true); + calls.clear(); + + // Down event when the current state is 0. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + calls.clear(); + + // Repeat event when the current state is 1. + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 1, '\0', SHIFT_LEFT_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kRepeat, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + calls.clear(); + } + + @Test + public void synchronizeShiftLeftDuringSiblingKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ShiftRight. The following events seem + // to have weird metaStates that don't follow Android's documentation (always using left masks) + // but are indeed observed on ChromeOS. + + // UP_EVENT, truePressed: false + + verifyEmbedderEvents(testShiftRightEvent(false, false, UP_EVENT, false), new KeyData[] {}); + verifyEmbedderEvents( + testShiftRightEvent(false, true, UP_EVENT, false), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, false), + }); + verifyEmbedderEvents( + testShiftRightEvent(true, false, UP_EVENT, false), + new KeyData[] { + buildShiftKeyData(SHIFT_LEFT_EVENT, UP_EVENT, true), + }); + verifyEmbedderEvents( + testShiftRightEvent(true, true, UP_EVENT, false), + new KeyData[] { + buildShiftKeyData(SHIFT_LEFT_EVENT, UP_EVENT, true), + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, false), + }); + + // UP_EVENT, truePressed: true + + verifyEmbedderEvents( + testShiftRightEvent(false, false, UP_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_LEFT_EVENT, DOWN_EVENT, true), + }); + verifyEmbedderEvents( + testShiftRightEvent(false, true, UP_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_LEFT_EVENT, DOWN_EVENT, true), + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, false), + }); + verifyEmbedderEvents(testShiftRightEvent(true, false, UP_EVENT, true), new KeyData[] {}); + verifyEmbedderEvents( + testShiftRightEvent(true, true, UP_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, false), + }); + + // DOWN_EVENT, truePressed: false - skipped, because they're impossible. + + // DOWN_EVENT, truePressed: true + + verifyEmbedderEvents( + testShiftRightEvent(false, false, DOWN_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, DOWN_EVENT, false), + }); + verifyEmbedderEvents( + testShiftRightEvent(false, true, DOWN_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, true), + buildShiftKeyData(SHIFT_RIGHT_EVENT, DOWN_EVENT, false), + }); + verifyEmbedderEvents( + testShiftRightEvent(true, false, DOWN_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, DOWN_EVENT, false), + }); + verifyEmbedderEvents( + testShiftRightEvent(true, true, DOWN_EVENT, true), + new KeyData[] { + buildShiftKeyData(SHIFT_RIGHT_EVENT, UP_EVENT, true), + buildShiftKeyData(SHIFT_RIGHT_EVENT, DOWN_EVENT, false), + }); + } + + @Test + public void synchronizeOtherModifiers() { + // Test if other modifiers can be synchronized during events of ArrowLeft. Only the minimal + // cases are used here since the full logic has been tested on ShiftLeft. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_CTRL_ON))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, true); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, true); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_ALT_ON))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, true); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 2); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, true); + calls.clear(); + } + + @Test + public void normalCapsLockEvents() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + // The following two events seem to have weird metaStates that don't follow Android's + // documentation (CapsLock flag set on down events) but are indeed observed on ChromeOS. + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + } + + @Test + public void synchronizeCapsLock() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 3); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, true); + assertEmbedderEventEquals( + calls.get(1).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, true); + assertEmbedderEventEquals( + calls.get(2).keyData, Type.kDown, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kDown, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_UP, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', META_CAPS_LOCK_ON))); + assertEquals(calls.size(), 3); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, true); + assertEmbedderEventEquals( + calls.get(1).keyData, Type.kDown, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, true); + assertEmbedderEventEquals( + calls.get(2).keyData, Type.kUp, PHYSICAL_ARROW_LEFT, LOGICAL_ARROW_LEFT, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_CAPS_LOCK, KEYCODE_CAPS_LOCK, 0, '\0', 0))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); + calls.clear(); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index b407a2d4a6730..7032c742f2cfe 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -103,7 +103,7 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs); // Send an enter key and make sure the Editable received it. - FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); + FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, '\n'); inputConnectionAdaptor.handleKeyEvent(keyEvent); verify(spyEditable, times(1)).insert(eq(0), anyString()); } @@ -1115,7 +1115,7 @@ public void testDoesNotConsumeBackButton() { ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); - FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); + FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, '\b'); boolean didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); diff --git a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java index 75ce7b6ddb302..4faa39107f50b 100644 --- a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -9,10 +9,23 @@ public FakeKeyEvent(int action, int keyCode) { super(action, keyCode); } + public FakeKeyEvent(int action, int keyCode, char character) { + super(action, keyCode); + this.character = character; + } + + public FakeKeyEvent( + int action, int scancode, int code, int repeat, char character, int metaState) { + super(0, 0, action, code, repeat, metaState, 0, scancode); + this.character = character; + } + + private char character = 0; + public final int getUnicodeChar() { if (getKeyCode() == KeyEvent.KEYCODE_BACK) { return 0; } - return 1; + return character; } } diff --git a/shell/platform/android/test/io/flutter/util/KeyCodes.java b/shell/platform/android/test/io/flutter/util/KeyCodes.java new file mode 100644 index 0000000000000..0f7035b9f3f70 --- /dev/null +++ b/shell/platform/android/test/io/flutter/util/KeyCodes.java @@ -0,0 +1,733 @@ +package io.flutter.util; + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// This file is generated by +// flutter/flutter:dev/tools/gen_keycodes/bin/gen_keycodes.dart and should not +// be edited directly. +// +// Edit the template +// flutter/flutter:dev/tools/gen_keycodes/data/key_codes_java.tmpl +// instead. +// +// See flutter/flutter:dev/tools/gen_keycodes/README.md for more information. + +/** + * This class contains keyboard constants to be used in unit tests. They should not be used in + * production code. + */ +public class KeyCodes { + public static final long PHYSICAL_HYPER = 0x00000010L; + public static final long PHYSICAL_SUPER_KEY = 0x00000011L; + public static final long PHYSICAL_FN = 0x00000012L; + public static final long PHYSICAL_FN_LOCK = 0x00000013L; + public static final long PHYSICAL_SUSPEND = 0x00000014L; + public static final long PHYSICAL_RESUME = 0x00000015L; + public static final long PHYSICAL_TURBO = 0x00000016L; + public static final long PHYSICAL_PRIVACY_SCREEN_TOGGLE = 0x00000017L; + public static final long PHYSICAL_MICROPHONE_MUTE_TOGGLE = 0x00000018L; + public static final long PHYSICAL_SLEEP = 0x00010082L; + public static final long PHYSICAL_WAKE_UP = 0x00010083L; + public static final long PHYSICAL_DISPLAY_TOGGLE_INT_EXT = 0x000100b5L; + public static final long PHYSICAL_GAME_BUTTON1 = 0x0005ff01L; + public static final long PHYSICAL_GAME_BUTTON2 = 0x0005ff02L; + public static final long PHYSICAL_GAME_BUTTON3 = 0x0005ff03L; + public static final long PHYSICAL_GAME_BUTTON4 = 0x0005ff04L; + public static final long PHYSICAL_GAME_BUTTON5 = 0x0005ff05L; + public static final long PHYSICAL_GAME_BUTTON6 = 0x0005ff06L; + public static final long PHYSICAL_GAME_BUTTON7 = 0x0005ff07L; + public static final long PHYSICAL_GAME_BUTTON8 = 0x0005ff08L; + public static final long PHYSICAL_GAME_BUTTON9 = 0x0005ff09L; + public static final long PHYSICAL_GAME_BUTTON10 = 0x0005ff0aL; + public static final long PHYSICAL_GAME_BUTTON11 = 0x0005ff0bL; + public static final long PHYSICAL_GAME_BUTTON12 = 0x0005ff0cL; + public static final long PHYSICAL_GAME_BUTTON13 = 0x0005ff0dL; + public static final long PHYSICAL_GAME_BUTTON14 = 0x0005ff0eL; + public static final long PHYSICAL_GAME_BUTTON15 = 0x0005ff0fL; + public static final long PHYSICAL_GAME_BUTTON16 = 0x0005ff10L; + public static final long PHYSICAL_GAME_BUTTON_A = 0x0005ff11L; + public static final long PHYSICAL_GAME_BUTTON_B = 0x0005ff12L; + public static final long PHYSICAL_GAME_BUTTON_C = 0x0005ff13L; + public static final long PHYSICAL_GAME_BUTTON_LEFT1 = 0x0005ff14L; + public static final long PHYSICAL_GAME_BUTTON_LEFT2 = 0x0005ff15L; + public static final long PHYSICAL_GAME_BUTTON_MODE = 0x0005ff16L; + public static final long PHYSICAL_GAME_BUTTON_RIGHT1 = 0x0005ff17L; + public static final long PHYSICAL_GAME_BUTTON_RIGHT2 = 0x0005ff18L; + public static final long PHYSICAL_GAME_BUTTON_SELECT = 0x0005ff19L; + public static final long PHYSICAL_GAME_BUTTON_START = 0x0005ff1aL; + public static final long PHYSICAL_GAME_BUTTON_THUMB_LEFT = 0x0005ff1bL; + public static final long PHYSICAL_GAME_BUTTON_THUMB_RIGHT = 0x0005ff1cL; + public static final long PHYSICAL_GAME_BUTTON_X = 0x0005ff1dL; + public static final long PHYSICAL_GAME_BUTTON_Y = 0x0005ff1eL; + public static final long PHYSICAL_GAME_BUTTON_Z = 0x0005ff1fL; + public static final long PHYSICAL_USB_RESERVED = 0x00070000L; + public static final long PHYSICAL_USB_ERROR_ROLL_OVER = 0x00070001L; + public static final long PHYSICAL_USB_POST_FAIL = 0x00070002L; + public static final long PHYSICAL_USB_ERROR_UNDEFINED = 0x00070003L; + public static final long PHYSICAL_KEY_A = 0x00070004L; + public static final long PHYSICAL_KEY_B = 0x00070005L; + public static final long PHYSICAL_KEY_C = 0x00070006L; + public static final long PHYSICAL_KEY_D = 0x00070007L; + public static final long PHYSICAL_KEY_E = 0x00070008L; + public static final long PHYSICAL_KEY_F = 0x00070009L; + public static final long PHYSICAL_KEY_G = 0x0007000aL; + public static final long PHYSICAL_KEY_H = 0x0007000bL; + public static final long PHYSICAL_KEY_I = 0x0007000cL; + public static final long PHYSICAL_KEY_J = 0x0007000dL; + public static final long PHYSICAL_KEY_K = 0x0007000eL; + public static final long PHYSICAL_KEY_L = 0x0007000fL; + public static final long PHYSICAL_KEY_M = 0x00070010L; + public static final long PHYSICAL_KEY_N = 0x00070011L; + public static final long PHYSICAL_KEY_O = 0x00070012L; + public static final long PHYSICAL_KEY_P = 0x00070013L; + public static final long PHYSICAL_KEY_Q = 0x00070014L; + public static final long PHYSICAL_KEY_R = 0x00070015L; + public static final long PHYSICAL_KEY_S = 0x00070016L; + public static final long PHYSICAL_KEY_T = 0x00070017L; + public static final long PHYSICAL_KEY_U = 0x00070018L; + public static final long PHYSICAL_KEY_V = 0x00070019L; + public static final long PHYSICAL_KEY_W = 0x0007001aL; + public static final long PHYSICAL_KEY_X = 0x0007001bL; + public static final long PHYSICAL_KEY_Y = 0x0007001cL; + public static final long PHYSICAL_KEY_Z = 0x0007001dL; + public static final long PHYSICAL_DIGIT1 = 0x0007001eL; + public static final long PHYSICAL_DIGIT2 = 0x0007001fL; + public static final long PHYSICAL_DIGIT3 = 0x00070020L; + public static final long PHYSICAL_DIGIT4 = 0x00070021L; + public static final long PHYSICAL_DIGIT5 = 0x00070022L; + public static final long PHYSICAL_DIGIT6 = 0x00070023L; + public static final long PHYSICAL_DIGIT7 = 0x00070024L; + public static final long PHYSICAL_DIGIT8 = 0x00070025L; + public static final long PHYSICAL_DIGIT9 = 0x00070026L; + public static final long PHYSICAL_DIGIT0 = 0x00070027L; + public static final long PHYSICAL_ENTER = 0x00070028L; + public static final long PHYSICAL_ESCAPE = 0x00070029L; + public static final long PHYSICAL_BACKSPACE = 0x0007002aL; + public static final long PHYSICAL_TAB = 0x0007002bL; + public static final long PHYSICAL_SPACE = 0x0007002cL; + public static final long PHYSICAL_MINUS = 0x0007002dL; + public static final long PHYSICAL_EQUAL = 0x0007002eL; + public static final long PHYSICAL_BRACKET_LEFT = 0x0007002fL; + public static final long PHYSICAL_BRACKET_RIGHT = 0x00070030L; + public static final long PHYSICAL_BACKSLASH = 0x00070031L; + public static final long PHYSICAL_SEMICOLON = 0x00070033L; + public static final long PHYSICAL_QUOTE = 0x00070034L; + public static final long PHYSICAL_BACKQUOTE = 0x00070035L; + public static final long PHYSICAL_COMMA = 0x00070036L; + public static final long PHYSICAL_PERIOD = 0x00070037L; + public static final long PHYSICAL_SLASH = 0x00070038L; + public static final long PHYSICAL_CAPS_LOCK = 0x00070039L; + public static final long PHYSICAL_F1 = 0x0007003aL; + public static final long PHYSICAL_F2 = 0x0007003bL; + public static final long PHYSICAL_F3 = 0x0007003cL; + public static final long PHYSICAL_F4 = 0x0007003dL; + public static final long PHYSICAL_F5 = 0x0007003eL; + public static final long PHYSICAL_F6 = 0x0007003fL; + public static final long PHYSICAL_F7 = 0x00070040L; + public static final long PHYSICAL_F8 = 0x00070041L; + public static final long PHYSICAL_F9 = 0x00070042L; + public static final long PHYSICAL_F10 = 0x00070043L; + public static final long PHYSICAL_F11 = 0x00070044L; + public static final long PHYSICAL_F12 = 0x00070045L; + public static final long PHYSICAL_PRINT_SCREEN = 0x00070046L; + public static final long PHYSICAL_SCROLL_LOCK = 0x00070047L; + public static final long PHYSICAL_PAUSE = 0x00070048L; + public static final long PHYSICAL_INSERT = 0x00070049L; + public static final long PHYSICAL_HOME = 0x0007004aL; + public static final long PHYSICAL_PAGE_UP = 0x0007004bL; + public static final long PHYSICAL_DELETE = 0x0007004cL; + public static final long PHYSICAL_END = 0x0007004dL; + public static final long PHYSICAL_PAGE_DOWN = 0x0007004eL; + public static final long PHYSICAL_ARROW_RIGHT = 0x0007004fL; + public static final long PHYSICAL_ARROW_LEFT = 0x00070050L; + public static final long PHYSICAL_ARROW_DOWN = 0x00070051L; + public static final long PHYSICAL_ARROW_UP = 0x00070052L; + public static final long PHYSICAL_NUM_LOCK = 0x00070053L; + public static final long PHYSICAL_NUMPAD_DIVIDE = 0x00070054L; + public static final long PHYSICAL_NUMPAD_MULTIPLY = 0x00070055L; + public static final long PHYSICAL_NUMPAD_SUBTRACT = 0x00070056L; + public static final long PHYSICAL_NUMPAD_ADD = 0x00070057L; + public static final long PHYSICAL_NUMPAD_ENTER = 0x00070058L; + public static final long PHYSICAL_NUMPAD1 = 0x00070059L; + public static final long PHYSICAL_NUMPAD2 = 0x0007005aL; + public static final long PHYSICAL_NUMPAD3 = 0x0007005bL; + public static final long PHYSICAL_NUMPAD4 = 0x0007005cL; + public static final long PHYSICAL_NUMPAD5 = 0x0007005dL; + public static final long PHYSICAL_NUMPAD6 = 0x0007005eL; + public static final long PHYSICAL_NUMPAD7 = 0x0007005fL; + public static final long PHYSICAL_NUMPAD8 = 0x00070060L; + public static final long PHYSICAL_NUMPAD9 = 0x00070061L; + public static final long PHYSICAL_NUMPAD0 = 0x00070062L; + public static final long PHYSICAL_NUMPAD_DECIMAL = 0x00070063L; + public static final long PHYSICAL_INTL_BACKSLASH = 0x00070064L; + public static final long PHYSICAL_CONTEXT_MENU = 0x00070065L; + public static final long PHYSICAL_POWER = 0x00070066L; + public static final long PHYSICAL_NUMPAD_EQUAL = 0x00070067L; + public static final long PHYSICAL_F13 = 0x00070068L; + public static final long PHYSICAL_F14 = 0x00070069L; + public static final long PHYSICAL_F15 = 0x0007006aL; + public static final long PHYSICAL_F16 = 0x0007006bL; + public static final long PHYSICAL_F17 = 0x0007006cL; + public static final long PHYSICAL_F18 = 0x0007006dL; + public static final long PHYSICAL_F19 = 0x0007006eL; + public static final long PHYSICAL_F20 = 0x0007006fL; + public static final long PHYSICAL_F21 = 0x00070070L; + public static final long PHYSICAL_F22 = 0x00070071L; + public static final long PHYSICAL_F23 = 0x00070072L; + public static final long PHYSICAL_F24 = 0x00070073L; + public static final long PHYSICAL_OPEN = 0x00070074L; + public static final long PHYSICAL_HELP = 0x00070075L; + public static final long PHYSICAL_SELECT = 0x00070077L; + public static final long PHYSICAL_AGAIN = 0x00070079L; + public static final long PHYSICAL_UNDO = 0x0007007aL; + public static final long PHYSICAL_CUT = 0x0007007bL; + public static final long PHYSICAL_COPY = 0x0007007cL; + public static final long PHYSICAL_PASTE = 0x0007007dL; + public static final long PHYSICAL_FIND = 0x0007007eL; + public static final long PHYSICAL_AUDIO_VOLUME_MUTE = 0x0007007fL; + public static final long PHYSICAL_AUDIO_VOLUME_UP = 0x00070080L; + public static final long PHYSICAL_AUDIO_VOLUME_DOWN = 0x00070081L; + public static final long PHYSICAL_NUMPAD_COMMA = 0x00070085L; + public static final long PHYSICAL_INTL_RO = 0x00070087L; + public static final long PHYSICAL_KANA_MODE = 0x00070088L; + public static final long PHYSICAL_INTL_YEN = 0x00070089L; + public static final long PHYSICAL_CONVERT = 0x0007008aL; + public static final long PHYSICAL_NON_CONVERT = 0x0007008bL; + public static final long PHYSICAL_LANG1 = 0x00070090L; + public static final long PHYSICAL_LANG2 = 0x00070091L; + public static final long PHYSICAL_LANG3 = 0x00070092L; + public static final long PHYSICAL_LANG4 = 0x00070093L; + public static final long PHYSICAL_LANG5 = 0x00070094L; + public static final long PHYSICAL_ABORT = 0x0007009bL; + public static final long PHYSICAL_PROPS = 0x000700a3L; + public static final long PHYSICAL_NUMPAD_PAREN_LEFT = 0x000700b6L; + public static final long PHYSICAL_NUMPAD_PAREN_RIGHT = 0x000700b7L; + public static final long PHYSICAL_NUMPAD_BACKSPACE = 0x000700bbL; + public static final long PHYSICAL_NUMPAD_MEMORY_STORE = 0x000700d0L; + public static final long PHYSICAL_NUMPAD_MEMORY_RECALL = 0x000700d1L; + public static final long PHYSICAL_NUMPAD_MEMORY_CLEAR = 0x000700d2L; + public static final long PHYSICAL_NUMPAD_MEMORY_ADD = 0x000700d3L; + public static final long PHYSICAL_NUMPAD_MEMORY_SUBTRACT = 0x000700d4L; + public static final long PHYSICAL_NUMPAD_SIGN_CHANGE = 0x000700d7L; + public static final long PHYSICAL_NUMPAD_CLEAR = 0x000700d8L; + public static final long PHYSICAL_NUMPAD_CLEAR_ENTRY = 0x000700d9L; + public static final long PHYSICAL_CONTROL_LEFT = 0x000700e0L; + public static final long PHYSICAL_SHIFT_LEFT = 0x000700e1L; + public static final long PHYSICAL_ALT_LEFT = 0x000700e2L; + public static final long PHYSICAL_META_LEFT = 0x000700e3L; + public static final long PHYSICAL_CONTROL_RIGHT = 0x000700e4L; + public static final long PHYSICAL_SHIFT_RIGHT = 0x000700e5L; + public static final long PHYSICAL_ALT_RIGHT = 0x000700e6L; + public static final long PHYSICAL_META_RIGHT = 0x000700e7L; + public static final long PHYSICAL_INFO = 0x000c0060L; + public static final long PHYSICAL_CLOSED_CAPTION_TOGGLE = 0x000c0061L; + public static final long PHYSICAL_BRIGHTNESS_UP = 0x000c006fL; + public static final long PHYSICAL_BRIGHTNESS_DOWN = 0x000c0070L; + public static final long PHYSICAL_BRIGHTNESS_TOGGLE = 0x000c0072L; + public static final long PHYSICAL_BRIGHTNESS_MINIMUM = 0x000c0073L; + public static final long PHYSICAL_BRIGHTNESS_MAXIMUM = 0x000c0074L; + public static final long PHYSICAL_BRIGHTNESS_AUTO = 0x000c0075L; + public static final long PHYSICAL_KBD_ILLUM_UP = 0x000c0079L; + public static final long PHYSICAL_KBD_ILLUM_DOWN = 0x000c007aL; + public static final long PHYSICAL_MEDIA_LAST = 0x000c0083L; + public static final long PHYSICAL_LAUNCH_PHONE = 0x000c008cL; + public static final long PHYSICAL_PROGRAM_GUIDE = 0x000c008dL; + public static final long PHYSICAL_EXIT = 0x000c0094L; + public static final long PHYSICAL_CHANNEL_UP = 0x000c009cL; + public static final long PHYSICAL_CHANNEL_DOWN = 0x000c009dL; + public static final long PHYSICAL_MEDIA_PLAY = 0x000c00b0L; + public static final long PHYSICAL_MEDIA_PAUSE = 0x000c00b1L; + public static final long PHYSICAL_MEDIA_RECORD = 0x000c00b2L; + public static final long PHYSICAL_MEDIA_FAST_FORWARD = 0x000c00b3L; + public static final long PHYSICAL_MEDIA_REWIND = 0x000c00b4L; + public static final long PHYSICAL_MEDIA_TRACK_NEXT = 0x000c00b5L; + public static final long PHYSICAL_MEDIA_TRACK_PREVIOUS = 0x000c00b6L; + public static final long PHYSICAL_MEDIA_STOP = 0x000c00b7L; + public static final long PHYSICAL_EJECT = 0x000c00b8L; + public static final long PHYSICAL_MEDIA_PLAY_PAUSE = 0x000c00cdL; + public static final long PHYSICAL_SPEECH_INPUT_TOGGLE = 0x000c00cfL; + public static final long PHYSICAL_BASS_BOOST = 0x000c00e5L; + public static final long PHYSICAL_MEDIA_SELECT = 0x000c0183L; + public static final long PHYSICAL_LAUNCH_WORD_PROCESSOR = 0x000c0184L; + public static final long PHYSICAL_LAUNCH_SPREADSHEET = 0x000c0186L; + public static final long PHYSICAL_LAUNCH_MAIL = 0x000c018aL; + public static final long PHYSICAL_LAUNCH_CONTACTS = 0x000c018dL; + public static final long PHYSICAL_LAUNCH_CALENDAR = 0x000c018eL; + public static final long PHYSICAL_LAUNCH_APP2 = 0x000c0192L; + public static final long PHYSICAL_LAUNCH_APP1 = 0x000c0194L; + public static final long PHYSICAL_LAUNCH_INTERNET_BROWSER = 0x000c0196L; + public static final long PHYSICAL_LOG_OFF = 0x000c019cL; + public static final long PHYSICAL_LOCK_SCREEN = 0x000c019eL; + public static final long PHYSICAL_LAUNCH_CONTROL_PANEL = 0x000c019fL; + public static final long PHYSICAL_SELECT_TASK = 0x000c01a2L; + public static final long PHYSICAL_LAUNCH_DOCUMENTS = 0x000c01a7L; + public static final long PHYSICAL_SPELL_CHECK = 0x000c01abL; + public static final long PHYSICAL_LAUNCH_KEYBOARD_LAYOUT = 0x000c01aeL; + public static final long PHYSICAL_LAUNCH_SCREEN_SAVER = 0x000c01b1L; + public static final long PHYSICAL_LAUNCH_AUDIO_BROWSER = 0x000c01b7L; + public static final long PHYSICAL_LAUNCH_ASSISTANT = 0x000c01cbL; + public static final long PHYSICAL_NEW_KEY = 0x000c0201L; + public static final long PHYSICAL_CLOSE = 0x000c0203L; + public static final long PHYSICAL_SAVE = 0x000c0207L; + public static final long PHYSICAL_PRINT = 0x000c0208L; + public static final long PHYSICAL_BROWSER_SEARCH = 0x000c0221L; + public static final long PHYSICAL_BROWSER_HOME = 0x000c0223L; + public static final long PHYSICAL_BROWSER_BACK = 0x000c0224L; + public static final long PHYSICAL_BROWSER_FORWARD = 0x000c0225L; + public static final long PHYSICAL_BROWSER_STOP = 0x000c0226L; + public static final long PHYSICAL_BROWSER_REFRESH = 0x000c0227L; + public static final long PHYSICAL_BROWSER_FAVORITES = 0x000c022aL; + public static final long PHYSICAL_ZOOM_IN = 0x000c022dL; + public static final long PHYSICAL_ZOOM_OUT = 0x000c022eL; + public static final long PHYSICAL_ZOOM_TOGGLE = 0x000c0232L; + public static final long PHYSICAL_REDO = 0x000c0279L; + public static final long PHYSICAL_MAIL_REPLY = 0x000c0289L; + public static final long PHYSICAL_MAIL_FORWARD = 0x000c028bL; + public static final long PHYSICAL_MAIL_SEND = 0x000c028cL; + public static final long PHYSICAL_KEYBOARD_LAYOUT_SELECT = 0x000c029dL; + public static final long PHYSICAL_SHOW_ALL_WINDOWS = 0x000c029fL; + + public static final long LOGICAL_SPACE = 0x00000000020L; + public static final long LOGICAL_EXCLAMATION = 0x00000000021L; + public static final long LOGICAL_QUOTE = 0x00000000022L; + public static final long LOGICAL_NUMBER_SIGN = 0x00000000023L; + public static final long LOGICAL_DOLLAR = 0x00000000024L; + public static final long LOGICAL_PERCENT = 0x00000000025L; + public static final long LOGICAL_AMPERSAND = 0x00000000026L; + public static final long LOGICAL_QUOTE_SINGLE = 0x00000000027L; + public static final long LOGICAL_PARENTHESIS_LEFT = 0x00000000028L; + public static final long LOGICAL_PARENTHESIS_RIGHT = 0x00000000029L; + public static final long LOGICAL_ASTERISK = 0x0000000002aL; + public static final long LOGICAL_ADD = 0x0000000002bL; + public static final long LOGICAL_COMMA = 0x0000000002cL; + public static final long LOGICAL_MINUS = 0x0000000002dL; + public static final long LOGICAL_PERIOD = 0x0000000002eL; + public static final long LOGICAL_SLASH = 0x0000000002fL; + public static final long LOGICAL_DIGIT0 = 0x00000000030L; + public static final long LOGICAL_DIGIT1 = 0x00000000031L; + public static final long LOGICAL_DIGIT2 = 0x00000000032L; + public static final long LOGICAL_DIGIT3 = 0x00000000033L; + public static final long LOGICAL_DIGIT4 = 0x00000000034L; + public static final long LOGICAL_DIGIT5 = 0x00000000035L; + public static final long LOGICAL_DIGIT6 = 0x00000000036L; + public static final long LOGICAL_DIGIT7 = 0x00000000037L; + public static final long LOGICAL_DIGIT8 = 0x00000000038L; + public static final long LOGICAL_DIGIT9 = 0x00000000039L; + public static final long LOGICAL_COLON = 0x0000000003aL; + public static final long LOGICAL_SEMICOLON = 0x0000000003bL; + public static final long LOGICAL_LESS = 0x0000000003cL; + public static final long LOGICAL_EQUAL = 0x0000000003dL; + public static final long LOGICAL_GREATER = 0x0000000003eL; + public static final long LOGICAL_QUESTION = 0x0000000003fL; + public static final long LOGICAL_AT = 0x00000000040L; + public static final long LOGICAL_BRACKET_LEFT = 0x0000000005bL; + public static final long LOGICAL_BACKSLASH = 0x0000000005cL; + public static final long LOGICAL_BRACKET_RIGHT = 0x0000000005dL; + public static final long LOGICAL_CARET = 0x0000000005eL; + public static final long LOGICAL_UNDERSCORE = 0x0000000005fL; + public static final long LOGICAL_BACKQUOTE = 0x00000000060L; + public static final long LOGICAL_KEY_A = 0x00000000061L; + public static final long LOGICAL_KEY_B = 0x00000000062L; + public static final long LOGICAL_KEY_C = 0x00000000063L; + public static final long LOGICAL_KEY_D = 0x00000000064L; + public static final long LOGICAL_KEY_E = 0x00000000065L; + public static final long LOGICAL_KEY_F = 0x00000000066L; + public static final long LOGICAL_KEY_G = 0x00000000067L; + public static final long LOGICAL_KEY_H = 0x00000000068L; + public static final long LOGICAL_KEY_I = 0x00000000069L; + public static final long LOGICAL_KEY_J = 0x0000000006aL; + public static final long LOGICAL_KEY_K = 0x0000000006bL; + public static final long LOGICAL_KEY_L = 0x0000000006cL; + public static final long LOGICAL_KEY_M = 0x0000000006dL; + public static final long LOGICAL_KEY_N = 0x0000000006eL; + public static final long LOGICAL_KEY_O = 0x0000000006fL; + public static final long LOGICAL_KEY_P = 0x00000000070L; + public static final long LOGICAL_KEY_Q = 0x00000000071L; + public static final long LOGICAL_KEY_R = 0x00000000072L; + public static final long LOGICAL_KEY_S = 0x00000000073L; + public static final long LOGICAL_KEY_T = 0x00000000074L; + public static final long LOGICAL_KEY_U = 0x00000000075L; + public static final long LOGICAL_KEY_V = 0x00000000076L; + public static final long LOGICAL_KEY_W = 0x00000000077L; + public static final long LOGICAL_KEY_X = 0x00000000078L; + public static final long LOGICAL_KEY_Y = 0x00000000079L; + public static final long LOGICAL_KEY_Z = 0x0000000007aL; + public static final long LOGICAL_BRACE_LEFT = 0x0000000007bL; + public static final long LOGICAL_BAR = 0x0000000007cL; + public static final long LOGICAL_BRACE_RIGHT = 0x0000000007dL; + public static final long LOGICAL_TILDE = 0x0000000007eL; + public static final long LOGICAL_UNIDENTIFIED = 0x00100000001L; + public static final long LOGICAL_BACKSPACE = 0x00100000008L; + public static final long LOGICAL_TAB = 0x00100000009L; + public static final long LOGICAL_ENTER = 0x0010000000dL; + public static final long LOGICAL_ESCAPE = 0x0010000001bL; + public static final long LOGICAL_DELETE = 0x0010000007fL; + public static final long LOGICAL_ACCEL = 0x00100000101L; + public static final long LOGICAL_ALT_GRAPH = 0x00100000103L; + public static final long LOGICAL_CAPS_LOCK = 0x00100000104L; + public static final long LOGICAL_FN = 0x00100000106L; + public static final long LOGICAL_FN_LOCK = 0x00100000107L; + public static final long LOGICAL_HYPER = 0x00100000108L; + public static final long LOGICAL_NUM_LOCK = 0x0010000010aL; + public static final long LOGICAL_SCROLL_LOCK = 0x0010000010cL; + public static final long LOGICAL_SUPER_KEY = 0x0010000010eL; + public static final long LOGICAL_SYMBOL = 0x0010000010fL; + public static final long LOGICAL_SYMBOL_LOCK = 0x00100000110L; + public static final long LOGICAL_SHIFT_LEVEL5 = 0x00100000111L; + public static final long LOGICAL_ARROW_DOWN = 0x00100000301L; + public static final long LOGICAL_ARROW_LEFT = 0x00100000302L; + public static final long LOGICAL_ARROW_RIGHT = 0x00100000303L; + public static final long LOGICAL_ARROW_UP = 0x00100000304L; + public static final long LOGICAL_END = 0x00100000305L; + public static final long LOGICAL_HOME = 0x00100000306L; + public static final long LOGICAL_PAGE_DOWN = 0x00100000307L; + public static final long LOGICAL_PAGE_UP = 0x00100000308L; + public static final long LOGICAL_CLEAR = 0x00100000401L; + public static final long LOGICAL_COPY = 0x00100000402L; + public static final long LOGICAL_CR_SEL = 0x00100000403L; + public static final long LOGICAL_CUT = 0x00100000404L; + public static final long LOGICAL_ERASE_EOF = 0x00100000405L; + public static final long LOGICAL_EX_SEL = 0x00100000406L; + public static final long LOGICAL_INSERT = 0x00100000407L; + public static final long LOGICAL_PASTE = 0x00100000408L; + public static final long LOGICAL_REDO = 0x00100000409L; + public static final long LOGICAL_UNDO = 0x0010000040aL; + public static final long LOGICAL_ACCEPT = 0x00100000501L; + public static final long LOGICAL_AGAIN = 0x00100000502L; + public static final long LOGICAL_ATTN = 0x00100000503L; + public static final long LOGICAL_CANCEL = 0x00100000504L; + public static final long LOGICAL_CONTEXT_MENU = 0x00100000505L; + public static final long LOGICAL_EXECUTE = 0x00100000506L; + public static final long LOGICAL_FIND = 0x00100000507L; + public static final long LOGICAL_HELP = 0x00100000508L; + public static final long LOGICAL_PAUSE = 0x00100000509L; + public static final long LOGICAL_PLAY = 0x0010000050aL; + public static final long LOGICAL_PROPS = 0x0010000050bL; + public static final long LOGICAL_SELECT = 0x0010000050cL; + public static final long LOGICAL_ZOOM_IN = 0x0010000050dL; + public static final long LOGICAL_ZOOM_OUT = 0x0010000050eL; + public static final long LOGICAL_BRIGHTNESS_DOWN = 0x00100000601L; + public static final long LOGICAL_BRIGHTNESS_UP = 0x00100000602L; + public static final long LOGICAL_CAMERA = 0x00100000603L; + public static final long LOGICAL_EJECT = 0x00100000604L; + public static final long LOGICAL_LOG_OFF = 0x00100000605L; + public static final long LOGICAL_POWER = 0x00100000606L; + public static final long LOGICAL_POWER_OFF = 0x00100000607L; + public static final long LOGICAL_PRINT_SCREEN = 0x00100000608L; + public static final long LOGICAL_HIBERNATE = 0x00100000609L; + public static final long LOGICAL_STANDBY = 0x0010000060aL; + public static final long LOGICAL_WAKE_UP = 0x0010000060bL; + public static final long LOGICAL_ALL_CANDIDATES = 0x00100000701L; + public static final long LOGICAL_ALPHANUMERIC = 0x00100000702L; + public static final long LOGICAL_CODE_INPUT = 0x00100000703L; + public static final long LOGICAL_COMPOSE = 0x00100000704L; + public static final long LOGICAL_CONVERT = 0x00100000705L; + public static final long LOGICAL_FINAL_MODE = 0x00100000706L; + public static final long LOGICAL_GROUP_FIRST = 0x00100000707L; + public static final long LOGICAL_GROUP_LAST = 0x00100000708L; + public static final long LOGICAL_GROUP_NEXT = 0x00100000709L; + public static final long LOGICAL_GROUP_PREVIOUS = 0x0010000070aL; + public static final long LOGICAL_MODE_CHANGE = 0x0010000070bL; + public static final long LOGICAL_NEXT_CANDIDATE = 0x0010000070cL; + public static final long LOGICAL_NON_CONVERT = 0x0010000070dL; + public static final long LOGICAL_PREVIOUS_CANDIDATE = 0x0010000070eL; + public static final long LOGICAL_PROCESS = 0x0010000070fL; + public static final long LOGICAL_SINGLE_CANDIDATE = 0x00100000710L; + public static final long LOGICAL_HANGUL_MODE = 0x00100000711L; + public static final long LOGICAL_HANJA_MODE = 0x00100000712L; + public static final long LOGICAL_JUNJA_MODE = 0x00100000713L; + public static final long LOGICAL_EISU = 0x00100000714L; + public static final long LOGICAL_HANKAKU = 0x00100000715L; + public static final long LOGICAL_HIRAGANA = 0x00100000716L; + public static final long LOGICAL_HIRAGANA_KATAKANA = 0x00100000717L; + public static final long LOGICAL_KANA_MODE = 0x00100000718L; + public static final long LOGICAL_KANJI_MODE = 0x00100000719L; + public static final long LOGICAL_KATAKANA = 0x0010000071aL; + public static final long LOGICAL_ROMAJI = 0x0010000071bL; + public static final long LOGICAL_ZENKAKU = 0x0010000071cL; + public static final long LOGICAL_ZENKAKU_HANKAKU = 0x0010000071dL; + public static final long LOGICAL_F1 = 0x00100000801L; + public static final long LOGICAL_F2 = 0x00100000802L; + public static final long LOGICAL_F3 = 0x00100000803L; + public static final long LOGICAL_F4 = 0x00100000804L; + public static final long LOGICAL_F5 = 0x00100000805L; + public static final long LOGICAL_F6 = 0x00100000806L; + public static final long LOGICAL_F7 = 0x00100000807L; + public static final long LOGICAL_F8 = 0x00100000808L; + public static final long LOGICAL_F9 = 0x00100000809L; + public static final long LOGICAL_F10 = 0x0010000080aL; + public static final long LOGICAL_F11 = 0x0010000080bL; + public static final long LOGICAL_F12 = 0x0010000080cL; + public static final long LOGICAL_F13 = 0x0010000080dL; + public static final long LOGICAL_F14 = 0x0010000080eL; + public static final long LOGICAL_F15 = 0x0010000080fL; + public static final long LOGICAL_F16 = 0x00100000810L; + public static final long LOGICAL_F17 = 0x00100000811L; + public static final long LOGICAL_F18 = 0x00100000812L; + public static final long LOGICAL_F19 = 0x00100000813L; + public static final long LOGICAL_F20 = 0x00100000814L; + public static final long LOGICAL_F21 = 0x00100000815L; + public static final long LOGICAL_F22 = 0x00100000816L; + public static final long LOGICAL_F23 = 0x00100000817L; + public static final long LOGICAL_F24 = 0x00100000818L; + public static final long LOGICAL_SOFT1 = 0x00100000901L; + public static final long LOGICAL_SOFT2 = 0x00100000902L; + public static final long LOGICAL_SOFT3 = 0x00100000903L; + public static final long LOGICAL_SOFT4 = 0x00100000904L; + public static final long LOGICAL_SOFT5 = 0x00100000905L; + public static final long LOGICAL_SOFT6 = 0x00100000906L; + public static final long LOGICAL_SOFT7 = 0x00100000907L; + public static final long LOGICAL_SOFT8 = 0x00100000908L; + public static final long LOGICAL_CLOSE = 0x00100000a01L; + public static final long LOGICAL_MAIL_FORWARD = 0x00100000a02L; + public static final long LOGICAL_MAIL_REPLY = 0x00100000a03L; + public static final long LOGICAL_MAIL_SEND = 0x00100000a04L; + public static final long LOGICAL_MEDIA_PLAY_PAUSE = 0x00100000a05L; + public static final long LOGICAL_MEDIA_STOP = 0x00100000a07L; + public static final long LOGICAL_MEDIA_TRACK_NEXT = 0x00100000a08L; + public static final long LOGICAL_MEDIA_TRACK_PREVIOUS = 0x00100000a09L; + public static final long LOGICAL_NEW_KEY = 0x00100000a0aL; + public static final long LOGICAL_OPEN = 0x00100000a0bL; + public static final long LOGICAL_PRINT = 0x00100000a0cL; + public static final long LOGICAL_SAVE = 0x00100000a0dL; + public static final long LOGICAL_SPELL_CHECK = 0x00100000a0eL; + public static final long LOGICAL_AUDIO_VOLUME_DOWN = 0x00100000a0fL; + public static final long LOGICAL_AUDIO_VOLUME_UP = 0x00100000a10L; + public static final long LOGICAL_AUDIO_VOLUME_MUTE = 0x00100000a11L; + public static final long LOGICAL_LAUNCH_APPLICATION2 = 0x00100000b01L; + public static final long LOGICAL_LAUNCH_CALENDAR = 0x00100000b02L; + public static final long LOGICAL_LAUNCH_MAIL = 0x00100000b03L; + public static final long LOGICAL_LAUNCH_MEDIA_PLAYER = 0x00100000b04L; + public static final long LOGICAL_LAUNCH_MUSIC_PLAYER = 0x00100000b05L; + public static final long LOGICAL_LAUNCH_APPLICATION1 = 0x00100000b06L; + public static final long LOGICAL_LAUNCH_SCREEN_SAVER = 0x00100000b07L; + public static final long LOGICAL_LAUNCH_SPREADSHEET = 0x00100000b08L; + public static final long LOGICAL_LAUNCH_WEB_BROWSER = 0x00100000b09L; + public static final long LOGICAL_LAUNCH_WEB_CAM = 0x00100000b0aL; + public static final long LOGICAL_LAUNCH_WORD_PROCESSOR = 0x00100000b0bL; + public static final long LOGICAL_LAUNCH_CONTACTS = 0x00100000b0cL; + public static final long LOGICAL_LAUNCH_PHONE = 0x00100000b0dL; + public static final long LOGICAL_LAUNCH_ASSISTANT = 0x00100000b0eL; + public static final long LOGICAL_LAUNCH_CONTROL_PANEL = 0x00100000b0fL; + public static final long LOGICAL_BROWSER_BACK = 0x00100000c01L; + public static final long LOGICAL_BROWSER_FAVORITES = 0x00100000c02L; + public static final long LOGICAL_BROWSER_FORWARD = 0x00100000c03L; + public static final long LOGICAL_BROWSER_HOME = 0x00100000c04L; + public static final long LOGICAL_BROWSER_REFRESH = 0x00100000c05L; + public static final long LOGICAL_BROWSER_SEARCH = 0x00100000c06L; + public static final long LOGICAL_BROWSER_STOP = 0x00100000c07L; + public static final long LOGICAL_AUDIO_BALANCE_LEFT = 0x00100000d01L; + public static final long LOGICAL_AUDIO_BALANCE_RIGHT = 0x00100000d02L; + public static final long LOGICAL_AUDIO_BASS_BOOST_DOWN = 0x00100000d03L; + public static final long LOGICAL_AUDIO_BASS_BOOST_UP = 0x00100000d04L; + public static final long LOGICAL_AUDIO_FADER_FRONT = 0x00100000d05L; + public static final long LOGICAL_AUDIO_FADER_REAR = 0x00100000d06L; + public static final long LOGICAL_AUDIO_SURROUND_MODE_NEXT = 0x00100000d07L; + public static final long LOGICAL_AVR_INPUT = 0x00100000d08L; + public static final long LOGICAL_AVR_POWER = 0x00100000d09L; + public static final long LOGICAL_CHANNEL_DOWN = 0x00100000d0aL; + public static final long LOGICAL_CHANNEL_UP = 0x00100000d0bL; + public static final long LOGICAL_COLOR_F0_RED = 0x00100000d0cL; + public static final long LOGICAL_COLOR_F1_GREEN = 0x00100000d0dL; + public static final long LOGICAL_COLOR_F2_YELLOW = 0x00100000d0eL; + public static final long LOGICAL_COLOR_F3_BLUE = 0x00100000d0fL; + public static final long LOGICAL_COLOR_F4_GREY = 0x00100000d10L; + public static final long LOGICAL_COLOR_F5_BROWN = 0x00100000d11L; + public static final long LOGICAL_CLOSED_CAPTION_TOGGLE = 0x00100000d12L; + public static final long LOGICAL_DIMMER = 0x00100000d13L; + public static final long LOGICAL_DISPLAY_SWAP = 0x00100000d14L; + public static final long LOGICAL_EXIT = 0x00100000d15L; + public static final long LOGICAL_FAVORITE_CLEAR0 = 0x00100000d16L; + public static final long LOGICAL_FAVORITE_CLEAR1 = 0x00100000d17L; + public static final long LOGICAL_FAVORITE_CLEAR2 = 0x00100000d18L; + public static final long LOGICAL_FAVORITE_CLEAR3 = 0x00100000d19L; + public static final long LOGICAL_FAVORITE_RECALL0 = 0x00100000d1aL; + public static final long LOGICAL_FAVORITE_RECALL1 = 0x00100000d1bL; + public static final long LOGICAL_FAVORITE_RECALL2 = 0x00100000d1cL; + public static final long LOGICAL_FAVORITE_RECALL3 = 0x00100000d1dL; + public static final long LOGICAL_FAVORITE_STORE0 = 0x00100000d1eL; + public static final long LOGICAL_FAVORITE_STORE1 = 0x00100000d1fL; + public static final long LOGICAL_FAVORITE_STORE2 = 0x00100000d20L; + public static final long LOGICAL_FAVORITE_STORE3 = 0x00100000d21L; + public static final long LOGICAL_GUIDE = 0x00100000d22L; + public static final long LOGICAL_GUIDE_NEXT_DAY = 0x00100000d23L; + public static final long LOGICAL_GUIDE_PREVIOUS_DAY = 0x00100000d24L; + public static final long LOGICAL_INFO = 0x00100000d25L; + public static final long LOGICAL_INSTANT_REPLAY = 0x00100000d26L; + public static final long LOGICAL_LINK = 0x00100000d27L; + public static final long LOGICAL_LIST_PROGRAM = 0x00100000d28L; + public static final long LOGICAL_LIVE_CONTENT = 0x00100000d29L; + public static final long LOGICAL_LOCK = 0x00100000d2aL; + public static final long LOGICAL_MEDIA_APPS = 0x00100000d2bL; + public static final long LOGICAL_MEDIA_FAST_FORWARD = 0x00100000d2cL; + public static final long LOGICAL_MEDIA_LAST = 0x00100000d2dL; + public static final long LOGICAL_MEDIA_PAUSE = 0x00100000d2eL; + public static final long LOGICAL_MEDIA_PLAY = 0x00100000d2fL; + public static final long LOGICAL_MEDIA_RECORD = 0x00100000d30L; + public static final long LOGICAL_MEDIA_REWIND = 0x00100000d31L; + public static final long LOGICAL_MEDIA_SKIP = 0x00100000d32L; + public static final long LOGICAL_NEXT_FAVORITE_CHANNEL = 0x00100000d33L; + public static final long LOGICAL_NEXT_USER_PROFILE = 0x00100000d34L; + public static final long LOGICAL_ON_DEMAND = 0x00100000d35L; + public static final long LOGICAL_P_IN_P_DOWN = 0x00100000d36L; + public static final long LOGICAL_P_IN_P_MOVE = 0x00100000d37L; + public static final long LOGICAL_P_IN_P_TOGGLE = 0x00100000d38L; + public static final long LOGICAL_P_IN_P_UP = 0x00100000d39L; + public static final long LOGICAL_PLAY_SPEED_DOWN = 0x00100000d3aL; + public static final long LOGICAL_PLAY_SPEED_RESET = 0x00100000d3bL; + public static final long LOGICAL_PLAY_SPEED_UP = 0x00100000d3cL; + public static final long LOGICAL_RANDOM_TOGGLE = 0x00100000d3dL; + public static final long LOGICAL_RC_LOW_BATTERY = 0x00100000d3eL; + public static final long LOGICAL_RECORD_SPEED_NEXT = 0x00100000d3fL; + public static final long LOGICAL_RF_BYPASS = 0x00100000d40L; + public static final long LOGICAL_SCAN_CHANNELS_TOGGLE = 0x00100000d41L; + public static final long LOGICAL_SCREEN_MODE_NEXT = 0x00100000d42L; + public static final long LOGICAL_SETTINGS = 0x00100000d43L; + public static final long LOGICAL_SPLIT_SCREEN_TOGGLE = 0x00100000d44L; + public static final long LOGICAL_STB_INPUT = 0x00100000d45L; + public static final long LOGICAL_STB_POWER = 0x00100000d46L; + public static final long LOGICAL_SUBTITLE = 0x00100000d47L; + public static final long LOGICAL_TELETEXT = 0x00100000d48L; + public static final long LOGICAL_TV = 0x00100000d49L; + public static final long LOGICAL_TV_INPUT = 0x00100000d4aL; + public static final long LOGICAL_TV_POWER = 0x00100000d4bL; + public static final long LOGICAL_VIDEO_MODE_NEXT = 0x00100000d4cL; + public static final long LOGICAL_WINK = 0x00100000d4dL; + public static final long LOGICAL_ZOOM_TOGGLE = 0x00100000d4eL; + public static final long LOGICAL_DVR = 0x00100000d4fL; + public static final long LOGICAL_MEDIA_AUDIO_TRACK = 0x00100000d50L; + public static final long LOGICAL_MEDIA_SKIP_BACKWARD = 0x00100000d51L; + public static final long LOGICAL_MEDIA_SKIP_FORWARD = 0x00100000d52L; + public static final long LOGICAL_MEDIA_STEP_BACKWARD = 0x00100000d53L; + public static final long LOGICAL_MEDIA_STEP_FORWARD = 0x00100000d54L; + public static final long LOGICAL_MEDIA_TOP_MENU = 0x00100000d55L; + public static final long LOGICAL_NAVIGATE_IN = 0x00100000d56L; + public static final long LOGICAL_NAVIGATE_NEXT = 0x00100000d57L; + public static final long LOGICAL_NAVIGATE_OUT = 0x00100000d58L; + public static final long LOGICAL_NAVIGATE_PREVIOUS = 0x00100000d59L; + public static final long LOGICAL_PAIRING = 0x00100000d5aL; + public static final long LOGICAL_MEDIA_CLOSE = 0x00100000d5bL; + public static final long LOGICAL_AUDIO_BASS_BOOST_TOGGLE = 0x00100000e02L; + public static final long LOGICAL_AUDIO_TREBLE_DOWN = 0x00100000e04L; + public static final long LOGICAL_AUDIO_TREBLE_UP = 0x00100000e05L; + public static final long LOGICAL_MICROPHONE_TOGGLE = 0x00100000e06L; + public static final long LOGICAL_MICROPHONE_VOLUME_DOWN = 0x00100000e07L; + public static final long LOGICAL_MICROPHONE_VOLUME_UP = 0x00100000e08L; + public static final long LOGICAL_MICROPHONE_VOLUME_MUTE = 0x00100000e09L; + public static final long LOGICAL_SPEECH_CORRECTION_LIST = 0x00100000f01L; + public static final long LOGICAL_SPEECH_INPUT_TOGGLE = 0x00100000f02L; + public static final long LOGICAL_APP_SWITCH = 0x00100001001L; + public static final long LOGICAL_CALL = 0x00100001002L; + public static final long LOGICAL_CAMERA_FOCUS = 0x00100001003L; + public static final long LOGICAL_END_CALL = 0x00100001004L; + public static final long LOGICAL_GO_BACK = 0x00100001005L; + public static final long LOGICAL_GO_HOME = 0x00100001006L; + public static final long LOGICAL_HEADSET_HOOK = 0x00100001007L; + public static final long LOGICAL_LAST_NUMBER_REDIAL = 0x00100001008L; + public static final long LOGICAL_NOTIFICATION = 0x00100001009L; + public static final long LOGICAL_MANNER_MODE = 0x0010000100aL; + public static final long LOGICAL_VOICE_DIAL = 0x0010000100bL; + public static final long LOGICAL_TV3_D_MODE = 0x00100001101L; + public static final long LOGICAL_TV_ANTENNA_CABLE = 0x00100001102L; + public static final long LOGICAL_TV_AUDIO_DESCRIPTION = 0x00100001103L; + public static final long LOGICAL_TV_AUDIO_DESCRIPTION_MIX_DOWN = 0x00100001104L; + public static final long LOGICAL_TV_AUDIO_DESCRIPTION_MIX_UP = 0x00100001105L; + public static final long LOGICAL_TV_CONTENTS_MENU = 0x00100001106L; + public static final long LOGICAL_TV_DATA_SERVICE = 0x00100001107L; + public static final long LOGICAL_TV_INPUT_COMPONENT1 = 0x00100001108L; + public static final long LOGICAL_TV_INPUT_COMPONENT2 = 0x00100001109L; + public static final long LOGICAL_TV_INPUT_COMPOSITE1 = 0x0010000110aL; + public static final long LOGICAL_TV_INPUT_COMPOSITE2 = 0x0010000110bL; + public static final long LOGICAL_TV_INPUT_HD_M1 = 0x0010000110cL; + public static final long LOGICAL_TV_INPUT_HD_M2 = 0x0010000110dL; + public static final long LOGICAL_TV_INPUT_HD_M3 = 0x0010000110eL; + public static final long LOGICAL_TV_INPUT_HD_M4 = 0x0010000110fL; + public static final long LOGICAL_TV_INPUT_V_G1 = 0x00100001110L; + public static final long LOGICAL_TV_MEDIA_CONTEXT = 0x00100001111L; + public static final long LOGICAL_TV_NETWORK = 0x00100001112L; + public static final long LOGICAL_TV_NUMBER_ENTRY = 0x00100001113L; + public static final long LOGICAL_TV_RADIO_SERVICE = 0x00100001114L; + public static final long LOGICAL_TV_SATELLITE = 0x00100001115L; + public static final long LOGICAL_TV_SATELLITE_B_S = 0x00100001116L; + public static final long LOGICAL_TV_SATELLITE_C_S = 0x00100001117L; + public static final long LOGICAL_TV_SATELLITE_TOGGLE = 0x00100001118L; + public static final long LOGICAL_TV_TERRESTRIAL_ANALOG = 0x00100001119L; + public static final long LOGICAL_TV_TERRESTRIAL_DIGITAL = 0x0010000111aL; + public static final long LOGICAL_TV_TIMER = 0x0010000111bL; + public static final long LOGICAL_KEY11 = 0x00100001201L; + public static final long LOGICAL_KEY12 = 0x00100001202L; + public static final long LOGICAL_SUSPEND = 0x00200000000L; + public static final long LOGICAL_RESUME = 0x00200000001L; + public static final long LOGICAL_SLEEP = 0x00200000002L; + public static final long LOGICAL_ABORT = 0x00200000003L; + public static final long LOGICAL_LANG1 = 0x00200000010L; + public static final long LOGICAL_LANG2 = 0x00200000011L; + public static final long LOGICAL_LANG3 = 0x00200000012L; + public static final long LOGICAL_LANG4 = 0x00200000013L; + public static final long LOGICAL_LANG5 = 0x00200000014L; + public static final long LOGICAL_INTL_BACKSLASH = 0x00200000020L; + public static final long LOGICAL_INTL_RO = 0x00200000021L; + public static final long LOGICAL_INTL_YEN = 0x00200000022L; + public static final long LOGICAL_CONTROL_LEFT = 0x00200000100L; + public static final long LOGICAL_CONTROL_RIGHT = 0x00200000101L; + public static final long LOGICAL_SHIFT_LEFT = 0x00200000102L; + public static final long LOGICAL_SHIFT_RIGHT = 0x00200000103L; + public static final long LOGICAL_ALT_LEFT = 0x00200000104L; + public static final long LOGICAL_ALT_RIGHT = 0x00200000105L; + public static final long LOGICAL_META_LEFT = 0x00200000106L; + public static final long LOGICAL_META_RIGHT = 0x00200000107L; + public static final long LOGICAL_CONTROL = 0x002000001f0L; + public static final long LOGICAL_SHIFT = 0x002000001f2L; + public static final long LOGICAL_ALT = 0x002000001f4L; + public static final long LOGICAL_META = 0x002000001f6L; + public static final long LOGICAL_NUMPAD_ENTER = 0x0020000020dL; + public static final long LOGICAL_NUMPAD_PAREN_LEFT = 0x00200000228L; + public static final long LOGICAL_NUMPAD_PAREN_RIGHT = 0x00200000229L; + public static final long LOGICAL_NUMPAD_MULTIPLY = 0x0020000022aL; + public static final long LOGICAL_NUMPAD_ADD = 0x0020000022bL; + public static final long LOGICAL_NUMPAD_COMMA = 0x0020000022cL; + public static final long LOGICAL_NUMPAD_SUBTRACT = 0x0020000022dL; + public static final long LOGICAL_NUMPAD_DECIMAL = 0x0020000022eL; + public static final long LOGICAL_NUMPAD_DIVIDE = 0x0020000022fL; + public static final long LOGICAL_NUMPAD0 = 0x00200000230L; + public static final long LOGICAL_NUMPAD1 = 0x00200000231L; + public static final long LOGICAL_NUMPAD2 = 0x00200000232L; + public static final long LOGICAL_NUMPAD3 = 0x00200000233L; + public static final long LOGICAL_NUMPAD4 = 0x00200000234L; + public static final long LOGICAL_NUMPAD5 = 0x00200000235L; + public static final long LOGICAL_NUMPAD6 = 0x00200000236L; + public static final long LOGICAL_NUMPAD7 = 0x00200000237L; + public static final long LOGICAL_NUMPAD8 = 0x00200000238L; + public static final long LOGICAL_NUMPAD9 = 0x00200000239L; + public static final long LOGICAL_NUMPAD_EQUAL = 0x0020000023dL; + public static final long LOGICAL_GAME_BUTTON1 = 0x00200000301L; + public static final long LOGICAL_GAME_BUTTON2 = 0x00200000302L; + public static final long LOGICAL_GAME_BUTTON3 = 0x00200000303L; + public static final long LOGICAL_GAME_BUTTON4 = 0x00200000304L; + public static final long LOGICAL_GAME_BUTTON5 = 0x00200000305L; + public static final long LOGICAL_GAME_BUTTON6 = 0x00200000306L; + public static final long LOGICAL_GAME_BUTTON7 = 0x00200000307L; + public static final long LOGICAL_GAME_BUTTON8 = 0x00200000308L; + public static final long LOGICAL_GAME_BUTTON9 = 0x00200000309L; + public static final long LOGICAL_GAME_BUTTON10 = 0x0020000030aL; + public static final long LOGICAL_GAME_BUTTON11 = 0x0020000030bL; + public static final long LOGICAL_GAME_BUTTON12 = 0x0020000030cL; + public static final long LOGICAL_GAME_BUTTON13 = 0x0020000030dL; + public static final long LOGICAL_GAME_BUTTON14 = 0x0020000030eL; + public static final long LOGICAL_GAME_BUTTON15 = 0x0020000030fL; + public static final long LOGICAL_GAME_BUTTON16 = 0x00200000310L; + public static final long LOGICAL_GAME_BUTTON_A = 0x00200000311L; + public static final long LOGICAL_GAME_BUTTON_B = 0x00200000312L; + public static final long LOGICAL_GAME_BUTTON_C = 0x00200000313L; + public static final long LOGICAL_GAME_BUTTON_LEFT1 = 0x00200000314L; + public static final long LOGICAL_GAME_BUTTON_LEFT2 = 0x00200000315L; + public static final long LOGICAL_GAME_BUTTON_MODE = 0x00200000316L; + public static final long LOGICAL_GAME_BUTTON_RIGHT1 = 0x00200000317L; + public static final long LOGICAL_GAME_BUTTON_RIGHT2 = 0x00200000318L; + public static final long LOGICAL_GAME_BUTTON_SELECT = 0x00200000319L; + public static final long LOGICAL_GAME_BUTTON_START = 0x0020000031aL; + public static final long LOGICAL_GAME_BUTTON_THUMB_LEFT = 0x0020000031bL; + public static final long LOGICAL_GAME_BUTTON_THUMB_RIGHT = 0x0020000031cL; + public static final long LOGICAL_GAME_BUTTON_X = 0x0020000031dL; + public static final long LOGICAL_GAME_BUTTON_Y = 0x0020000031eL; + public static final long LOGICAL_GAME_BUTTON_Z = 0x0020000031fL; +} diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 774cc32558824..759f649f3c64b 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -78,6 +78,8 @@ const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1; // - lib/ui/platform_dispatcher.dart, _kFlutterKeyDataChannel // - shell/platform/darwin/ios/framework/Source/FlutterEngine.mm, // FlutterKeyDataChannel +// - io/flutter/embedding/android/KeyData.java, +// CHANNEL // // Not to be confused with "flutter/keyevent", which is used to send raw // key event data in a platform-dependent format.