diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 9da7c58768bd8..5c23ea40beca9 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -7,6 +7,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; +import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; @@ -315,7 +316,9 @@ private void applyStateToSelection(TextInputChannel.TextEditState state) { } // Samsung's Korean keyboard has a bug where it always attempts to combine characters based on - // its internal state, ignoring if and when the cursor is moved programmatically. + // its internal state, ignoring if and when the cursor is moved programmatically. The same bug + // also causes non-korean keyboards to occasionally duplicate text when tapping in the middle + // of existing text to edit it. // // Fully restarting the IMM works around this because it flushes the keyboard's internal state // and stops it from trying to incorrectly combine characters. However this also has some @@ -324,13 +327,14 @@ private void applyStateToSelection(TextInputChannel.TextEditState state) { @SuppressWarnings("deprecation") private boolean isRestartAlwaysRequired() { InputMethodSubtype subtype = mImm.getCurrentInputMethodSubtype(); - if (subtype == null) { + // Impacted devices all shipped with Android Lollipop or newer. + if (subtype == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !Build.MANUFACTURER.equals("samsung")) { return false; } - String language = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - ? subtype.getLanguageTag() - : subtype.getLocale(); - return Build.MANUFACTURER.equals("samsung") && language.equals("ko"); + String keyboardName = Settings.Secure.getString(mView.getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); + // The Samsung keyboard is called "com.sec.android.inputmethod/.SamsungKeypad" but look + // for "Samsung" just in case Samsung changes the name of the keyboard. + return keyboardName.contains("Samsung"); } private void clearTextInputClient() { 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 a0598380b70f6..cd5eb377861dc 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -1,6 +1,7 @@ package io.flutter.plugin.editing; import android.content.Context; +import android.provider.Settings; import android.util.SparseIntArray; import android.view.View; import android.view.inputmethod.InputMethodManager; @@ -24,7 +25,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -@Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class) +@Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class, sdk = 27) @RunWith(RobolectricTestRunner.class) public class TextInputPluginTest { @Test @@ -47,12 +48,15 @@ public void setTextInputEditingState_doesNotRestartWhenTextIsIdentical() { assertEquals(1, testImm.getRestartCount(testView)); } - // See https://github.com/flutter/flutter/issues/29341 + // See https://github.com/flutter/flutter/issues/29341 and https://github.com/flutter/flutter/issues/31512 + // All modern Samsung keybords are affected including non-korean languages and thus + // need the restart. @Test - public void setTextInputEditingState_alwaysRestartsOnAffectedDevices() { + public void setTextInputEditingState_alwaysRestartsOnAffectedDevices2() { // Initialize a TextInputPlugin that needs to be always restarted. ShadowBuild.setManufacturer("samsung"); - InputMethodSubtype inputMethodSubtype = new InputMethodSubtype(0, 0, /*locale=*/"ko", "", "", false, false); + InputMethodSubtype inputMethodSubtype = new InputMethodSubtype(0, 0, /*locale=*/"en", "", "", false, false); + Settings.Secure.putString(RuntimeEnvironment.application.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, "com.sec.android.inputmethod/.SamsungKeypad"); TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(RuntimeEnvironment.application); @@ -69,6 +73,28 @@ public void setTextInputEditingState_alwaysRestartsOnAffectedDevices() { assertEquals(2, testImm.getRestartCount(testView)); } + @Test + public void setTextInputEditingState_doesNotRestartOnUnaffectedDevices() { + // Initialize a TextInputPlugin that needs to be always restarted. + ShadowBuild.setManufacturer("samsung"); + InputMethodSubtype inputMethodSubtype = new InputMethodSubtype(0, 0, /*locale=*/"en", "", "", false, false); + Settings.Secure.putString(RuntimeEnvironment.application.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, "com.fake.test.blah/.NotTheRightKeyboard"); + TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setCurrentInputMethodSubtype(inputMethodSubtype); + View testView = new View(RuntimeEnvironment.application); + TextInputPlugin textInputPlugin = new TextInputPlugin(testView, mock(DartExecutor.class), mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient(0, new TextInputChannel.Configuration(false, false, TextInputChannel.TextCapitalization.NONE, null, 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)); + + // Move the cursor. + assertEquals(1, testImm.getRestartCount(testView)); + textInputPlugin.setTextInputEditingState(testView, new TextInputChannel.TextEditState("", 0, 0)); + + // Verify that we've restarted the input. + assertEquals(1, testImm.getRestartCount(testView)); + } + @Test public void setTextInputEditingState_nullInputMethodSubtype() { TestImm testImm = Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE));