From 5725c76b4f9280c15e82f966ac05bf66f70fc733 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 4 May 2022 04:55:07 -0700 Subject: [PATCH 01/26] Impl --- ci/licenses_golden/licenses_flutter | 3 + shell/platform/android/BUILD.gn | 3 + .../io/flutter/embedding/android/KeyData.java | 81 +++ .../android/KeyEmbedderResponder.java | 219 ++++++++ .../embedding/android/KeyboardManager.java | 3 +- .../embedding/android/KeyboardMap.java | 524 ++++++++++++++++++ 6 files changed, 832 insertions(+), 1 deletion(-) 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 diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c80c2529d2507..c5851bf23bc58 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1325,7 +1325,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/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/KeyData.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/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 204a5b3355e09..c7f0b6827ef30 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -177,7 +177,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/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java new file mode 100644 index 0000000000000..484bfff17285e --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -0,0 +1,81 @@ +// 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 java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.io.UnsupportedEncodingException; + +/** + * 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 KeyData { + private static final String TAG = "KeyData"; + + private static final int FIELD_COUNT = 5; + private static final int BYTES_PER_FIELD = 8; + + public enum Type { + kDown(0), + kUp(1), + kRepeat(2); + + private int value; + private Type(int value) { + this.value = value; + } + public int getValue() { + return value; + } + } + + public KeyData() {} + + long timestamp; + Type type; + long physicalKey; + long logicalKey; + boolean synthesized; + + // Nullable + Character character; + + ByteBuffer toBytes() { + byte[] charUtf8 = null; + if (character != null) { + final String charStr = "" + character; + try { + charUtf8 = charStr.getBytes("UTF8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("Decoding error"); + } + } + final int charSize = charUtf8 == null ? 0 : charUtf8.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 (charUtf8 != null) { + packet.put(charUtf8); + } + + // Verify that the packet is the expected size. + if (packet.position() != packet.capacity()) { + throw new AssertionError("Packet is not filled"); + } + + packet.rewind(); + 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..3f06a377768e0 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -0,0 +1,219 @@ +// 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.KeyCharacterMap; +import android.view.KeyEvent; +import androidx.annotation.NonNull; +import java.util.HashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import io.flutter.plugin.common.BinaryMessenger; + +/** + * 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"; + + // TODO(dkwingsmt): Doc + public static final String CHANNEL = "flutter/keydata"; + + // TODO(dkwingsmt): put the constants to key map. + private static final Long kValueMask = 0x000ffffffffl; + private static final Long kUnicodePlane = 0x00000000000l; + private static final Long kAndroidPlane = 0x01100000000l; + + private final HashMap pressingRecords = new HashMap(); + private BinaryMessenger messenger; + + public KeyEmbedderResponder(BinaryMessenger messenger) { + this.messenger = messenger; + } + + private int combiningCharacter; + // TODO(dkwingsmt): Deduplicate this function from KeyChannelResponder.java. + /** + * 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: + * + *

