From 776fafa99aa2ec2f45b20f58e3ad9d374ede41f8 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 13 Jan 2020 15:51:06 -0800 Subject: [PATCH 1/4] Fix hardware keyboard enter so it triggers an action. --- .../flutter/plugin/editing/InputConnectionAdaptor.java | 9 ++++++++- .../io/flutter/plugin/editing/TextInputPlugin.java | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 8a601f50aec4a..d7f79d074e93d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -27,6 +27,7 @@ class InputConnectionAdaptor extends BaseInputConnection { private final int mClient; private final TextInputChannel textInputChannel; private final Editable mEditable; + private final EditorInfo mEditorInfo; private int mBatchCount; private InputMethodManager mImm; private final Layout mLayout; @@ -36,13 +37,15 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, - Editable editable + Editable editable, + EditorInfo editorInfo ) { super(view, true); mFlutterView = view; mClient = client; this.textInputChannel = textInputChannel; mEditable = editable; + mEditorInfo = editorInfo; mBatchCount = 0; // We create a dummy Layout with max width so that the selection // shifting acts as if all text were in one line. @@ -203,6 +206,10 @@ public boolean sendKeyEvent(KeyEvent event) { int newSel = Math.min(selStart + 1, mEditable.length()); setSelection(newSel, newSel); return true; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || + event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER) { + performEditorAction(mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION); + return true; } else { // Enter a character. int character = event.getUnicodeChar(); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index e66e27053ee68..f836ddcffee87 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -235,7 +235,8 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { view, inputTarget.id, textInputChannel, - mEditable + mEditable, + outAttrs ); outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); From 837c362ea969791d8306c40ae00b8a7df4089778 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 13 Jan 2020 16:27:59 -0800 Subject: [PATCH 2/4] Add tests --- .../plugin/editing/TextInputPluginTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index e4cd753b8fa39..e617c46910562 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -4,11 +4,15 @@ import android.content.res.AssetManager; import android.provider.Settings; import android.util.SparseIntArray; +import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import java.nio.ByteBuffer; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,6 +25,7 @@ import org.robolectric.shadows.ShadowBuild; import org.robolectric.shadows.ShadowInputMethodManager; +import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -155,6 +160,34 @@ public void setTextInputEditingState_nullInputMethodSubtype() { assertEquals(1, testImm.getRestartCount(testView)); } + @Test + public void inputConnection_createsActionFromEnter() { + Log.setLogLevel(android.util.Log.VERBOSE); + TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + FlutterJNI mockFlutterJni = mock(FlutterJNI.class); + View testView = new View(RuntimeEnvironment.application); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); + TextInputPlugin textInputPlugin = new TextInputPlugin(testView, dartExecutor, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient(0, new TextInputChannel.Configuration(false, false, true, TextInputChannel.TextCapitalization.NONE, new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false), null, null)); + // There's a pending restart since we initialized the text input client. Flush that now. + textInputPlugin.setTextInputEditingState(testView, new TextInputChannel.TextEditState("", 0, 0)); + + + ByteBuffer message = JSONMethodCodec.INSTANCE.encodeMethodCall( + new MethodCall("TextInputClient.performAction", Arrays.asList(0, "TextInputAction.done"))); + verify(dartExecutor, times(1)).send("flutter/textinput", message, null); + InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); + + connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); + verify(dartExecutor, times(2)).send("flutter/textinput", message, null); + connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); + + connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER)); + verify(dartExecutor, times(3)).send("flutter/textinput", message, null); + connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_NUMPAD_ENTER)); + } + + @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; From 79277729161720bae92c08bda4aadafac74e8adc Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 14 Jan 2020 13:49:03 -0800 Subject: [PATCH 3/4] Fix method call checks in test --- .../plugin/editing/TextInputPluginTest.java | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index e617c46910562..7ae4b2fd77a7c 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -4,37 +4,44 @@ import android.content.res.AssetManager; import android.provider.Settings; import android.util.SparseIntArray; -import android.view.KeyEvent; -import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import android.view.KeyEvent; +import android.view.View; import java.nio.ByteBuffer; -import java.util.Arrays; -import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; +import org.junit.Test; + +import org.json.JSONArray; +import org.json.JSONException; + import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowBuild; import org.robolectric.shadows.ShadowInputMethodManager; -import io.flutter.Log; -import io.flutter.embedding.engine.FlutterJNI; +import org.mockito.ArgumentCaptor; + import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMethodCodec; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.platform.PlatformViewsController; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; + +import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -43,8 +50,22 @@ @Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class, sdk = 27) @RunWith(RobolectricTestRunner.class) public class TextInputPluginTest { + // Verifies the method and arguments for a captured method call. + private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs) throws JSONException { + buffer.rewind(); + MethodCall methodCall = JSONMethodCodec.INSTANCE.decodeMethodCall(buffer); + assertEquals(methodName, methodCall.method); + if (expectedArgs != null) { + JSONArray args = methodCall.arguments(); + assertEquals(expectedArgs.length, args.length()); + for (int i = 0; i < args.length(); i++) { + assertEquals(expectedArgs[i], args.get(i).toString()); + } + } + } + @Test - public void textInputPlugin_RequestsReattachOnCreation() { + public void textInputPlugin_RequestsReattachOnCreation() throws JSONException { // Initialize a general TextInputPlugin. InputMethodSubtype inputMethodSubtype = mock(InputMethodSubtype.class); TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); @@ -55,8 +76,12 @@ public void textInputPlugin_RequestsReattachOnCreation() { DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, dartExecutor, mock(PlatformViewsController.class)); - ByteBuffer message = JSONMethodCodec.INSTANCE.encodeMethodCall(new MethodCall("TextInputClient.requestExistingInputState", null)); - verify(dartExecutor, times(1)).send("flutter/textinput", message, null); + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(dartExecutor, times(1)).send(channelCaptor.capture(), bufferCaptor.capture(), any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.requestExistingInputState", null); } @Test @@ -161,33 +186,40 @@ public void setTextInputEditingState_nullInputMethodSubtype() { } @Test - public void inputConnection_createsActionFromEnter() { - Log.setLogLevel(android.util.Log.VERBOSE); + public void inputConnection_createsActionFromEnter() throws JSONException { TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); FlutterJNI mockFlutterJni = mock(FlutterJNI.class); View testView = new View(RuntimeEnvironment.application); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, dartExecutor, mock(PlatformViewsController.class)); - textInputPlugin.setTextInputClient(0, new TextInputChannel.Configuration(false, false, true, TextInputChannel.TextCapitalization.NONE, new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false), null, null)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, false, true, TextInputChannel.TextCapitalization.NONE, + new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false), null, null)); // There's a pending restart since we initialized the text input client. Flush that now. textInputPlugin.setTextInputEditingState(testView, new TextInputChannel.TextEditState("", 0, 0)); - - ByteBuffer message = JSONMethodCodec.INSTANCE.encodeMethodCall( - new MethodCall("TextInputClient.performAction", Arrays.asList(0, "TextInputAction.done"))); - verify(dartExecutor, times(1)).send("flutter/textinput", message, null); + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(dartExecutor, times(1)).send(channelCaptor.capture(), bufferCaptor.capture(), any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.requestExistingInputState", null); InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); - verify(dartExecutor, times(2)).send("flutter/textinput", message, null); + verify(dartExecutor, times(2)).send(channelCaptor.capture(), bufferCaptor.capture(), any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.performAction", new String[] {"0", "TextInputAction.done"}); connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER)); - verify(dartExecutor, times(3)).send("flutter/textinput", message, null); + verify(dartExecutor, times(3)).send(channelCaptor.capture(), bufferCaptor.capture(), any(BinaryMessenger.BinaryReply.class)); + assertEquals("flutter/textinput", channelCaptor.getValue()); + verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.performAction", new String[] {"0", "TextInputAction.done"}); connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_NUMPAD_ENTER)); } - @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; From 47914f91658f39e13ce62d113f89f50f0233057b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 14 Jan 2020 14:06:10 -0800 Subject: [PATCH 4/4] Review changes --- .../test/io/flutter/plugin/editing/TextInputPluginTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 7ae4b2fd77a7c..61f121f88eb66 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -217,7 +217,6 @@ public void inputConnection_createsActionFromEnter() throws JSONException { verify(dartExecutor, times(3)).send(channelCaptor.capture(), bufferCaptor.capture(), any(BinaryMessenger.BinaryReply.class)); assertEquals("flutter/textinput", channelCaptor.getValue()); verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.performAction", new String[] {"0", "TextInputAction.done"}); - connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_NUMPAD_ENTER)); } @Implements(InputMethodManager.class)