From a6de6785ef3899f02c0264478bf75eb3613306ca Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Thu, 16 Jan 2020 18:04:58 -0800 Subject: [PATCH] Samsung fix duplication on punctuation: Update keyboard on finish compose. (flutter/engine#15701) --- .../editing/InputConnectionAdaptor.java | 20 ++++++++++ .../plugin/editing/TextInputPluginTest.java | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+) 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 d7f79d074e9..b52497884cd 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 @@ -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.text.TextPaint; 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 @@ class InputConnectionAdaptor extends BaseInputConnection { 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/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 61f121f88eb..e55867a3d6e 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2,8 +2,10 @@ package io.flutter.plugin.editing; 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 class TextInputPluginTest { 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 class TextInputPluginTest { 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; + } } }