+ * + *

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; + } + + private Long getPhysicalKey(@NonNull KeyEvent event) { + final Long byMapping = KeyboardMap.scanCodeToPhysical.get(event.getScanCode()); + if (byMapping != null) { + return byMapping; + } + // TODO(dkwingsmt): Logic for D-pad + return kAndroidPlane + event.getScanCode(); + } + + private Long getLogicalKey(@NonNull KeyEvent event) { + // TODO(dkwingsmt): Better logic. + final Long byMapping = KeyboardMap.keyCodeToLogical.get(event.getKeyCode()); + if (byMapping != null) { + return byMapping; + } + return kAndroidPlane + event.getKeyCode(); + } + + void updatePressingState(Long physicalKey, Long logicalKey) { + if (logicalKey != 0) { + 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"); + } + } + + // Return: if any events has been sent + private boolean handleEventImpl( + @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + final Long physicalKey = getPhysicalKey(event); + final Long logicalKey = getLogicalKey(event); + final long timestamp = event.getEventTime(); + final Long lastLogicalRecord = pressingRecords.get(physicalKey); +// final boolean isRepeat = event.getRepeatCount() > 0; + + boolean isDown = false; + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + isDown = true; + break; + case KeyEvent.ACTION_UP: + isDown = false; + break; + case KeyEvent.ACTION_MULTIPLE: + // This action type has been deprecated in API level 29, and we don't know how to process it. + return false; + } + + Character character = 0; + + // TODO(dkwingsmt): repeat logic + KeyData.Type type; + if (isDown) { + if (lastLogicalRecord == null) { + type = KeyData.Type.kDown; + } else { + // A key has been pressed that has the exact physical key as a currently + // pressed one. This can happen during repeated events. + type = KeyData.Type.kRepeat; + } + character = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); + } else { // is_down_event false + if (lastLogicalRecord == null) { + // The physical key has been released before. It might indicate a missed + // event due to loss of focus, or multiple keyboards pressed keys with the + // same physical key. Ignore the up event. + return false; + } else { + type = KeyData.Type.kUp; + } + } + + if (type != KeyData.Type.kRepeat) { + updatePressingState(physicalKey, isDown ? logicalKey : 0); + } + + final KeyData output = new KeyData(); + output.timestamp = timestamp; + 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; + sendKeyEvent(output, null); + } + + private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHandledCallback) { + final BinaryMessenger.BinaryReply handleMessageReply = onKeyEventHandledCallback == null ? null : message -> { + Boolean handled = false; + if (message.capacity() == 0) { + handled = message.get() != 0; + } + onKeyEventHandledCallback.onKeyEventHandled(handled); + }; + + messenger.send(CHANNEL, data.toBytes(), handleMessageReply); + } + + @Override + public void handleEvent( + @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + final boolean sentAny = handleEventImpl(event, onKeyEventHandledCallback); + if (!sentAny) { + onKeyEventHandledCallback.onKeyEventHandled(true); + synthesizeEvent(true, 0l, 0l, 0l); + } + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 38bbb7c985d8f..c1ddf7ea8ea38 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -51,7 +51,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..4124dfd6df9fe --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -0,0 +1,524 @@ +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 java.util.HashMap; + +public class KeyboardMap { + 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 + } + }; + + 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 + } + }; +} From 177a50a3e0f49b4c0201746b6335b21b9b9345ad Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 9 May 2022 23:17:46 -0700 Subject: [PATCH 02/26] First test passed --- .../io/flutter/embedding/android/KeyData.java | 73 +++++-- .../android/KeyEmbedderResponder.java | 52 +++-- .../android/KeyboardManagerTest.java | 191 +++++++++++++----- .../test/io/flutter/util/FakeKeyEvent.java | 24 ++- 4 files changed, 245 insertions(+), 95 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java index 484bfff17285e..b97d0291563ae 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyData.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -4,19 +4,25 @@ package io.flutter.embedding.android; +import androidx.annotation.NonNull; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.io.UnsupportedEncodingException; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; /** - * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending processed - * information in {@link KeyData}. + * 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 KeyData { private static final String TAG = "KeyData"; + // TODO(dkwingsmt): Doc + 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; @@ -25,37 +31,64 @@ public enum Type { kUp(1), kRepeat(2); - private int value; - private Type(int value) { + private long value; + + private Type(long value) { this.value = value; } - public int getValue() { + + 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"); + } + } } public KeyData() {} + 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())); + if (charSize != 0) { + final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer); + this.character = charBuffer.toString(); + } + } + long timestamp; Type type; long physicalKey; long logicalKey; boolean synthesized; - // Nullable - Character character; + // Nullable bytes of characters encoded in UTF8. + String character; ByteBuffer toBytes() { - byte[] charUtf8 = null; - if (character != null) { - final String charStr = "" + character; - try { - charUtf8 = charStr.getBytes("UTF8"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError("Decoding error"); - } - } - final int charSize = charUtf8 == null ? 0 : charUtf8.length; + final ByteBuffer charBuffer = + character == null ? null : StandardCharsets.UTF_8.encode(character); + final int charSize = charBuffer == null ? 0 : charBuffer.capacity(); final ByteBuffer packet = ByteBuffer.allocateDirect((1 + FIELD_COUNT) * BYTES_PER_FIELD + charSize); packet.order(ByteOrder.LITTLE_ENDIAN); @@ -66,8 +99,8 @@ ByteBuffer toBytes() { packet.putLong(physicalKey); packet.putLong(logicalKey); packet.putLong(synthesized ? 1l : 0l); - if (charUtf8 != null) { - packet.put(charUtf8); + if (charBuffer != null) { + packet.put(charBuffer); } // Verify that the packet is the expected size. diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 3f06a377768e0..bf5340269a1cc 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -7,23 +7,18 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; -import java.util.HashMap; -import java.util.function.BiConsumer; -import java.util.function.Consumer; 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}. + * 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"; - // TODO(dkwingsmt): Doc - public static final String CHANNEL = "flutter/keydata"; - // TODO(dkwingsmt): put the constants to key map. private static final Long kValueMask = 0x000ffffffffl; private static final Long kUnicodePlane = 0x00000000000l; @@ -93,7 +88,7 @@ Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { } private Long getPhysicalKey(@NonNull KeyEvent event) { - final Long byMapping = KeyboardMap.scanCodeToPhysical.get(event.getScanCode()); + final Long byMapping = KeyboardMap.scanCodeToPhysical.get((long) event.getScanCode()); if (byMapping != null) { return byMapping; } @@ -103,7 +98,7 @@ private Long getPhysicalKey(@NonNull KeyEvent event) { private Long getLogicalKey(@NonNull KeyEvent event) { // TODO(dkwingsmt): Better logic. - final Long byMapping = KeyboardMap.keyCodeToLogical.get(event.getKeyCode()); + final Long byMapping = KeyboardMap.keyCodeToLogical.get((long) event.getKeyCode()); if (byMapping != null) { return byMapping; } @@ -113,12 +108,10 @@ private Long getLogicalKey(@NonNull KeyEvent event) { void updatePressingState(Long physicalKey, Long logicalKey) { if (logicalKey != 0) { final Long previousValue = pressingRecords.put(physicalKey, logicalKey); - if (previousValue != null) - throw new AssertionError("The key was not empty"); + 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"); + if (previousValue == null) throw new AssertionError("The key was empty"); } } @@ -129,7 +122,7 @@ private boolean handleEventImpl( final Long logicalKey = getLogicalKey(event); final long timestamp = event.getEventTime(); final Long lastLogicalRecord = pressingRecords.get(physicalKey); -// final boolean isRepeat = event.getRepeatCount() > 0; + // final boolean isRepeat = event.getRepeatCount() > 0; boolean isDown = false; switch (event.getAction()) { @@ -139,8 +132,7 @@ private boolean handleEventImpl( case KeyEvent.ACTION_UP: isDown = false; break; - case KeyEvent.ACTION_MULTIPLE: - // This action type has been deprecated in API level 29, and we don't know how to process it. + default: return false; } @@ -157,7 +149,7 @@ private boolean handleEventImpl( type = KeyData.Type.kRepeat; } character = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); - } else { // is_down_event false + } else { // is_down_event false if (lastLogicalRecord == null) { // The physical key has been released before. It might indicate a missed // event due to loss of focus, or multiple keyboards pressed keys with the @@ -177,7 +169,7 @@ private boolean handleEventImpl( output.type = type; output.logicalKey = logicalKey; output.physicalKey = physicalKey; - output.character = character; + output.character = "" + character; output.synthesized = false; sendKeyEvent(output, onKeyEventHandledCallback); @@ -196,15 +188,19 @@ private void synthesizeEvent(boolean isDown, Long logicalKey, Long physicalKey, } private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHandledCallback) { - final BinaryMessenger.BinaryReply handleMessageReply = onKeyEventHandledCallback == null ? null : message -> { - Boolean handled = false; - if (message.capacity() == 0) { - handled = message.get() != 0; - } - onKeyEventHandledCallback.onKeyEventHandled(handled); - }; - - messenger.send(CHANNEL, data.toBytes(), handleMessageReply); + 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); } @Override 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..140069c49d64e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -3,6 +3,7 @@ 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.mock; @@ -44,10 +45,17 @@ static enum Type { /** * 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 embedderObject}, with an optional {@link + * reply}. + */ + kEmbedder, } /** @@ -68,33 +76,29 @@ private CallRecord() {} */ public Consumer reply; /** The data for a call record of type {@link Type.kChannel}. */ - public ChannelCallData channelData; + public JSONObject channelObject; + /** The data for a call record of type {@link Type.kEmbedder}. */ + public KeyData embedderObject; /** Construct a call record of type {@link Type.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.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 type {@link Type.kEmbedder}. */ + static CallRecord embedderCall( + @NonNull KeyData embedderObject, @Nullable Consumer reply) { + final CallRecord record = new CallRecord(); + record.type = Type.kEmbedder; + record.embedderObject = embedderObject; + 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 +106,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 +118,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 +139,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)) + doAnswer(invocation -> onMessengerMessage(invocation)) .when(mockMessenger) .send(any(String.class), any(ByteBuffer.class), any(BinaryMessenger.BinaryReply.class)); @@ -161,7 +189,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 +197,69 @@ 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 +274,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 +284,21 @@ static void assertChannelEventEquals( } } + /** Assert that the embedder call is an event that matches the given data. */ + static void assertEmbedderEventEquals( + @NonNull KeyData data, + KeyData.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); + } + @Before public void setUp() { MockitoAnnotations.openMocks(this); @@ -246,7 +318,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,7 +327,7 @@ 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(); @@ -266,7 +338,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(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0, 0x1e, 'a'); + 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).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "a", false); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -281,7 +380,7 @@ public void primaryRespondersHaveTheHighestPrecedence() { } @Test - public void textInputPluginHasTheSecondHighestPrecedence() { + public void textInputHandlesEventsIfNoRespondersDo() { final KeyboardTester tester = new KeyboardTester(); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final ArrayList calls = new ArrayList(); @@ -292,7 +391,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,7 +410,7 @@ 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(); @@ -322,7 +421,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,7 +437,7 @@ public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { } @Test - public void respondsFalseWhenHandlingRedispatchedEvents() { + public void redispatchedEventsAreCorrectlySkipped() { final KeyboardTester tester = new KeyboardTester(); final ArrayList calls = new ArrayList(); @@ -349,7 +448,7 @@ public void respondsFalseWhenHandlingRedispatchedEvents() { 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. diff --git a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java index 75ce7b6ddb302..19f97b7ea0232 100644 --- a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -9,10 +9,32 @@ public FakeKeyEvent(int action, int keyCode) { super(action, keyCode); } + public FakeKeyEvent( + long eventTime, + int action, + int code, + int repeat, + int metaState, + int scancode, + char character) { + super( + /*downTime*/ eventTime, + eventTime, + action, + code, + repeat, + metaState, /*deviceId*/ + 0, + scancode); + this.character = character; + } + + private char character = 0; + public final int getUnicodeChar() { if (getKeyCode() == KeyEvent.KEYCODE_BACK) { return 0; } - return 1; + return character; } } From 16834f39b94d2eb3fbda6fb52ca5bf16a248e8b1 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 10 May 2022 05:07:28 -0700 Subject: [PATCH 03/26] Runnable and more tests --- lib/ui/key.dart | 2 +- .../io/flutter/embedding/android/KeyData.java | 34 ++-- .../android/KeyEmbedderResponder.java | 13 +- .../android/KeyboardManagerTest.java | 163 +++++++++++++++++- 4 files changed, 187 insertions(+), 25 deletions(-) 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/shell/platform/android/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java index b97d0291563ae..e3fc1fa53e534 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyData.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -5,10 +5,9 @@ package io.flutter.embedding.android; import androidx.annotation.NonNull; +import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; /** * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending @@ -70,9 +69,15 @@ public KeyData(@NonNull ByteBuffer buffer) { 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 CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer); - this.character = charBuffer.toString(); + 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"); + } } } @@ -86,9 +91,14 @@ public KeyData(@NonNull ByteBuffer buffer) { String character; ByteBuffer toBytes() { - final ByteBuffer charBuffer = - character == null ? null : StandardCharsets.UTF_8.encode(character); - final int charSize = charBuffer == null ? 0 : charBuffer.capacity(); + 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; + System.out.println(charSize); final ByteBuffer packet = ByteBuffer.allocateDirect((1 + FIELD_COUNT) * BYTES_PER_FIELD + charSize); packet.order(ByteOrder.LITTLE_ENDIAN); @@ -99,16 +109,10 @@ ByteBuffer toBytes() { packet.putLong(physicalKey); packet.putLong(logicalKey); packet.putLong(synthesized ? 1l : 0l); - if (charBuffer != null) { - packet.put(charBuffer); - } - - // Verify that the packet is the expected size. - if (packet.position() != packet.capacity()) { - throw new AssertionError("Packet is not filled"); + if (charBytes != null) { + packet.put(charBytes); } - packet.rewind(); return packet; } } diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index bf5340269a1cc..1221dd4fe3cdf 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -61,6 +61,10 @@ public KeyEmbedderResponder(BinaryMessenger messenger) { * https://en.wikipedia.org/wiki/Combining_character */ Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { + if (newCharacterCodePoint == 0) { + combiningCharacter = 0; + return 0; + } char complexCharacter = (char) newCharacterCodePoint; boolean isNewCodePointACombiningCharacter = (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; @@ -136,7 +140,7 @@ private boolean handleEventImpl( return false; } - Character character = 0; + String character = null; // TODO(dkwingsmt): repeat logic KeyData.Type type; @@ -148,7 +152,10 @@ private boolean handleEventImpl( // pressed one. This can happen during repeated events. type = KeyData.Type.kRepeat; } - character = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); + final char complexChar = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); + if (complexChar != 0) { + character = "" + complexChar; + } } else { // is_down_event false if (lastLogicalRecord == null) { // The physical key has been released before. It might indicate a missed @@ -169,7 +176,7 @@ private boolean handleEventImpl( output.type = type; output.logicalKey = logicalKey; output.physicalKey = physicalKey; - output.character = "" + character; + output.character = character; output.synthesized = false; sendKeyEvent(output, onKeyEventHandledCallback); 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 140069c49d64e..f7c01acc25e74 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -299,6 +299,18 @@ static void assertEmbedderEventEquals( assertEquals(synthesized, data.synthesized); } + /** + * Print each byte of the given buffer as a hex (such as "0a" for 0x0a), and return the + * concatenated string. + */ + 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); @@ -306,11 +318,67 @@ 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 = KeyData.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 = KeyData.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 respondsTrueWhenHandlingNewEvents() { 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); @@ -330,7 +398,7 @@ public void respondsTrueWhenHandlingNewEvents() { public void channelReponderHandlesEvents() { 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); @@ -356,7 +424,7 @@ public void channelReponderHandlesEvents() { public void embedderReponderHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); final KeyEvent keyEvent = new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0, 0x1e, 'a'); - final ArrayList calls = new ArrayList(); + final ArrayList calls = new ArrayList<>(); tester.recordEmbedderCallsTo(calls); @@ -383,7 +451,7 @@ public void embedderReponderHandlesEvents() { public void textInputHandlesEventsIfNoRespondersDo() { 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); @@ -413,7 +481,7 @@ public void textInputHandlesEventsIfNoRespondersDo() { public void redispatchEventsIfTextInputDoesntHandle() { 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); @@ -439,7 +507,7 @@ public void redispatchEventsIfTextInputDoesntHandle() { @Test public void redispatchedEventsAreCorrectlySkipped() { final KeyboardTester tester = new KeyboardTester(); - final ArrayList calls = new ArrayList(); + final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -465,4 +533,87 @@ public void redispatchedEventsAreCorrectlySkipped() { // 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(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0, 0x1e, 'a'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "a", false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 1, 0, 0x1e, 'a'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kRepeat, 0x00070004l, 0x00000000061l, "a", false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x1d, 0, 0, 0x1e, 'a'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kUp, 0x00070004l, 0x00000000061l, 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(100, KeyEvent.ACTION_DOWN, 0x3b, 0, 0x41, 0x2a, '\0'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kDown, 0x000700e1l, 0x200000102l, null, false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0x41, 0x1e, 'A'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "A", false); + calls.clear(); + + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x1d, 0, 0x41, 0x1e, 'A'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kUp, 0x00070004l, 0x00000000061l, null, false); + calls.clear(); + + // ShiftLeft + assertEquals( + true, + tester.keyboardManager.handleEvent( + new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x3b, 0, 0x41, 0x2a, '\0'))); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).embedderObject, KeyData.Type.kUp, 0x000700e1l, 0x200000102l, null, false); + calls.clear(); + } } From 20a21056ff8d2a9881c5e225144bae4ab88df7e2 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Thu, 12 May 2022 03:47:56 -0700 Subject: [PATCH 04/26] Use constants --- .../android/KeyboardManagerTest.java | 78 +- .../test/io/flutter/util/KeyCodes.java | 733 ++++++++++++++++++ 2 files changed, 795 insertions(+), 16 deletions(-) create mode 100644 shell/platform/android/test/io/flutter/util/KeyCodes.java 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 f7c01acc25e74..75c99bfc15545 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -17,6 +17,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMessageCodec; import io.flutter.util.FakeKeyEvent; +import io.flutter.util.KeyCodes; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.function.BiConsumer; @@ -34,6 +35,10 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class KeyboardManagerTest { + static public final int SCAN_KEY_A = 0x1e; + static public final int SCAN_SHIFT_LEFT = 0x2a; + + /** * Records a message that {@link KeyboardManager} sends to outside. * @@ -423,7 +428,8 @@ public void channelReponderHandlesEvents() { @Test public void embedderReponderHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0, 0x1e, 'a'); + final KeyEvent keyEvent = + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'); final ArrayList calls = new ArrayList<>(); tester.recordEmbedderCallsTo(calls); @@ -433,7 +439,12 @@ public void embedderReponderHandlesEvents() { assertEquals(true, result); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "a", false); + calls.get(0).embedderObject, + KeyData.Type.kDown, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + "a", + false); // Don't send the key event to the text plugin if the only primary responder // hasn't responded. @@ -545,28 +556,43 @@ public void tapLowerA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0, 0x1e, 'a'))); + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "a", false); + calls.get(0).embedderObject, + KeyData.Type.kDown, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + "a", + false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 1, 0, 0x1e, 'a'))); + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 1, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kRepeat, 0x00070004l, 0x00000000061l, "a", false); + calls.get(0).embedderObject, + KeyData.Type.kRepeat, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + "a", + false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x1d, 0, 0, 0x1e, 'a'))); + new FakeKeyEvent(100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kUp, 0x00070004l, 0x00000000061l, null, false); + calls.get(0).embedderObject, + KeyData.Type.kUp, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + null, + false); calls.clear(); } @@ -582,38 +608,58 @@ public void tapUpperA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x3b, 0, 0x41, 0x2a, '\0'))); + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kDown, 0x000700e1l, 0x200000102l, null, false); + calls.get(0).embedderObject, + KeyData.Type.kDown, + KeyCodes.PHYSICAL_SHIFT_LEFT, + KeyCodes.LOGICAL_SHIFT_LEFT, + null, + false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x1d, 0, 0x41, 0x1e, 'A'))); + new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kDown, 0x00070004l, 0x00000000061l, "A", false); + calls.get(0).embedderObject, + KeyData.Type.kDown, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + "A", + false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x1d, 0, 0x41, 0x1e, 'A'))); + new FakeKeyEvent(100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kUp, 0x00070004l, 0x00000000061l, null, false); + calls.get(0).embedderObject, + KeyData.Type.kUp, + KeyCodes.PHYSICAL_KEY_A, + KeyCodes.LOGICAL_KEY_A, + null, + false); calls.clear(); // ShiftLeft assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x3b, 0, 0x41, 0x2a, '\0'))); + new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, KeyData.Type.kUp, 0x000700e1l, 0x200000102l, null, false); + calls.get(0).embedderObject, + KeyData.Type.kUp, + KeyCodes.PHYSICAL_SHIFT_LEFT, + KeyCodes.LOGICAL_SHIFT_LEFT, + null, + false); calls.clear(); } } 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..e910ffe1088f7 --- /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; +} From e9a3d8b0a687ef238817e9500af36c64cd3b1e25 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 13 May 2022 02:07:34 -0700 Subject: [PATCH 05/26] static import --- .../android/KeyboardManagerTest.java | 108 ++++++------------ 1 file changed, 35 insertions(+), 73 deletions(-) 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 75c99bfc15545..9e825436c4ab1 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -1,5 +1,7 @@ package io.flutter.embedding.android; +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; @@ -17,7 +19,6 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMessageCodec; import io.flutter.util.FakeKeyEvent; -import io.flutter.util.KeyCodes; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.function.BiConsumer; @@ -35,9 +36,8 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class KeyboardManagerTest { - static public final int SCAN_KEY_A = 0x1e; - static public final int SCAN_SHIFT_LEFT = 0x2a; - + public static final int SCAN_KEY_A = 0x1e; + public static final int SCAN_SHIFT_LEFT = 0x2a; /** * Records a message that {@link KeyboardManager} sends to outside. @@ -46,7 +46,7 @@ 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. * @@ -57,8 +57,7 @@ static enum Type { /** * The embedder responder sent a message through the key data channel. * - *

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

This call record will have a non-null {@link keyData}, with an optional {@link reply}. */ kEmbedder, } @@ -70,7 +69,7 @@ static enum Type { */ private CallRecord() {} - Type type; + Kind kind; /** * The callback given by the keyboard manager. @@ -80,27 +79,26 @@ private CallRecord() {} * continue processing the key event. */ public Consumer reply; - /** The data for a call record of type {@link Type.kChannel}. */ + /** The data for a call record of kind {@link Kind.kChannel}. */ public JSONObject channelObject; - /** The data for a call record of type {@link Type.kEmbedder}. */ - public KeyData embedderObject; + /** 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 JSONObject channelObject, @Nullable Consumer reply) { final CallRecord record = new CallRecord(); - record.type = Type.kChannel; + record.kind = Kind.kChannel; record.channelObject = channelObject; record.reply = reply; return record; } - /** Construct a call record of type {@link Type.kEmbedder}. */ - static CallRecord embedderCall( - @NonNull KeyData embedderObject, @Nullable Consumer reply) { + /** Construct a call record of kind {@link Kind.kEmbedder}. */ + static CallRecord embedderCall(@NonNull KeyData keyData, @Nullable Consumer reply) { final CallRecord record = new CallRecord(); - record.type = Type.kEmbedder; - record.embedderObject = embedderObject; + record.kind = Kind.kEmbedder; + record.keyData = keyData; record.reply = reply; return record; } @@ -292,7 +290,7 @@ static void assertChannelEventEquals( /** Assert that the embedder call is an event that matches the given data. */ static void assertEmbedderEventEquals( @NonNull KeyData data, - KeyData.Type type, + Type type, long physicalKey, long logicalKey, String character, @@ -330,7 +328,7 @@ public void serializeAndDeserializeKeyData() { data1.physicalKey = 0x0a; data1.logicalKey = 0x0b; data1.timestamp = 0x0c; - data1.type = KeyData.Type.kRepeat; + data1.type = Type.kRepeat; data1.character = "A"; data1.synthesized = true; @@ -358,7 +356,7 @@ public void serializeAndDeserializeKeyData() { data2.physicalKey = 0xaaaabbbbccccl; data2.logicalKey = 0x666677778888l; data2.timestamp = 0x333344445555l; - data2.type = KeyData.Type.kUp; + data2.type = Type.kUp; data2.character = null; data2.synthesized = false; @@ -439,12 +437,7 @@ public void embedderReponderHandlesEvents() { assertEquals(true, result); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kDown, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - "a", - false); + 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. @@ -556,29 +549,21 @@ public void tapLowerA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); + new FakeKeyEvent( + 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kDown, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - "a", - false); + calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 1, 0, SCAN_KEY_A, 'a'))); + new FakeKeyEvent( + 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 1, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kRepeat, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - "a", - false); + calls.get(0).keyData, Type.kRepeat, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); calls.clear(); assertEquals( @@ -587,12 +572,7 @@ public void tapLowerA() { new FakeKeyEvent(100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kUp, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - null, - false); + calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); } @@ -611,40 +591,27 @@ public void tapUpperA() { new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kDown, - KeyCodes.PHYSICAL_SHIFT_LEFT, - KeyCodes.LOGICAL_SHIFT_LEFT, - null, - false); + calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); + new FakeKeyEvent( + 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kDown, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - "A", - false); + calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "A", false); calls.clear(); assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); + new FakeKeyEvent( + 100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kUp, - KeyCodes.PHYSICAL_KEY_A, - KeyCodes.LOGICAL_KEY_A, - null, - false); + calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); // ShiftLeft @@ -654,12 +621,7 @@ public void tapUpperA() { new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( - calls.get(0).embedderObject, - KeyData.Type.kUp, - KeyCodes.PHYSICAL_SHIFT_LEFT, - KeyCodes.LOGICAL_SHIFT_LEFT, - null, - false); + calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); } } From 02512cc75c23893f5ee458a59caae571cdef9f27 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 13 May 2022 02:56:11 -0700 Subject: [PATCH 06/26] modifier keys --- .../android/KeyboardManagerTest.java | 138 ++++++++++++++++-- .../test/io/flutter/util/FakeKeyEvent.java | 13 +- 2 files changed, 128 insertions(+), 23 deletions(-) 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 9e825436c4ab1..a2b588a540c1f 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static android.view.KeyEvent.*; import android.view.KeyEvent; import androidx.annotation.NonNull; @@ -38,6 +39,13 @@ public class KeyboardManagerTest { public static final int SCAN_KEY_A = 0x1e; 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_META_LEFT = 0x2a; +// public static final int SCAN_META_RIGHT = 0x36; /** * Records a message that {@link KeyboardManager} sends to outside. @@ -228,9 +236,8 @@ public void respondToEmbedderCallsWith(boolean handled) { */ public void recordEmbedderCallsTo(@NonNull ArrayList storage) { embedderHandler = - (KeyData keyData, Consumer reply) -> { - storage.add(CallRecord.embedderCall(keyData, reply)); - }; + (KeyData keyData, Consumer reply) -> + storage.add(CallRecord.embedderCall(keyData, reply)); } /** Set text calls to respond with the given response. */ @@ -380,7 +387,7 @@ public void serializeAndDeserializeKeyData() { @Test public void respondsTrueWhenHandlingNewEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -400,7 +407,7 @@ public void respondsTrueWhenHandlingNewEvents() { @Test public void channelReponderHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -427,7 +434,7 @@ public void channelReponderHandlesEvents() { public void embedderReponderHandlesEvents() { final KeyboardTester tester = new KeyboardTester(); final KeyEvent keyEvent = - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'); + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0); final ArrayList calls = new ArrayList<>(); tester.recordEmbedderCallsTo(calls); @@ -454,7 +461,7 @@ public void embedderReponderHandlesEvents() { @Test public void textInputHandlesEventsIfNoRespondersDo() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -484,7 +491,7 @@ public void textInputHandlesEventsIfNoRespondersDo() { @Test public void redispatchEventsIfTextInputDoesntHandle() { final KeyboardTester tester = new KeyboardTester(); - final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, 65); final ArrayList calls = new ArrayList<>(); tester.recordChannelCallsTo(calls); @@ -515,7 +522,7 @@ public void redispatchedEventsAreCorrectlySkipped() { 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); @@ -550,7 +557,7 @@ public void tapLowerA() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); + ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); @@ -560,7 +567,7 @@ public void tapLowerA() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 1, 0, SCAN_KEY_A, 'a'))); + ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 1, 'a', 0))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kRepeat, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); @@ -569,7 +576,7 @@ public void tapLowerA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0, SCAN_KEY_A, 'a'))); + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); @@ -588,7 +595,7 @@ public void tapUpperA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_DOWN, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); + new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); @@ -598,7 +605,7 @@ public void tapUpperA() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - 100, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); + ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "A", false); @@ -608,7 +615,7 @@ public void tapUpperA() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - 100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0x41, SCAN_KEY_A, 'A'))); + ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); @@ -618,10 +625,109 @@ public void tapUpperA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(100, KeyEvent.ACTION_UP, 0x3b, 0, 0x41, SCAN_SHIFT_LEFT, '\0'))); + 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(); + } + + @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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); assertEquals(calls.size(), 1); assertEmbedderEventEquals( calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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', 0x3000)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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', 0x3000)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, 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)); + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, Type.kUp, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false); + calls.clear(); } } diff --git a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java index 19f97b7ea0232..954fea6e5e05c 100644 --- a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -10,20 +10,19 @@ public FakeKeyEvent(int action, int keyCode) { } public FakeKeyEvent( - long eventTime, int action, + int scancode, int code, int repeat, - int metaState, - int scancode, - char character) { + char character, + int metaState) { super( - /*downTime*/ eventTime, - eventTime, + 0, + 0, action, code, repeat, - metaState, /*deviceId*/ + metaState, 0, scancode); this.character = character; From 10c7f1e9158d370c68abffa3f841eaf0e5a4f115 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 13 May 2022 03:58:36 -0700 Subject: [PATCH 07/26] nonUsKeys --- .../android/KeyboardManagerTest.java | 185 +++++++++++------- .../test/io/flutter/util/FakeKeyEvent.java | 17 +- 2 files changed, 113 insertions(+), 89 deletions(-) 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 a2b588a540c1f..11cb6ce06f2e4 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -1,5 +1,6 @@ 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; @@ -11,7 +12,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static android.view.KeyEvent.*; import android.view.KeyEvent; import androidx.annotation.NonNull; @@ -38,14 +38,15 @@ @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_META_LEFT = 0x2a; -// public static final int SCAN_META_RIGHT = 0x36; + // public static final int SCAN_META_LEFT = 0x2a; + // public static final int SCAN_META_RIGHT = 0x36; /** * Records a message that {@link KeyboardManager} sends to outside. @@ -309,9 +310,24 @@ static void assertEmbedderEventEquals( assertEquals(synthesized, data.synthesized); } + /** Assert that data has one event, which is an embedder call that matches the given data. */ + static void assertSingleEmbedderEventEquals( + @NonNull ArrayList calls, + Type type, + long physicalKey, + long logicalKey, + String character, + boolean synthesized) { + assertEquals(calls.size(), 1); + assertEmbedderEventEquals( + calls.get(0).keyData, type, physicalKey, logicalKey, character, synthesized); + } + /** * 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()]; @@ -433,8 +449,7 @@ public void channelReponderHandlesEvents() { @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 KeyEvent keyEvent = new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0); final ArrayList calls = new ArrayList<>(); tester.recordEmbedderCallsTo(calls); @@ -556,30 +571,22 @@ public void tapLowerA() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent( - ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0))); + assertSingleEmbedderEventEquals(calls, 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))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kRepeat, PHYSICAL_KEY_A, LOGICAL_KEY_A, "a", false); + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 1, 'a', 0))); + assertSingleEmbedderEventEquals(calls, 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))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); + assertSingleEmbedderEventEquals(calls, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); } @@ -596,29 +603,22 @@ public void tapUpperA() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_KEY_A, LOGICAL_KEY_A, "A", false); + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); + assertSingleEmbedderEventEquals(calls, 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))); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'A', 0x41))); + assertSingleEmbedderEventEquals(calls, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); // ShiftLeft @@ -626,9 +626,8 @@ public void tapUpperA() { 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); + assertSingleEmbedderEventEquals( + calls, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); } @@ -643,91 +642,129 @@ public void modifierKeys() { // ShiftLeft tester.keyboardManager.handleEvent( new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0x41)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0)); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_RIGHT, LOGICAL_SHIFT_RIGHT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_RIGHT, LOGICAL_SHIFT_RIGHT, null, false); + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_RIGHT, KEYCODE_SHIFT_RIGHT, 0, '\0', 0)); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_CONTROL_LEFT, LOGICAL_CONTROL_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_CONTROL_RIGHT, LOGICAL_CONTROL_RIGHT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_CONTROL_RIGHT, LOGICAL_CONTROL_RIGHT, null, false); + assertSingleEmbedderEventEquals( + calls, 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', 0x3000)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, false); + assertSingleEmbedderEventEquals( + calls, 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', 0x3000)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kDown, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false); + assertSingleEmbedderEventEquals( + calls, 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)); - assertEquals(calls.size(), 1); - assertEmbedderEventEquals( - calls.get(0).keyData, Type.kUp, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false); + assertSingleEmbedderEventEquals( + calls, 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', 0x41)); + assertSingleEmbedderEventEquals(calls, Type.kDown, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, "1", false); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '1', 0)); + assertSingleEmbedderEventEquals(calls, 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)); + assertSingleEmbedderEventEquals(calls, Type.kDown, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, "&", false); + calls.clear(); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '&', 0)); + assertSingleEmbedderEventEquals(calls, 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', 0x41)); + calls.clear(); + + // Russian lowerA + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, '\u0444', 0)); + assertSingleEmbedderEventEquals(calls, 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)); + assertSingleEmbedderEventEquals(calls, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); } } diff --git a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java index 954fea6e5e05c..691a7d0cc84bc 100644 --- a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -10,21 +10,8 @@ public FakeKeyEvent(int action, int keyCode) { } public FakeKeyEvent( - int action, - int scancode, - int code, - int repeat, - char character, - int metaState) { - super( - 0, - 0, - action, - code, - repeat, - metaState, - 0, - scancode); + int action, int scancode, int code, int repeat, char character, int metaState) { + super(0, 0, action, code, repeat, metaState, 0, scancode); this.character = character; } From 8199d0b488feef31cd4a1ed002b9714898b2f82a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 13 May 2022 05:34:06 -0700 Subject: [PATCH 08/26] order test, duplicate/abrupt test --- .../io/flutter/embedding/android/KeyData.java | 1 - .../android/KeyEmbedderResponder.java | 19 +++-- .../android/KeyboardManagerTest.java | 73 ++++++++++++++++++- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java index e3fc1fa53e534..1adc66a0216e0 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyData.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -98,7 +98,6 @@ ByteBuffer toBytes() { throw new AssertionError("UTF-8 not supported"); } final int charSize = charBytes == null ? 0 : charBytes.length; - System.out.println(charSize); final ByteBuffer packet = ByteBuffer.allocateDirect((1 + FIELD_COUNT) * BYTES_PER_FIELD + charSize); packet.order(ByteOrder.LITTLE_ENDIAN); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 1221dd4fe3cdf..217f991bfe458 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -126,9 +126,9 @@ private boolean handleEventImpl( final Long logicalKey = getLogicalKey(event); final long timestamp = event.getEventTime(); final Long lastLogicalRecord = pressingRecords.get(physicalKey); - // final boolean isRepeat = event.getRepeatCount() > 0; + final boolean isRepeat = event.getRepeatCount() > 0; - boolean isDown = false; + boolean isDown; switch (event.getAction()) { case KeyEvent.ACTION_DOWN: isDown = true; @@ -149,8 +149,14 @@ private boolean handleEventImpl( type = KeyData.Type.kDown; } else { // A key has been pressed that has the exact physical key as a currently - // pressed one. This can happen during repeated events. - type = KeyData.Type.kRepeat; + // pressed one. + if (isRepeat) { + type = KeyData.Type.kRepeat; + } else { + synthesizeEvent(false, lastLogicalRecord, physicalKey, timestamp); + pressingRecords.remove(physicalKey); + type = KeyData.Type.kDown; + } } final char complexChar = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); if (complexChar != 0) { @@ -159,8 +165,7 @@ private boolean handleEventImpl( } else { // is_down_event false if (lastLogicalRecord == null) { // The physical key has been released before. It might indicate a missed - // event due to loss of focus, or multiple keyboards pressed keys with the - // same physical key. Ignore the up event. + // event due to loss of focus. Ignore the up event. return false; } else { type = KeyData.Type.kUp; @@ -215,8 +220,8 @@ public void handleEvent( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { final boolean sentAny = handleEventImpl(event, onKeyEventHandledCallback); if (!sentAny) { - onKeyEventHandledCallback.onKeyEventHandled(true); synthesizeEvent(true, 0l, 0l, 0l); + onKeyEventHandledCallback.onKeyEventHandled(true); } } } 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 11cb6ce06f2e4..759610d7e75d6 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -172,7 +173,7 @@ public KeyboardTester() { BinaryMessenger mockMessenger = mock(BinaryMessenger.class); doAnswer(invocation -> onMessengerMessage(invocation)) .when(mockMessenger) - .send(any(String.class), any(ByteBuffer.class)); + .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)); @@ -473,6 +474,35 @@ public void embedderReponderHandlesEvents() { verify(tester.mockView, times(0)).redispatch(any(KeyEvent.class)); } + @Test + public void bothRespondersHandlesEvents() { + final KeyboardTester tester = new KeyboardTester(); + 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(); @@ -631,6 +661,47 @@ public void tapUpperA() { 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))); + assertSingleEmbedderEventEquals(calls, 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))); + assertSingleEmbedderEventEquals(calls, Type.kDown, 0l, 0l, null, true); + calls.clear(); + } + @Test public void modifierKeys() { final KeyboardTester tester = new KeyboardTester(); From be9ac1b8c8e4c3683ff0793a4c99871571a7050b Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 16 May 2022 03:56:52 -0700 Subject: [PATCH 09/26] Synthesize pressing keys --- .../android/KeyEmbedderResponder.java | 128 +++++++++++++++--- .../android/KeyboardManagerTest.java | 111 ++++++++++++++- 2 files changed, 214 insertions(+), 25 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 217f991bfe458..3390ae7001de2 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -8,7 +8,11 @@ import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.plugin.common.BinaryMessenger; + +import java.util.ArrayList; import java.util.HashMap; +import java.util.function.Function; +import java.util.function.Supplier; /** * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending @@ -19,16 +23,49 @@ public class KeyEmbedderResponder implements KeyboardManager.Responder { private static final String TAG = "KeyEmbedderResponder"; + private static class SyncGoal { + public SyncGoal(boolean toggling, long physicalKey, long logicalKey, int mask) { + this.physicalKey = physicalKey; + this.logicalKey = logicalKey; + this.toggling = toggling; + this.mask = mask; + } + + public final long physicalKey; + public final long logicalKey; + public final boolean toggling; + public final int mask; + public boolean enabled = false; + } + + 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"); + } + } + // TODO(dkwingsmt): put the constants to key map. private static final Long kValueMask = 0x000ffffffffl; private static final Long kUnicodePlane = 0x00000000000l; private static final Long kAndroidPlane = 0x01100000000l; - private final HashMap pressingRecords = new HashMap(); + private final HashMap pressingRecords = new HashMap<>(); private BinaryMessenger messenger; + private final ArrayList syncGoals = new ArrayList<>(); + public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; + // ShiftLeft + this.syncGoals.add(new SyncGoal(false, 0x00000700e1L, 0x0200000102L, KeyEvent.META_SHIFT_LEFT_ON)); + // CapsLock + this.syncGoals.add(new SyncGoal(true, 0x0000070039L, 0x0100000104L, KeyEvent.META_CAPS_LOCK_ON)); } private int combiningCharacter; @@ -110,7 +147,7 @@ private Long getLogicalKey(@NonNull KeyEvent event) { } void updatePressingState(Long physicalKey, Long logicalKey) { - if (logicalKey != 0) { + if (logicalKey != null) { final Long previousValue = pressingRecords.put(physicalKey, logicalKey); if (previousValue != null) throw new AssertionError("The key was not empty"); } else { @@ -119,42 +156,94 @@ void updatePressingState(Long physicalKey, Long logicalKey) { } } + void synchronizePressingKey(SyncGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { + final boolean nowPressed = pressingRecords.containsKey(goal.physicalKey); + final KeyData.Type type = getEventType(event); + boolean targetPressed = true; + System.out.printf("Sync [ph 0x%x eventPh 0x%x] nowP %d trueP %d type %d\n", goal.physicalKey, eventPhysicalKey, nowPressed ? 1 : 0, truePressed ? 1 : 0, type.getValue()); + if (eventPhysicalKey != goal.physicalKey) { + targetPressed = truePressed; + } else { + // The state if the goal key is the event key: + // + // EventType Down Up + // Pressed 1 0 + switch (type) { + case kDown: + targetPressed = false; + if (!truePressed) { + throw new AssertionError(String.format( + "Unexpected metaState 0 for key 0x%x during an ACTION_DOWN event.", eventPhysicalKey)); + } + break; + case kUp: + // Incoming event is a repeat. 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. + if (truePressed) { + throw new AssertionError(String.format( + "Unexpected metaState 1 for key 0x%x during an ACTION_UP event.", eventPhysicalKey)); + } + return; + 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 sending printable characters. + if (!truePressed) { + throw new AssertionError(String.format( + "Unexpected metaState 0 for key 0x%x during an ACTION_down repeat event.", eventPhysicalKey)); + } + return; + } + } + if (nowPressed != targetPressed) { + synthesizeEvent(targetPressed, goal.logicalKey, goal.physicalKey, event.getEventTime()); + updatePressingState(goal.physicalKey, targetPressed ? goal.logicalKey : null); + } + } + // Return: if any events has been sent private boolean handleEventImpl( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); - final long timestamp = event.getEventTime(); - final Long lastLogicalRecord = pressingRecords.get(physicalKey); - final boolean isRepeat = event.getRepeatCount() > 0; - boolean isDown; + for (final SyncGoal goal : syncGoals) { + final boolean targetOn = (event.getMetaState() & goal.mask) != 0; + System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, targetOn ? 1 : 0); + if (goal.toggling) { +// synchronizeTogglingKey(goal, targetOn, physicalKey, event); + } else { + synchronizePressingKey(goal, targetOn, physicalKey, event); + } + } + + boolean isDownEvent; switch (event.getAction()) { case KeyEvent.ACTION_DOWN: - isDown = true; + isDownEvent = true; break; case KeyEvent.ACTION_UP: - isDown = false; + isDownEvent = false; break; default: return false; } - String character = null; - - // TODO(dkwingsmt): repeat logic KeyData.Type type; - if (isDown) { + 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 (isRepeat) { + if (event.getRepeatCount() > 0) { type = KeyData.Type.kRepeat; } else { - synthesizeEvent(false, lastLogicalRecord, physicalKey, timestamp); - pressingRecords.remove(physicalKey); + synthesizeEvent(false, lastLogicalRecord, physicalKey, event.getEventTime()); + updatePressingState(physicalKey, null); type = KeyData.Type.kDown; } } @@ -162,10 +251,9 @@ private boolean handleEventImpl( if (complexChar != 0) { character = "" + complexChar; } - } else { // is_down_event false + } else { // isDownEvent is false if (lastLogicalRecord == null) { - // The physical key has been released before. It might indicate a missed - // event due to loss of focus. Ignore the up event. + // Ignore abrupt up events. return false; } else { type = KeyData.Type.kUp; @@ -173,11 +261,11 @@ private boolean handleEventImpl( } if (type != KeyData.Type.kRepeat) { - updatePressingState(physicalKey, isDown ? logicalKey : 0); + updatePressingState(physicalKey, isDownEvent ? logicalKey : null); } final KeyData output = new KeyData(); - output.timestamp = timestamp; + output.timestamp = event.getEventTime(); output.type = type; output.logicalKey = logicalKey; output.physicalKey = physicalKey; 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 759610d7e75d6..dfebf9d45cee2 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -8,8 +8,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; 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; @@ -46,6 +46,7 @@ public class KeyboardManagerTest { 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_META_LEFT = 0x2a; // public static final int SCAN_META_RIGHT = 0x36; @@ -483,7 +484,9 @@ public void bothRespondersHandlesEvents() { tester.recordEmbedderCallsTo(calls); tester.respondToTextInputWith(true); - final boolean result = tester.keyboardManager.handleEvent(new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0)); + 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); @@ -681,8 +684,10 @@ public void duplicateDownEventsArePrecededBySynthesizedUpEvents() { 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); + 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(); } @@ -799,7 +804,7 @@ public void nonUsKeys() { // French 1 tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_DIGIT1, KEYCODE_1, 0, '1', 0x41)); + new FakeKeyEvent(ACTION_DOWN, SCAN_DIGIT1, KEYCODE_1, 0, '1', 0)); assertSingleEmbedderEventEquals(calls, Type.kDown, PHYSICAL_DIGIT1, LOGICAL_DIGIT1, "1", false); calls.clear(); @@ -838,4 +843,100 @@ public void nonUsKeys() { assertSingleEmbedderEventEquals(calls, Type.kUp, PHYSICAL_KEY_A, LOGICAL_KEY_A, null, false); calls.clear(); } + + @Test + public void synchronizeShiftLeft() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; + + // Test if ShiftLeft can be synchronized for events of other keys. + + 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 if ShiftLeft can be synchronized for events of this key. Test all 6 cases (3 types x 2 states) + // in the following order to keep the desired current states. + + // Repeat event while 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 while 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 while 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 while 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 while 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 while 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(); + } } From b042d057cec7521f81ee43953e988dcf4eaf500c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 16 May 2022 15:02:00 -0700 Subject: [PATCH 10/26] WIP --- .../io/flutter/embedding/android/KeyEmbedderResponder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 3390ae7001de2..425dad33e3014 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -160,7 +160,7 @@ void synchronizePressingKey(SyncGoal goal, boolean truePressed, long eventPhysic final boolean nowPressed = pressingRecords.containsKey(goal.physicalKey); final KeyData.Type type = getEventType(event); boolean targetPressed = true; - System.out.printf("Sync [ph 0x%x eventPh 0x%x] nowP %d trueP %d type %d\n", goal.physicalKey, eventPhysicalKey, nowPressed ? 1 : 0, truePressed ? 1 : 0, type.getValue()); + // System.out.printf("Sync [ph 0x%x eventPh 0x%x] nowP %d trueP %d type %d\n", goal.physicalKey, eventPhysicalKey, nowPressed ? 1 : 0, truePressed ? 1 : 0, type.getValue()); if (eventPhysicalKey != goal.physicalKey) { targetPressed = truePressed; } else { @@ -210,7 +210,7 @@ private boolean handleEventImpl( for (final SyncGoal goal : syncGoals) { final boolean targetOn = (event.getMetaState() & goal.mask) != 0; - System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, targetOn ? 1 : 0); + // System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, targetOn ? 1 : 0); if (goal.toggling) { // synchronizeTogglingKey(goal, targetOn, physicalKey, event); } else { From 1824806287319e5368fc25f383eefc8172c606eb Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 16 May 2022 17:18:41 -0700 Subject: [PATCH 11/26] New sync algorithm --- .../android/KeyEmbedderResponder.java | 184 +++++++++++------- .../android/KeyboardManagerTest.java | 25 ++- 2 files changed, 134 insertions(+), 75 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 425dad33e3014..0616fa527baea 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -8,11 +8,8 @@ import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.plugin.common.BinaryMessenger; - import java.util.ArrayList; import java.util.HashMap; -import java.util.function.Function; -import java.util.function.Supplier; /** * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending @@ -23,19 +20,24 @@ public class KeyEmbedderResponder implements KeyboardManager.Responder { private static final String TAG = "KeyEmbedderResponder"; - private static class SyncGoal { - public SyncGoal(boolean toggling, long physicalKey, long logicalKey, int mask) { + private static class KeyPair { + public KeyPair(long physicalKey, long logicalKey) { this.physicalKey = physicalKey; this.logicalKey = logicalKey; - this.toggling = toggling; + } + + public long physicalKey; + public long logicalKey; + } + + private static class PressingGoal { + public PressingGoal(int mask, KeyPair[] keys) { this.mask = mask; + this.keys = keys; } - public final long physicalKey; - public final long logicalKey; - public final boolean toggling; public final int mask; - public boolean enabled = false; + public final KeyPair[] keys; } private static KeyData.Type getEventType(KeyEvent event) { @@ -58,14 +60,20 @@ private static KeyData.Type getEventType(KeyEvent event) { private final HashMap pressingRecords = new HashMap<>(); private BinaryMessenger messenger; - private final ArrayList syncGoals = new ArrayList<>(); + private final ArrayList pressingGoals = new ArrayList<>(); public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; - // ShiftLeft - this.syncGoals.add(new SyncGoal(false, 0x00000700e1L, 0x0200000102L, KeyEvent.META_SHIFT_LEFT_ON)); + this.pressingGoals.add( + new PressingGoal( + KeyEvent.META_SHIFT_LEFT_ON, + new KeyPair[] { + new KeyPair(0x00000700e1L, 0x0200000102L), // ShiftLeft + new KeyPair(0x00000700e5L, 0x0200000103L), // ShiftRight + })); // CapsLock - this.syncGoals.add(new SyncGoal(true, 0x0000070039L, 0x0100000104L, KeyEvent.META_CAPS_LOCK_ON)); + // this.pressingGoals.add(new PressingGoal(true, 0x0000070039L, 0x0100000104L, + // KeyEvent.META_CAPS_LOCK_ON)); } private int combiningCharacter; @@ -133,12 +141,10 @@ private Long getPhysicalKey(@NonNull KeyEvent event) { if (byMapping != null) { return byMapping; } - // TODO(dkwingsmt): Logic for D-pad return kAndroidPlane + event.getScanCode(); } private Long getLogicalKey(@NonNull KeyEvent event) { - // TODO(dkwingsmt): Better logic. final Long byMapping = KeyboardMap.keyCodeToLogical.get((long) event.getKeyCode()); if (byMapping != null) { return byMapping; @@ -156,66 +162,114 @@ void updatePressingState(Long physicalKey, Long logicalKey) { } } - void synchronizePressingKey(SyncGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { - final boolean nowPressed = pressingRecords.containsKey(goal.physicalKey); - final KeyData.Type type = getEventType(event); - boolean targetPressed = true; - // System.out.printf("Sync [ph 0x%x eventPh 0x%x] nowP %d trueP %d type %d\n", goal.physicalKey, eventPhysicalKey, nowPressed ? 1 : 0, truePressed ? 1 : 0, type.getValue()); - if (eventPhysicalKey != goal.physicalKey) { - targetPressed = truePressed; + void synchronizePressingKey( + PressingGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { + // A goal can contain multiple keys. The target of synchronization process is to derive a + // pre-event state that can fulfill the true state (`truePressed`) after the event, while + // requiring as few synthesized events from the current state (`nowStates`) as possible. + // + // NowState ----------------> PreEventState --------------> TrueState + // Synchronization Event + + 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].physicalKey == eventPhysicalKey) { + 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.", + eventPhysicalKey)); + } + 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. + preEventStates[keyIdx] = nowStates[keyIdx]; + return; + 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 sending 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.", + eventPhysicalKey)); + } + preEventStates[keyIdx] = nowStates[keyIdx]; + postEventAnyPressed = true; + return; + } + } 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]; + postEventAnyPressed = postEventAnyPressed || nowStates[keyIdx]; + } else { + preEventStates[keyIdx] = true; + postEventAnyPressed = true; + } + } + if (!postEventAnyPressed) { + preEventStates[0] = true; + } } else { - // The state if the goal key is the event key: - // - // EventType Down Up - // Pressed 1 0 - switch (type) { - case kDown: - targetPressed = false; - if (!truePressed) { - throw new AssertionError(String.format( - "Unexpected metaState 0 for key 0x%x during an ACTION_DOWN event.", eventPhysicalKey)); - } - break; - case kUp: - // Incoming event is a repeat. 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. - if (truePressed) { - throw new AssertionError(String.format( - "Unexpected metaState 1 for key 0x%x during an ACTION_UP event.", eventPhysicalKey)); - } - return; - 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 sending printable characters. - if (!truePressed) { - throw new AssertionError(String.format( - "Unexpected metaState 0 for key 0x%x during an ACTION_down repeat event.", eventPhysicalKey)); - } - return; + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (preEventStates[keyIdx] != null) { + continue; + } + preEventStates[keyIdx] = false; } } - if (nowPressed != targetPressed) { - synthesizeEvent(targetPressed, goal.logicalKey, goal.physicalKey, event.getEventTime()); - updatePressingState(goal.physicalKey, targetPressed ? goal.logicalKey : null); + + for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { + if (nowStates[keyIdx] != preEventStates[keyIdx]) { + final KeyPair key = goal.keys[keyIdx]; + synthesizeEvent( + preEventStates[keyIdx], key.logicalKey, key.physicalKey, event.getEventTime()); + } } } // Return: if any events has been sent private boolean handleEventImpl( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + System.out.printf( + "KeyEvent [0x%x] scan 0x%x key 0x%x uni 0x%x meta 0x%x\n", + event.getAction(), + event.getScanCode(), + event.getKeyCode(), + event.getUnicodeChar(), + event.getMetaState()); final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); - for (final SyncGoal goal : syncGoals) { - final boolean targetOn = (event.getMetaState() & goal.mask) != 0; - // System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, targetOn ? 1 : 0); - if (goal.toggling) { -// synchronizeTogglingKey(goal, targetOn, physicalKey, event); - } else { - synchronizePressingKey(goal, targetOn, physicalKey, event); - } + for (final PressingGoal goal : pressingGoals) { + // System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, + // targetOn ? 1 : 0); + // synchronizeTogglingKey(goal, targetOn, physicalKey, event); + synchronizePressingKey(goal, (event.getMetaState() & goal.mask) != 0, physicalKey, event); } boolean isDownEvent; @@ -243,7 +297,6 @@ private boolean handleEventImpl( type = KeyData.Type.kRepeat; } else { synthesizeEvent(false, lastLogicalRecord, physicalKey, event.getEventTime()); - updatePressingState(physicalKey, null); type = KeyData.Type.kDown; } } @@ -284,6 +337,7 @@ private void synthesizeEvent(boolean isDown, Long logicalKey, Long physicalKey, output.physicalKey = physicalKey; output.character = null; output.synthesized = true; + updatePressingState(physicalKey, isDown ? logicalKey : null); sendKeyEvent(output, null); } 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 dfebf9d45cee2..cf2ae9fb44000 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -824,12 +824,12 @@ public void nonUsKeys() { calls.clear(); tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '&', 0)); + new FakeKeyEvent(ACTION_UP, SCAN_DIGIT1, KEYCODE_1, 0, '&', 0x41)); assertSingleEmbedderEventEquals(calls, 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', 0x41)); + new FakeKeyEvent(ACTION_UP, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', 0)); calls.clear(); // Russian lowerA @@ -859,7 +859,8 @@ public void synchronizeShiftLeft() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', SHIFT_LEFT_ON))); + 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); @@ -874,14 +875,16 @@ public void synchronizeShiftLeft() { calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true); calls.clear(); - // Test if ShiftLeft can be synchronized for events of this key. Test all 6 cases (3 types x 2 states) + // Test if ShiftLeft can be synchronized for events of this key. Test all 6 cases (3 types x 2 + // states) // in the following order to keep the desired current states. // Repeat event while current state is 0. assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 1, '\0', SHIFT_LEFT_ON))); + 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); @@ -891,7 +894,8 @@ public void synchronizeShiftLeft() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON))); + 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); @@ -915,15 +919,15 @@ public void synchronizeShiftLeft() { 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); + assertEmbedderEventEquals(calls.get(0).keyData, Type.kDown, 0l, 0l, null, true); calls.clear(); // Down event while current state is 0. assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 0, '\0', SHIFT_LEFT_ON))); + 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); @@ -933,7 +937,8 @@ public void synchronizeShiftLeft() { assertEquals( true, tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_SHIFT_LEFT, KEYCODE_SHIFT_LEFT, 1, '\0', SHIFT_LEFT_ON))); + 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); From af9861e9dd5b4226e8641ee1d115f2232c624708 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 16 May 2022 19:16:55 -0700 Subject: [PATCH 12/26] Sync with synonym --- .../android/KeyEmbedderResponder.java | 22 ++- .../android/KeyboardManagerTest.java | 177 ++++++++++++++++-- 2 files changed, 178 insertions(+), 21 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 0616fa527baea..30e5fcedd4103 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -66,7 +66,7 @@ public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; this.pressingGoals.add( new PressingGoal( - KeyEvent.META_SHIFT_LEFT_ON, + KeyEvent.META_SHIFT_ON, new KeyPair[] { new KeyPair(0x00000700e1L, 0x0200000102L), // ShiftLeft new KeyPair(0x00000700e5L, 0x0200000103L), // ShiftRight @@ -164,9 +164,13 @@ void updatePressingState(Long physicalKey, Long logicalKey) { void synchronizePressingKey( PressingGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { - // A goal can contain multiple keys. The target of synchronization process is to derive a - // pre-event state that can fulfill the true state (`truePressed`) after the event, while - // requiring as few synthesized events from the current state (`nowStates`) as possible. + // A goal can contain multiple keys sharing the same mask. During an + // incoming event, there might be a synthesized Flutter event for each key, + // followed by an eventual main Flutter event. The core of the + // synchronization algorithm is to derive a pre-event state that can + // fulfill the true state (`truePressed`) after the event, while requiring + // as few synthesized events from the current state (`nowStates`) as + // possible. // // NowState ----------------> PreEventState --------------> TrueState // Synchronization Event @@ -195,7 +199,7 @@ void synchronizePressingKey( // don't synthesize a down event even if it's not. The later code will handle such cases // by skipping abrupt up events. preEventStates[keyIdx] = nowStates[keyIdx]; - return; + 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 @@ -209,7 +213,7 @@ void synchronizePressingKey( } preEventStates[keyIdx] = nowStates[keyIdx]; postEventAnyPressed = true; - return; + break; } } else { postEventAnyPressed = postEventAnyPressed || nowStates[keyIdx]; @@ -266,9 +270,9 @@ private boolean handleEventImpl( final Long logicalKey = getLogicalKey(event); for (final PressingGoal goal : pressingGoals) { - // System.out.printf("Meta 0x%x mask 0x%x result %d\n", event.getMetaState(), goal.mask, - // targetOn ? 1 : 0); - // synchronizeTogglingKey(goal, targetOn, physicalKey, event); + // System.out.printf( + // "Meta 0x%x mask 0x%x result %d\n", + // event.getMetaState(), goal.mask, (event.getMetaState() & goal.mask) != 0 ? 1 : 0); synchronizePressingKey(goal, (event.getMetaState() & goal.mask) != 0, physicalKey, event); } 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 cf2ae9fb44000..2564bff404150 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -23,8 +23,10 @@ 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 org.json.JSONException; import org.json.JSONObject; import org.junit.Before; @@ -50,6 +52,11 @@ public class KeyboardManagerTest { // public static final int SCAN_META_LEFT = 0x2a; // public static final int SCAN_META_RIGHT = 0x36; + 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; + /** * Records a message that {@link KeyboardManager} sends to outside. * @@ -845,7 +852,8 @@ public void nonUsKeys() { } @Test - public void synchronizeShiftLeft() { + public void synchronizeShiftLeftDuringForeignKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ArrowLeft. final KeyboardTester tester = new KeyboardTester(); final ArrayList calls = new ArrayList<>(); @@ -854,8 +862,6 @@ public void synchronizeShiftLeft() { final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; - // Test if ShiftLeft can be synchronized for events of other keys. - assertEquals( true, tester.keyboardManager.handleEvent( @@ -874,12 +880,22 @@ public void synchronizeShiftLeft() { assertEmbedderEventEquals( calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, true); calls.clear(); + } - // Test if ShiftLeft can be synchronized for events of this key. Test all 6 cases (3 types x 2 - // states) - // in the following order to keep the desired current states. + @Test + public void synchronizeShiftLeftDuringSelfKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ShiftLeft. + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); - // Repeat event while current state is 0. + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.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( @@ -890,7 +906,7 @@ public void synchronizeShiftLeft() { calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); - // Down event while current state is 1. + // Down event when the current state is 1. assertEquals( true, tester.keyboardManager.handleEvent( @@ -903,7 +919,7 @@ public void synchronizeShiftLeft() { calls.get(1).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); - // Up event while current state is 1. + // Up event when the current state is 1. assertEquals( true, tester.keyboardManager.handleEvent( @@ -913,7 +929,7 @@ public void synchronizeShiftLeft() { calls.get(0).keyData, Type.kUp, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); - // Up event while current state is 0. + // Up event when the current state is 0. assertEquals( true, tester.keyboardManager.handleEvent( @@ -922,7 +938,7 @@ public void synchronizeShiftLeft() { assertEmbedderEventEquals(calls.get(0).keyData, Type.kDown, 0l, 0l, null, true); calls.clear(); - // Down event while current state is 0. + // Down event when the current state is 0. assertEquals( true, tester.keyboardManager.handleEvent( @@ -933,7 +949,7 @@ public void synchronizeShiftLeft() { calls.get(0).keyData, Type.kDown, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); - // Repeat event while current state is 1. + // Repeat event when the current state is 1. assertEquals( true, tester.keyboardManager.handleEvent( @@ -944,4 +960,141 @@ public void synchronizeShiftLeft() { calls.get(0).keyData, Type.kRepeat, PHYSICAL_SHIFT_LEFT, LOGICAL_SHIFT_LEFT, null, false); calls.clear(); } + + // Given the pressing states of ShiftLeft and ShiftRight, and the specified type of ShiftRight + // event occurs + // that requires the given true pressing state, store the resulting events to `calls`. + // + // The `calls` is always cleared at the beginning. + public static List testShiftRightEvent( + boolean preEventLeftPressed, + boolean preEventRightPressed, + boolean rightEventIsDown, + boolean truePressed) { + System.out.printf("New test case\n"); + final ArrayList calls = new ArrayList<>(); + final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.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; + } + + public 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); + } + } + + @Test + public void synchronizeShiftLeftDuringSiblingKeyEvents() { + // Test if ShiftLeft can be synchronized during events of ShiftRight. + // Real device test on ChromeOS shows that, unlike the documentations, ShiftRight events set + // meta flag META_SHIFT_LEFT_ON instead of META_SHIFT_RIGHT_ON (likewise for other modifiers.) + + // 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), + }); + } } From 019faa66c12b79992d6c45c7e51c56a339c03bb9 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 02:52:40 -0700 Subject: [PATCH 13/26] Add all pressing goals and mask constants --- .../android/KeyEmbedderResponder.java | 76 +++++-------------- .../embedding/android/KeyboardMap.java | 47 ++++++++++++ .../android/KeyboardManagerTest.java | 4 +- 3 files changed, 69 insertions(+), 58 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 30e5fcedd4103..7b7fd487c840e 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -8,7 +8,6 @@ import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.plugin.common.BinaryMessenger; -import java.util.ArrayList; import java.util.HashMap; /** @@ -20,26 +19,6 @@ public class KeyEmbedderResponder implements KeyboardManager.Responder { private static final String TAG = "KeyEmbedderResponder"; - private static class KeyPair { - public KeyPair(long physicalKey, long logicalKey) { - this.physicalKey = physicalKey; - this.logicalKey = logicalKey; - } - - public long physicalKey; - public long logicalKey; - } - - private static class PressingGoal { - public PressingGoal(int mask, KeyPair[] keys) { - this.mask = mask; - this.keys = keys; - } - - public final int mask; - public final KeyPair[] keys; - } - private static KeyData.Type getEventType(KeyEvent event) { final boolean isRepeatEvent = event.getRepeatCount() > 0; switch (event.getAction()) { @@ -52,25 +31,11 @@ private static KeyData.Type getEventType(KeyEvent event) { } } - // TODO(dkwingsmt): put the constants to key map. - private static final Long kValueMask = 0x000ffffffffl; - private static final Long kUnicodePlane = 0x00000000000l; - private static final Long kAndroidPlane = 0x01100000000l; - private final HashMap pressingRecords = new HashMap<>(); private BinaryMessenger messenger; - private final ArrayList pressingGoals = new ArrayList<>(); - public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; - this.pressingGoals.add( - new PressingGoal( - KeyEvent.META_SHIFT_ON, - new KeyPair[] { - new KeyPair(0x00000700e1L, 0x0200000102L), // ShiftLeft - new KeyPair(0x00000700e5L, 0x0200000103L), // ShiftRight - })); // CapsLock // this.pressingGoals.add(new PressingGoal(true, 0x0000070039L, 0x0100000104L, // KeyEvent.META_CAPS_LOCK_ON)); @@ -141,7 +106,7 @@ private Long getPhysicalKey(@NonNull KeyEvent event) { if (byMapping != null) { return byMapping; } - return kAndroidPlane + event.getScanCode(); + return KeyboardMap.kAndroidPlane + event.getScanCode(); } private Long getLogicalKey(@NonNull KeyEvent event) { @@ -149,7 +114,7 @@ private Long getLogicalKey(@NonNull KeyEvent event) { if (byMapping != null) { return byMapping; } - return kAndroidPlane + event.getKeyCode(); + return KeyboardMap.kAndroidPlane + event.getKeyCode(); } void updatePressingState(Long physicalKey, Long logicalKey) { @@ -163,18 +128,16 @@ void updatePressingState(Long physicalKey, Long logicalKey) { } void synchronizePressingKey( - PressingGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { - // A goal can contain multiple keys sharing the same mask. During an - // incoming event, there might be a synthesized Flutter event for each key, - // followed by an eventual main Flutter event. The core of the - // synchronization algorithm is to derive a pre-event state that can - // fulfill the true state (`truePressed`) after the event, while requiring - // as few synthesized events from the current state (`nowStates`) as - // possible. + KeyboardMap.PressingGoal goal, boolean truePressed, long eventPhysicalKey, 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 - + // 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; @@ -195,16 +158,16 @@ void synchronizePressingKey( } 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. + // 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 sending printable characters. Obviously don't synthesize up - // events either. + // Don't synthesize a down event here, or there will be a down event and a repeat event, + // both of which sending printable characters. Obviously don't synthesize up events + // either. if (!truePressed) { throw new AssertionError( String.format( @@ -247,9 +210,10 @@ void synchronizePressingKey( } } + // Dispatch synthesized events for state differences. for (int keyIdx = 0; keyIdx < goal.keys.length; keyIdx += 1) { if (nowStates[keyIdx] != preEventStates[keyIdx]) { - final KeyPair key = goal.keys[keyIdx]; + final KeyboardMap.KeyPair key = goal.keys[keyIdx]; synthesizeEvent( preEventStates[keyIdx], key.logicalKey, key.physicalKey, event.getEventTime()); } @@ -269,7 +233,7 @@ private boolean handleEventImpl( final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); - for (final PressingGoal goal : pressingGoals) { + for (final KeyboardMap.PressingGoal goal : KeyboardMap.pressingGoals) { // System.out.printf( // "Meta 0x%x mask 0x%x result %d\n", // event.getMetaState(), goal.mask, (event.getMetaState() & goal.mask) != 0 ? 1 : 0); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index 4124dfd6df9fe..e02f3002759c5 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -11,9 +11,30 @@ // 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; public class KeyboardMap { + public static class KeyPair { + public KeyPair(long physicalKey, long logicalKey) { + this.physicalKey = physicalKey; + this.logicalKey = logicalKey; + } + + public long physicalKey; + public long logicalKey; + } + + public static class PressingGoal { + public PressingGoal(int mask, KeyPair[] keys) { + this.mask = mask; + this.keys = keys; + } + + public final int mask; + public final KeyPair[] keys; + } + public static final HashMap scanCodeToPhysical = new HashMap() { private static final long serialVersionUID = 1L; @@ -521,4 +542,30 @@ public class KeyboardMap { 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 + }), + }; + + public static final long kValueMask = 0x000ffffffffL; + public static final long kUnicodePlane = 0x00000000000L; + public static final long kAndroidPlane = 0x01100000000L; } 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 2564bff404150..c538cd968b037 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -776,7 +776,7 @@ public void modifierKeys() { // AltLeft tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_LEFT, KEYCODE_ALT_LEFT, 0, '\0', 0x3000)); + new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_LEFT, KEYCODE_ALT_LEFT, 0, '\0', 0x12)); assertSingleEmbedderEventEquals( calls, Type.kDown, PHYSICAL_ALT_LEFT, LOGICAL_ALT_LEFT, null, false); calls.clear(); @@ -789,7 +789,7 @@ public void modifierKeys() { // AltRight tester.keyboardManager.handleEvent( - new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_RIGHT, KEYCODE_ALT_RIGHT, 0, '\0', 0x3000)); + new FakeKeyEvent(ACTION_DOWN, SCAN_ALT_RIGHT, KEYCODE_ALT_RIGHT, 0, '\0', 0x12)); assertSingleEmbedderEventEquals( calls, Type.kDown, PHYSICAL_ALT_RIGHT, LOGICAL_ALT_RIGHT, null, false); calls.clear(); From 95e8b022053ea35c76bb298d3689a512d8421c0c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 03:35:19 -0700 Subject: [PATCH 14/26] Test all modifiers --- .../android/KeyboardManagerTest.java | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) 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 c538cd968b037..5f933a33e3cfa 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -961,9 +961,8 @@ public void synchronizeShiftLeftDuringSelfKeyEvents() { calls.clear(); } - // Given the pressing states of ShiftLeft and ShiftRight, and the specified type of ShiftRight - // event occurs - // that requires the given true pressing state, store the resulting events to `calls`. + // Start a new tester, generate a ShiftRight event under the specified condition, and return the + // output events for that event. // // The `calls` is always cleared at the beginning. public static List testShiftRightEvent( @@ -971,7 +970,6 @@ public static List testShiftRightEvent( boolean preEventRightPressed, boolean rightEventIsDown, boolean truePressed) { - System.out.printf("New test case\n"); final ArrayList calls = new ArrayList<>(); final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; @@ -1097,4 +1095,53 @@ public void synchronizeShiftLeftDuringSiblingKeyEvents() { 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', KeyEvent.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', KeyEvent.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(); + } } From 364fde2ef60d9df0f1a622b083f93a14b541dcb7 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 04:51:39 -0700 Subject: [PATCH 15/26] Add tests for capslock --- .../android/KeyEmbedderResponder.java | 57 ++++-- .../embedding/android/KeyboardMap.java | 19 ++ .../android/KeyboardManagerTest.java | 163 ++++++++++++++++-- 3 files changed, 216 insertions(+), 23 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 7b7fd487c840e..840bbb1861dd4 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -7,6 +7,8 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; +import io.flutter.embedding.android.KeyboardMap.PressingGoal; +import io.flutter.embedding.android.KeyboardMap.TogglingGoal; import io.flutter.plugin.common.BinaryMessenger; import java.util.HashMap; @@ -32,13 +34,15 @@ private static KeyData.Type getEventType(KeyEvent event) { } private final HashMap pressingRecords = new HashMap<>(); - private BinaryMessenger messenger; + private final BinaryMessenger messenger; + // Map from logical key + private final HashMap togglingGoals = new HashMap<>(); public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; - // CapsLock - // this.pressingGoals.add(new PressingGoal(true, 0x0000070039L, 0x0100000104L, - // KeyEvent.META_CAPS_LOCK_ON)); + for (final TogglingGoal goal : KeyboardMap.getTogglingGoals()) { + togglingGoals.put(goal.logicalKey, goal); + } } private int combiningCharacter; @@ -128,7 +132,7 @@ void updatePressingState(Long physicalKey, Long logicalKey) { } void synchronizePressingKey( - KeyboardMap.PressingGoal goal, boolean truePressed, long eventPhysicalKey, KeyEvent event) { + 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. // @@ -145,7 +149,7 @@ void synchronizePressingKey( // 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].physicalKey == eventPhysicalKey) { + if (goal.keys[keyIdx].logicalKey == eventLogicalKey) { switch (getEventType(event)) { case kDown: preEventStates[keyIdx] = false; @@ -154,7 +158,7 @@ void synchronizePressingKey( throw new AssertionError( String.format( "Unexpected metaState 0 for key 0x%x during an ACTION_down event.", - eventPhysicalKey)); + eventLogicalKey)); } break; case kUp: @@ -172,7 +176,7 @@ void synchronizePressingKey( throw new AssertionError( String.format( "Unexpected metaState 0 for key 0x%x during an ACTION_down repeat event.", - eventPhysicalKey)); + eventLogicalKey)); } preEventStates[keyIdx] = nowStates[keyIdx]; postEventAnyPressed = true; @@ -220,6 +224,26 @@ void synchronizePressingKey( } } + 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()); + } + } + // Return: if any events has been sent private boolean handleEventImpl( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { @@ -233,11 +257,12 @@ private boolean handleEventImpl( final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); - for (final KeyboardMap.PressingGoal goal : KeyboardMap.pressingGoals) { - // System.out.printf( - // "Meta 0x%x mask 0x%x result %d\n", - // event.getMetaState(), goal.mask, (event.getMetaState() & goal.mask) != 0 ? 1 : 0); - synchronizePressingKey(goal, (event.getMetaState() & goal.mask) != 0, physicalKey, 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; @@ -284,6 +309,12 @@ private boolean handleEventImpl( 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(); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index e02f3002759c5..9cc71d7aceb7a 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -35,6 +35,19 @@ public PressingGoal(int mask, KeyPair[] keys) { public final KeyPair[] keys; } + 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; + public boolean enabled = false; + } + public static final HashMap scanCodeToPhysical = new HashMap() { private static final long serialVersionUID = 1L; @@ -565,6 +578,12 @@ public PressingGoal(int mask, KeyPair[] keys) { }), }; + public static TogglingGoal[] getTogglingGoals() { + return new TogglingGoal[] { + new TogglingGoal(KeyEvent.META_CAPS_LOCK_ON, 0x0000070039L, 0x0100000104L), + }; + } + public static final long kValueMask = 0x000ffffffffL; public static final long kUnicodePlane = 0x00000000000L; public static final long kAndroidPlane = 0x01100000000L; 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 5f933a33e3cfa..c5831bc2c8d02 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -49,8 +49,7 @@ public class KeyboardManagerTest { 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_META_LEFT = 0x2a; - // public static final int SCAN_META_RIGHT = 0x36; + public static final int SCAN_CAPS_LOCK = 0x3a; public static final boolean DOWN_EVENT = true; public static final boolean UP_EVENT = false; @@ -860,7 +859,7 @@ public void synchronizeShiftLeftDuringForeignKeyEvents() { tester.recordEmbedderCallsTo(calls); tester.respondToTextInputWith(true); // Suppress redispatching - final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; + final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON; assertEquals( true, @@ -891,7 +890,7 @@ public void synchronizeShiftLeftDuringSelfKeyEvents() { tester.recordEmbedderCallsTo(calls); tester.respondToTextInputWith(true); // Suppress redispatching - final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; + 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. @@ -971,7 +970,7 @@ public static List testShiftRightEvent( boolean rightEventIsDown, boolean truePressed) { final ArrayList calls = new ArrayList<>(); - final int SHIFT_LEFT_ON = KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON; + final int SHIFT_LEFT_ON = META_SHIFT_LEFT_ON | META_SHIFT_ON; final KeyboardTester tester = new KeyboardTester(); tester.respondToTextInputWith(true); // Suppress redispatching @@ -1024,9 +1023,9 @@ public static void verifyEmbedderEvents(List receivedCalls, KeyData[ @Test public void synchronizeShiftLeftDuringSiblingKeyEvents() { - // Test if ShiftLeft can be synchronized during events of ShiftRight. - // Real device test on ChromeOS shows that, unlike the documentations, ShiftRight events set - // meta flag META_SHIFT_LEFT_ON instead of META_SHIFT_RIGHT_ON (likewise for other modifiers.) + // 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 @@ -1110,7 +1109,7 @@ public void synchronizeOtherModifiers() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', KeyEvent.META_CTRL_ON))); + 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); @@ -1129,7 +1128,7 @@ public void synchronizeOtherModifiers() { true, tester.keyboardManager.handleEvent( new FakeKeyEvent( - ACTION_DOWN, SCAN_ARROW_LEFT, KEYCODE_DPAD_LEFT, 0, '\0', KeyEvent.META_ALT_ON))); + 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); @@ -1144,4 +1143,148 @@ public void synchronizeOtherModifiers() { 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(); + } } From ccff89af9a6dd93e3d7a96b5fd362d7d85c1db43 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 04:59:01 -0700 Subject: [PATCH 16/26] Gen --- .../android/io/flutter/embedding/android/KeyboardMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index 9cc71d7aceb7a..db99cf3d5786b 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -580,7 +580,7 @@ public TogglingGoal(int mask, long physicalKey, long logicalKey) { public static TogglingGoal[] getTogglingGoals() { return new TogglingGoal[] { - new TogglingGoal(KeyEvent.META_CAPS_LOCK_ON, 0x0000070039L, 0x0100000104L), + new TogglingGoal(KeyEvent.META_CAPS_LOCK_ON, 0x00070039L, 0x0100000104L), }; } From 2ec5c3156272beb716b71f57bb9615304903dd65 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 15:18:15 -0700 Subject: [PATCH 17/26] Docs --- .../android/KeyEmbedderResponder.java | 95 +++++++++++++++---- .../embedding/android/KeyboardMap.java | 28 ++++++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 840bbb1861dd4..d1dc5a7692167 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -6,10 +6,13 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; + import androidx.annotation.NonNull; + import io.flutter.embedding.android.KeyboardMap.PressingGoal; import io.flutter.embedding.android.KeyboardMap.TogglingGoal; import io.flutter.plugin.common.BinaryMessenger; + import java.util.HashMap; /** @@ -21,6 +24,7 @@ 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()) { @@ -33,9 +37,17 @@ private static KeyData.Type getEventType(KeyEvent event) { } } - private final HashMap pressingRecords = new HashMap<>(); + // The messenger 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`. private final BinaryMessenger messenger; - // Map from logical key + // The keys being pressed currently, mapped from physical keys to logical keys. + 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. private final HashMap togglingGoals = new HashMap<>(); public KeyEmbedderResponder(BinaryMessenger messenger) { @@ -47,6 +59,7 @@ public KeyEmbedderResponder(BinaryMessenger messenger) { private int combiningCharacter; // TODO(dkwingsmt): Deduplicate this function from KeyChannelResponder.java. + /** * 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 @@ -105,6 +118,9 @@ Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { return complexCharacter; } + // Get the physical key for this event. + // + // The returned value is never null. private Long getPhysicalKey(@NonNull KeyEvent event) { final Long byMapping = KeyboardMap.scanCodeToPhysical.get((long) event.getScanCode()); if (byMapping != null) { @@ -113,6 +129,9 @@ private Long getPhysicalKey(@NonNull KeyEvent event) { return KeyboardMap.kAndroidPlane + event.getScanCode(); } + // 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) { @@ -121,7 +140,14 @@ private Long getLogicalKey(@NonNull KeyEvent event) { return KeyboardMap.kAndroidPlane + event.getKeyCode(); } - void updatePressingState(Long physicalKey, Long logicalKey) { + // 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"); @@ -131,6 +157,16 @@ void updatePressingState(Long physicalKey, Long logicalKey) { } } + // 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 @@ -168,10 +204,9 @@ void synchronizePressingKey( 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 sending printable characters. Obviously don't synthesize up events - // either. + // 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( @@ -196,7 +231,6 @@ void synchronizePressingKey( } if (postEventAnyPressed) { preEventStates[keyIdx] = nowStates[keyIdx]; - postEventAnyPressed = postEventAnyPressed || nowStates[keyIdx]; } else { preEventStates[keyIdx] = true; postEventAnyPressed = true; @@ -224,6 +258,19 @@ void synchronizePressingKey( } } + // 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) { @@ -244,7 +291,9 @@ void synchronizeTogglingKey( } } - // Return: if any events has been sent + // Implements the core algorithm of `handleEvent`. + // + // Returns whether any events are dispatched. private boolean handleEventImpl( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { System.out.printf( @@ -345,17 +394,31 @@ private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHand onKeyEventHandledCallback == null ? null : message -> { - Boolean handled = false; - message.rewind(); - if (message.capacity() != 0) { - handled = message.get() != 0; - } - onKeyEventHandledCallback.onKeyEventHandled(handled); - }; + 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) { diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index db99cf3d5786b..80b8adfe147e3 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java @@ -14,7 +14,9 @@ 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; @@ -25,6 +27,11 @@ public KeyPair(long physicalKey, long logicalKey) { 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; @@ -35,6 +42,14 @@ public PressingGoal(int mask, KeyPair[] keys) { 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 object of this class is not immutable. 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; @@ -45,9 +60,15 @@ public TogglingGoal(int mask, long physicalKey, long 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; @@ -288,6 +309,7 @@ public TogglingGoal(int mask, long physicalKey, long logicalKey) { } }; + /** 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; @@ -578,6 +600,12 @@ public TogglingGoal(int mask, long physicalKey, long logicalKey) { }), }; + /** + * 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 not immutable. + */ public static TogglingGoal[] getTogglingGoals() { return new TogglingGoal[] { new TogglingGoal(KeyEvent.META_CAPS_LOCK_ON, 0x00070039L, 0x0100000104L), From e815d1be3e6babc697a13f086601c6107fe2f6f4 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 15:23:40 -0700 Subject: [PATCH 18/26] mutable --- .../embedding/android/KeyboardMap.java | Bin 32704 -> 32694 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index 80b8adfe147e3773b854c79c738bbad5d1bef412..1f7c2add3794f89b4e799ad0b60bff03e89e5919 100644 GIT binary patch delta 74 zcmX@`pK;rN#tm=TG>R4S(-cZFGK&?Ga}tY-6%vb56>>{U5|eUL^%O!fQWdHlax(L> d8751zt8fAprgAALC}?eVV`nYj99Wmn2mmD)8C3uP delta 71 zcmdn?pYgzd#tm=TBoy+~6iPBOixrY{5{ru!fQ-ES5{1m%$&Bp6j0%%Q*;P2X6ciM+ VfQnK#JF&Btvq9uHyVvD20sxFS7Uci{ From 388628129228db2ff7ba482c8f85dd2b4864842b Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 18:46:22 -0700 Subject: [PATCH 19/26] Extract CharacterCombiner, docs --- .../android/KeyChannelResponder.java | 62 +--------- .../android/KeyEmbedderResponder.java | 116 ++++-------------- .../embedding/android/KeyboardManager.java | 64 ++++++++++ .../embedding/android/KeyboardMap.java | Bin 32694 -> 32700 bytes .../android/KeyChannelResponderTest.java | 26 ---- .../android/KeyboardManagerTest.java | 28 +++++ 6 files changed, 122 insertions(+), 174 deletions(-) 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: - * - *

