diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 2568032a7aaa1..f158ef5ba4525 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -407,6 +407,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/android/FlutterActivityTest.java", "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", + "test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java", "test/io/flutter/util/PreconditionsTest.java", ] diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 55386eeba5b45..2a409619c1143 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import io.flutter.Log; @@ -222,6 +223,17 @@ public void unspecifiedAction(int inputClientId) { ); } + /** + * Instructs Flutter to clear the current input client, which ends the text + * input interaction with the given input control. + */ + public void onConnectionClosed(int inputClientId) { + channel.invokeMethod( + "TextInputClient.onConnectionClosed", + Collections.singletonList(inputClientId) + ); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests * that are parsed from the underlying platform channel. diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index f0d337a77f389..6d3768183cf2f 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -5,10 +5,10 @@ package io.flutter.plugin.editing; import android.content.Context; +import android.support.annotation.NonNull; import android.text.DynamicLayout; import android.text.Editable; import android.text.Layout; -import android.text.Layout.Directions; import android.text.Selection; import android.text.TextPaint; import android.view.KeyEvent; @@ -19,30 +19,37 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.Log; -import io.flutter.plugin.common.ErrorLogResult; -import io.flutter.plugin.common.MethodChannel; class InputConnectionAdaptor extends BaseInputConnection { + @NonNull private final View mFlutterView; private final int mClient; + @NonNull private final TextInputChannel textInputChannel; + @NonNull private final Editable mEditable; + @NonNull + private final Runnable onConnectionClosed; private int mBatchCount; + @NonNull private InputMethodManager mImm; + @NonNull private final Layout mLayout; @SuppressWarnings("deprecation") public InputConnectionAdaptor( - View view, + @NonNull View view, int client, - TextInputChannel textInputChannel, - Editable editable + @NonNull TextInputChannel textInputChannel, + @NonNull Editable editable, + @NonNull Runnable onConnectionClosed ) { super(view, true); mFlutterView = view; mClient = client; this.textInputChannel = textInputChannel; mEditable = editable; + this.onConnectionClosed = onConnectionClosed; mBatchCount = 0; // We create a dummy Layout with max width so that the selection // shifting acts as if all text were in one line. @@ -50,6 +57,13 @@ public InputConnectionAdaptor( mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); } + @Override + public void closeConnection() { + super.closeConnection(); + textInputChannel.onConnectionClosed(mClient); + onConnectionClosed.run(); + } + // Send the current state of the editable to Flutter. private void updateEditingState() { // If the IME is in the middle of a batch edit, then wait until it completes. diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index a3affe66c3a3d..0efd1934776b1 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -46,6 +46,13 @@ public class TextInputPlugin { // target is a platform view. See the comments on lockPlatformViewInputConnection for more details. private boolean isInputConnectionLocked; + private final Runnable onInputConnectionClosed = new Runnable() { + @Override + public void run() { + clearTextInputClient(); + } + }; + public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, @NonNull PlatformViewsController platformViewsController) { mView = view; mImm = (InputMethodManager) view.getContext().getSystemService( @@ -219,7 +226,8 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { view, inputTarget.id, textInputChannel, - mEditable + mEditable, + onInputConnectionClosed ); outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 07815c071179a..6c95754ccb402 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -12,6 +12,7 @@ import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; +import io.flutter.embedding.engine.systemchannels.TextInputChannelTest; import io.flutter.util.PreconditionsTest; @RunWith(Suite.class) @@ -21,7 +22,8 @@ FlutterActivityTest.class, FlutterFragmentTest.class, // FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this - FlutterEngineCacheTest.class + FlutterEngineCacheTest.class, + TextInputChannelTest.class }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ public class FlutterTestSuite {} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java new file mode 100644 index 0000000000000..f6820d93e6545 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java @@ -0,0 +1,89 @@ +package io.flutter.embedding.engine.systemchannels; + +import org.hamcrest.Description; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.nio.ByteBuffer; +import java.util.Collections; + +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class TextInputChannelTest { + @Test + public void itNotifiesFrameworkWhenPlatformClosesInputConnection() { + // Setup test. + final int INPUT_CLIENT_ID = 9; // Arbitrary integer. + DartExecutor dartExecutor = mock(DartExecutor.class); + + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + + // Execute behavior under test. + textInputChannel.onConnectionClosed(INPUT_CLIENT_ID); + + // Verify results. + verify(dartExecutor, times(1)).send( + eq("flutter/textinput"), + ByteBufferMatcher.eqByteBuffer(JSONMethodCodec.INSTANCE.encodeMethodCall( + new MethodCall( + "TextInputClient.onConnectionClosed", + Collections.singletonList(INPUT_CLIENT_ID) + ) + )), + eq(null) + ); + } + + /** + * Mockito matcher that compares two {@link ByteBuffer}s by resetting both buffers and then + * utilizing their standard {@code equals()} method. + *
+ * This matcher will change the state of the expected and actual buffers. The exact change in
+ * state depends on where the comparison fails or succeeds.
+ */
+ static class ByteBufferMatcher extends ArgumentMatcher