diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 7d5e643fdcb..cea4f342c63 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -440,6 +440,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActi FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/android/AndroidKeyProcessor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 7779ecef835..b6560fca387 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -108,6 +108,7 @@ java_library("flutter_shell_java") { "io/flutter/app/FlutterPluginRegistry.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterJNI.java", + "io/flutter/embedding/engine/android/AndroidKeyProcessor.java", "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", diff --git a/shell/platform/android/io/flutter/embedding/engine/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/engine/android/AndroidKeyProcessor.java new file mode 100644 index 00000000000..0a876cec5e2 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/android/AndroidKeyProcessor.java @@ -0,0 +1,87 @@ +package io.flutter.embedding.engine.android; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; + +public class AndroidKeyProcessor { + private final KeyEventChannel keyEventChannel; + private int combiningCharacter; + + public AndroidKeyProcessor(@NonNull KeyEventChannel keyEventChannel) { + this.keyEventChannel = keyEventChannel; + } + + public void onKeyUp(@NonNull KeyEvent keyEvent) { + Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); + keyEventChannel.keyUp( + new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter) + ); + } + + public void onKeyDown(@NonNull KeyEvent keyEvent) { + Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); + keyEventChannel.keyDown( + new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter) + ); + } + + /** + * 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 + */ + @Nullable + private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { + if (newCharacterCodePoint == 0) { + return null; + } + + Character 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; + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java index 2ecd048386f..5b90d2a761d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java @@ -5,6 +5,7 @@ package io.flutter.embedding.engine.systemchannels; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.KeyEvent; import java.util.HashMap; @@ -26,7 +27,7 @@ public class KeyEventChannel { this.channel = new BasicMessageChannel<>(dartExecutor, "flutter/keyevent", JSONMessageCodec.INSTANCE); } - public void keyUp(@NonNull KeyEvent keyEvent) { + public void keyUp(@NonNull FlutterKeyEvent keyEvent) { Map message = new HashMap<>(); message.put("type", "keyup"); message.put("keymap", "android"); @@ -35,7 +36,7 @@ public class KeyEventChannel { channel.send(message); } - public void keyDown(@NonNull KeyEvent keyEvent) { + public void keyDown(@NonNull FlutterKeyEvent keyEvent) { Map message = new HashMap<>(); message.put("type", "keydown"); message.put("keymap", "android"); @@ -44,11 +45,68 @@ public class KeyEventChannel { channel.send(message); } - private void encodeKeyEvent(@NonNull KeyEvent event, @NonNull Map message) { - message.put("flags", event.getFlags()); - message.put("codePoint", event.getUnicodeChar()); - message.put("keyCode", event.getKeyCode()); - message.put("scanCode", event.getScanCode()); - message.put("metaState", event.getMetaState()); + private void encodeKeyEvent(@NonNull FlutterKeyEvent event, @NonNull Map message) { + message.put("flags", event.flags); + message.put("plainCodePoint", event.plainCodePoint); + message.put("codePoint", event.codePoint); + message.put("keyCode", event.keyCode); + message.put("scanCode", event.scanCode); + message.put("metaState", event.metaState); + if (event.complexCharacter != null) { + message.put("character", event.complexCharacter.toString()); + } + } + + /** + * Key event as defined by Flutter. + */ + public static class FlutterKeyEvent { + public final int flags; + public final int plainCodePoint; + public final int codePoint; + public final int keyCode; + @Nullable + public final Character complexCharacter; + public final int scanCode; + public final int metaState; + + public FlutterKeyEvent( + @NonNull KeyEvent androidKeyEvent + ) { + this(androidKeyEvent, null); + } + + public FlutterKeyEvent( + @NonNull KeyEvent androidKeyEvent, + @Nullable Character complexCharacter + ) { + this( + androidKeyEvent.getFlags(), + androidKeyEvent.getUnicodeChar(0x0), + androidKeyEvent.getUnicodeChar(), + androidKeyEvent.getKeyCode(), + complexCharacter, + androidKeyEvent.getScanCode(), + androidKeyEvent.getMetaState() + ); + } + + public FlutterKeyEvent( + int flags, + int plainCodePoint, + int codePoint, + int keyCode, + @Nullable Character complexCharacter, + int scanCode, + int metaState + ) { + this.flags = flags; + this.plainCodePoint = plainCodePoint; + this.codePoint = codePoint; + this.keyCode = keyCode; + this.complexCharacter = complexCharacter; + this.scanCode = scanCode; + this.metaState = metaState; + } } } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 04abba458ac..b5126c31d54 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -26,6 +26,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import io.flutter.app.FlutterPluginRegistry; +import io.flutter.embedding.engine.android.AndroidKeyProcessor; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; @@ -93,6 +94,7 @@ public class FlutterView extends SurfaceView private final SystemChannel systemChannel; private final InputMethodManager mImm; private final TextInputPlugin mTextInputPlugin; + private final AndroidKeyProcessor androidKeyProcessor; private final SurfaceHolder.Callback mSurfaceCallback; private final ViewportMetrics mMetrics; private final AccessibilityManager mAccessibilityManager; @@ -172,7 +174,7 @@ public class FlutterView extends SurfaceView addActivityLifecycleListener(platformPlugin); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mTextInputPlugin = new TextInputPlugin(this); - + androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel); setLocales(getResources().getConfiguration()); sendUserPlatformSettingsToDart(); @@ -183,7 +185,7 @@ public class FlutterView extends SurfaceView if (!isAttached()) { return super.onKeyUp(keyCode, event); } - keyEventChannel.keyUp(event); + androidKeyProcessor.onKeyUp(event); return super.onKeyUp(keyCode, event); } @@ -199,7 +201,7 @@ public class FlutterView extends SurfaceView } } - keyEventChannel.keyDown(event); + androidKeyProcessor.onKeyDown(event); return super.onKeyDown(keyCode, event); }