- * - *

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/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index d1dc5a7692167..e3fdb90e02e62 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -4,15 +4,12 @@ package io.flutter.embedding.android; -import android.view.KeyCharacterMap; 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; /** @@ -41,14 +38,18 @@ private static KeyData.Type getEventType(KeyEvent event) { // // On `handleEvent`, Flutter events are marshalled into byte buffers in the format specified by // `KeyData.toBytes`. - private final BinaryMessenger messenger; + @NonNull private final BinaryMessenger messenger; // The keys being pressed currently, mapped from physical keys to logical keys. - private final HashMap pressingRecords = new HashMap<>(); + @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. - private final HashMap togglingGoals = new HashMap<>(); + @NonNull private final HashMap togglingGoals = new HashMap<>(); + + @NonNull + private final KeyboardManager.CharacterCombiner characterCombiner = + new KeyboardManager.CharacterCombiner(); public KeyEmbedderResponder(BinaryMessenger messenger) { this.messenger = messenger; @@ -57,67 +58,6 @@ public KeyEmbedderResponder(BinaryMessenger messenger) { } } - private int combiningCharacter; - // TODO(dkwingsmt): Deduplicate this function from KeyChannelResponder.java. - - /** - * 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: - * - *

- * - *

The following reference explains the concept of a "combining character": - * https://en.wikipedia.org/wiki/Combining_character - */ - Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { - if (newCharacterCodePoint == 0) { - combiningCharacter = 0; - return 0; - } - 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; - } - // Get the physical key for this event. // // The returned value is never null. @@ -164,7 +104,8 @@ void updatePressingState(@NonNull Long physicalKey, @Nullable Long logicalKey) { // 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), + // 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( @@ -296,13 +237,6 @@ void synchronizeTogglingKey( // Returns whether any events are dispatched. private boolean handleEventImpl( @NonNull KeyEvent event, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { - System.out.printf( - "KeyEvent [0x%x] scan 0x%x key 0x%x uni 0x%x meta 0x%x\n", - event.getAction(), - event.getScanCode(), - event.getKeyCode(), - event.getUnicodeChar(), - event.getMetaState()); final Long physicalKey = getPhysicalKey(event); final Long logicalKey = getLogicalKey(event); @@ -342,7 +276,8 @@ private boolean handleEventImpl( type = KeyData.Type.kDown; } } - final char complexChar = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); + final char complexChar = + characterCombiner.applyCombiningCharacterToBaseCharacter(event.getUnicodeChar()); if (complexChar != 0) { character = "" + complexChar; } @@ -394,13 +329,13 @@ private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHand onKeyEventHandledCallback == null ? null : message -> { - Boolean handled = false; - message.rewind(); - if (message.capacity() != 0) { - handled = message.get() != 0; - } - onKeyEventHandledCallback.onKeyEventHandled(handled); - }; + Boolean handled = false; + message.rewind(); + if (message.capacity() != 0) { + handled = message.get() != 0; + } + onKeyEventHandledCallback.onKeyEventHandled(handled); + }; messenger.send(KeyData.CHANNEL, data.toBytes(), handleMessageReply); } @@ -409,15 +344,14 @@ private void sendKeyEvent(KeyData data, OnKeyEventHandledCallback onKeyEventHand * 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. + *

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. + * @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( diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index c1ddf7ea8ea38..be527612f351d 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: + * + *

+ * + *

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}. * diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java b/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java index 1f7c2add3794f89b4e799ad0b60bff03e89e5919..f4272be12007766d5187fd738dfbe742744fec1f 100644 GIT binary patch delta 62 zcmdn?pK;HB#trA$7?mfVXLDszP~I%cUdqI$oSs?|oSc~FoS%}ap}Bb)=N=PYkeGLB LB~&QZF47SI$I}(1 delta 59 zcmdngHLTdrY`NBHpQ$ KV1Z=2NJjv_eilmr 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 c5831bc2c8d02..74d08283f7ef4 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -14,6 +14,7 @@ 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; @@ -27,6 +28,7 @@ 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; @@ -56,6 +58,8 @@ public class KeyboardManagerTest { 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. * @@ -408,6 +412,30 @@ public void serializeAndDeserializeKeyData() { 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(); From 614e193b0877189cb66d72203854aa54e7e34f6f Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 18:47:05 -0700 Subject: [PATCH 20/26] doc --- .../android/io/flutter/embedding/android/KeyboardManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index be527612f351d..76e344a4e5d26 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -48,7 +48,7 @@ public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate * 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. + *

