From 72bfcbb0e2e29cf68b9d172f4024fed1a9df32ae Mon Sep 17 00:00:00 2001 From: guolinaileen <2993595+guolinaileen@users.noreply.github.com> Date: Mon, 10 Aug 2020 14:04:11 -0700 Subject: [PATCH] Add TextInput performPrivateCommand to Flutter Engine (flutter/engine#20188) New command for Crowdsource 2/2 --- .../systemchannels/TextInputChannel.java | 33 ++ .../editing/InputConnectionAdaptor.java | 7 + .../editing/InputConnectionAdaptorTest.java | 312 ++++++++++++++++++ 3 files changed, 352 insertions(+) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 78f6ec86e61..b05921c84bb 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -14,6 +14,7 @@ import io.flutter.plugin.common.MethodChannel; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -283,6 +284,38 @@ public class TextInputChannel { Arrays.asList(inputClientId, "TextInputAction.unspecified")); } + public void performPrivateCommand(int inputClientId, String action, Bundle data) { + HashMap json = new HashMap<>(); + json.put("action", action); + if (data != null) { + HashMap dataMap = new HashMap<>(); + Set keySet = data.keySet(); + for (String key : keySet) { + Object value = data.get(key); + if (value instanceof byte[]) { + dataMap.put(key, data.getByteArray(key)); + } else if (value instanceof Byte) { + dataMap.put(key, data.getByte(key)); + } else if (value instanceof char[]) { + dataMap.put(key, data.getCharArray(key)); + } else if (value instanceof Character) { + dataMap.put(key, data.getChar(key)); + } else if (value instanceof CharSequence[]) { + dataMap.put(key, data.getCharSequenceArray(key)); + } else if (value instanceof CharSequence) { + dataMap.put(key, data.getCharSequence(key)); + } else if (value instanceof float[]) { + dataMap.put(key, data.getFloatArray(key)); + } else if (value instanceof Float) { + dataMap.put(key, data.getFloat(key)); + } + } + json.put("data", dataMap); + } + channel.invokeMethod( + "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed * from the underlying platform channel. diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 29b3fb859e1..ab4ed5c6b1d 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -9,6 +9,7 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.text.DynamicLayout; import android.text.Editable; @@ -477,6 +478,12 @@ class InputConnectionAdaptor extends BaseInputConnection { return false; } + @Override + public boolean performPrivateCommand(String action, Bundle data) { + textInputChannel.performPrivateCommand(mClient, action, data); + return true; + } + @Override public boolean performEditorAction(int actionCode) { markDirty(); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index d3afb22e597..458e75b7cfb 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -3,6 +3,7 @@ package io.flutter.plugin.editing; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.eq; @@ -14,6 +15,7 @@ import static org.mockito.Mockito.when; import android.content.ClipboardManager; import android.content.res.AssetManager; +import android.os.Bundle; import android.text.Editable; import android.text.Emoji; import android.text.InputType; @@ -26,9 +28,16 @@ import android.view.inputmethod.ExtractedText; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; 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.util.FakeKeyEvent; +import java.nio.ByteBuffer; +import org.json.JSONArray; +import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -37,6 +46,21 @@ import org.robolectric.shadows.ShadowClipboardManager; @Config(manifest = Config.NONE, shadows = ShadowClipboardManager.class) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { + // 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 inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -125,6 +149,294 @@ public class InputConnectionAdaptorTest { assertTrue(editable.toString().startsWith(textToBePasted)); } + @Test + public void testPerformPrivateCommand_dataIsNull() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + adaptor.performPrivateCommand("actionCommand", 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.performPrivateCommand", + new String[] {"0", "{\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsByteArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte[] buffer = new byte[] {'a', 'b', 'c', 'd'}; + bundle.putByteArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[97,98,99,100]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsByte() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + byte b = 3; + bundle.putByte("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":3},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char[] buffer = new char[] {'a', 'b', 'c', 'd'}; + bundle.putCharArray("keyboard_layout", buffer); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] { + "0", + "{\"data\":{\"keyboard_layout\":[\"a\",\"b\",\"c\",\"d\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsChar() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + char b = 'a'; + bundle.putChar("keyboard_layout", b); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":\"a\"},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence1 = new StringBuffer("abc"); + CharSequence charSequence2 = new StringBuffer("efg"); + CharSequence[] value = {charSequence1, charSequence2}; + bundle.putCharSequenceArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[\"abc\",\"efg\"]},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + CharSequence charSequence = new StringBuffer("abc"); + bundle.putCharSequence("keyboard_layout", charSequence); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":\"abc\"},\"action\":\"actionCommand\"}" + }); + } + + @Test + public void testPerformPrivateCommand_dataIsFloat() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float value = 0.5f; + bundle.putFloat("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] {"0", "{\"data\":{\"keyboard_layout\":0.5},\"action\":\"actionCommand\"}"}); + } + + @Test + public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException { + View testView = new View(RuntimeEnvironment.application); + int client = 0; + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + Editable editable = sampleEditable(0, 0); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, client, textInputChannel, editable, null, mockFlutterJNI); + + Bundle bundle = new Bundle(); + float[] value = {0.5f, 0.6f}; + bundle.putFloatArray("keyboard_layout", value); + adaptor.performPrivateCommand("actionCommand", bundle); + + 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.performPrivateCommand", + new String[] { + "0", "{\"data\":{\"keyboard_layout\":[0.5,0.6]},\"action\":\"actionCommand\"}" + }); + } + @Test public void testSendKeyEvent_shiftKeyUpCancelsSelection() { int selStart = 5;