diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index d7f79d074e93d..b52497884cd55 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -5,6 +5,7 @@ package io.flutter.plugin.editing; import android.content.Context; +import android.os.Build; import android.text.DynamicLayout; import android.text.Editable; import android.text.Layout; @@ -14,6 +15,7 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; @@ -133,6 +135,24 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { return result; } + @Override + public boolean finishComposingText() { + boolean result = super.finishComposingText(); + + if (Build.VERSION.SDK_INT >= 21) { + // Update the keyboard with a reset/empty composing region. Critical on + // Samsung keyboards to prevent punctuation duplication. + CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); + builder.setComposingText(-1, ""); + CursorAnchorInfo anchorInfo = builder.build(); + mImm.updateCursorAnchorInfo(mFlutterView, anchorInfo); + } + + updateEditingState(); + return result; + } + + @Override public boolean setSelection(int start, int end) { boolean result = super.setSelection(start, end); 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 61f121f88eb66..e55867a3d6eeb 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2,8 +2,10 @@ import android.content.Context; import android.content.res.AssetManager; +import android.os.Build; import android.provider.Settings; import android.util.SparseIntArray; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; @@ -219,10 +221,37 @@ public void inputConnection_createsActionFromEnter() throws JSONException { verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.performAction", new String[] {"0", "TextInputAction.done"}); } + @Test + public void inputConnection_finishComposingTextUpdatesIMM() 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)); + // There's a pending restart since we initialized the text input client. Flush that now. + textInputPlugin.setTextInputEditingState(testView, new TextInputChannel.TextEditState("", 0, 0)); + InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); + + connection.finishComposingText(); + + if (Build.VERSION.SDK_INT >= 21) { + CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); + builder.setComposingText(-1, ""); + CursorAnchorInfo anchorInfo = builder.build(); + assertEquals(testImm.getLastCursorAnchorInfo(), anchorInfo); + } + } + @Implements(InputMethodManager.class) public static class TestImm extends ShadowInputMethodManager { private InputMethodSubtype currentInputMethodSubtype; private SparseIntArray restartCounter = new SparseIntArray(); + private CursorAnchorInfo cursorAnchorInfo; public TestImm() { } @@ -245,5 +274,14 @@ public void setCurrentInputMethodSubtype(InputMethodSubtype inputMethodSubtype) public int getRestartCount(View view) { return restartCounter.get(view.hashCode(), /*defaultValue=*/0); } + + @Implementation + public void updateCursorAnchorInfo(View view, CursorAnchorInfo cursorAnchorInfo) { + this.cursorAnchorInfo = cursorAnchorInfo; + } + + public CursorAnchorInfo getLastCursorAnchorInfo() { + return cursorAnchorInfo; + } } }