This class is not used by {@link KeyboardManager}, but by its responders. */ public static class CharacterCombiner { private int combiningCharacter = 0; From 926aded5d14b29a28e55afb271c225ce46a31e7d Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 19:07:07 -0700 Subject: [PATCH 21/26] License --- ci/licenses_golden/licenses_flutter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c6564ee37ac67..ab13597d0e588 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1343,10 +1343,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/KeyData.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 From e4fc8493c5b31e02b03a4a332015d2052491bf2e Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 17 May 2022 19:32:48 -0700 Subject: [PATCH 22/26] Doc for keydata --- lib/ui/window/key_data.h | 3 ++ lib/ui/window/key_data_packet.h | 3 ++ .../io/flutter/embedding/android/KeyData.java | 39 +++++++++++++++---- shell/platform/embedder/embedder.cc | 2 + 4 files changed, 40 insertions(+), 7 deletions(-) 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/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java index 1adc66a0216e0..8fe479533d504 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyData.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -5,20 +5,28 @@ 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; /** - * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending - * processed information in {@link KeyData}. + * The resulting Flutter key events generated by {@link KeyEmbedderResponder}, and are sent through + * the messenger after being marshalled with {@link #toBytes()}. * - *

This class corresponds to the HardwareKeyboard API in the framework. + *

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"; - // TODO(dkwingsmt): Doc + /** + * 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`. @@ -54,8 +62,16 @@ static Type fromLong(long 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(); @@ -87,9 +103,18 @@ public KeyData(@NonNull ByteBuffer buffer) { long logicalKey; boolean synthesized; - // Nullable bytes of characters encoded in UTF8. - String character; - + /** + * 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 { 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. From 41810a076427a1c4aff3cf88e23691768aa577bf Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 18 May 2022 10:10:13 -0700 Subject: [PATCH 23/26] Apply suggestions from code review Co-authored-by: Greg Spencer --- .../io/flutter/embedding/android/KeyEmbedderResponder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index e3fdb90e02e62..df2e8fee55255 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -34,7 +34,7 @@ private static KeyData.Type getEventType(KeyEvent event) { } } - // The messenger used to send Flutter key events to the framework. + // 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`. @@ -206,7 +206,7 @@ void synchronizePressingKey( // 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 + // 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 From 6b6ae7182462250e35ebd98836685bbe09e4da43 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 18 May 2022 10:15:17 -0700 Subject: [PATCH 24/26] Use & --- .../io/flutter/embedding/android/KeyEmbedderResponder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index df2e8fee55255..c817c60ceb739 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -66,7 +66,7 @@ private Long getPhysicalKey(@NonNull KeyEvent event) { if (byMapping != null) { return byMapping; } - return KeyboardMap.kAndroidPlane + event.getScanCode(); + return KeyboardMap.kAndroidPlane & event.getScanCode(); } // Get the logical key for this event. @@ -77,7 +77,7 @@ private Long getLogicalKey(@NonNull KeyEvent event) { if (byMapping != null) { return byMapping; } - return KeyboardMap.kAndroidPlane + event.getKeyCode(); + return KeyboardMap.kAndroidPlane & event.getKeyCode(); } // Update `pressingRecords`. From ecbca045bff226ebf22324bb5baa3d281abf7d0c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 18 May 2022 10:15:55 -0700 Subject: [PATCH 25/26] Format --- .../io/flutter/embedding/android/KeyData.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyData.java b/shell/platform/android/io/flutter/embedding/android/KeyData.java index 8fe479533d504..ee0e0fbb81f99 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyData.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyData.java @@ -62,15 +62,13 @@ static Type fromLong(long value) { } } - /** - * Creates an empty {@link KeyData}. - */ + /** 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}. + *

For the binary format, see {@code lib/ui/window/key_data_packet.h}. */ public KeyData(@NonNull ByteBuffer buffer) { final long charSize = buffer.getLong(); @@ -103,15 +101,13 @@ public KeyData(@NonNull ByteBuffer buffer) { long logicalKey; boolean synthesized; - /** - * The character of this key data encoded in UTF-8. - */ + /** 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}. + *

For the binary format, see {@code lib/ui/window/key_data_packet.h}. * * @return the marshalled bytes. */ From c2cd062b83e44ca282c4afbbdde3f54e874d2c91 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 18 May 2022 10:47:03 -0700 Subject: [PATCH 26/26] Fix tests --- .../flutter/plugin/editing/InputConnectionAdaptorTest.java | 4 ++-- .../platform/android/test/io/flutter/util/FakeKeyEvent.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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 691a7d0cc84bc..4faa39107f50b 100644 --- a/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -9,6 +9,11 @@ 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);