diff --git a/shell/platform/android/embedding_bundle/build.gradle b/shell/platform/android/embedding_bundle/build.gradle index c3eea2b22fbf4..d0db01e401c0c 100644 --- a/shell/platform/android/embedding_bundle/build.gradle +++ b/shell/platform/android/embedding_bundle/build.gradle @@ -34,6 +34,7 @@ configurations { // Use any of these configurations for dependencies required for testing the embedding. embeddingTesting embeddingTesting_v16 + embeddingTesting_v26 } android { @@ -61,6 +62,8 @@ android { embeddingTesting "org.robolectric:android-all:9-robolectric-4913185-2" // Get robolectric shadows for SDK=16 used by PlatformPluginTest. embeddingTesting_v16 "org.robolectric:android-all:4.1.2_r1-robolectric-r1" + // Get robolectric shadows for SDK=26 used by TextInputPluginTest. + embeddingTesting_v26 "org.robolectric:android-all:8.0.0_r4-robolectric-r1" embeddingTesting "androidx.fragment:fragment-testing:1.1.0" embeddingTesting "org.mockito:mockito-all:1.10.19" embeddingTesting ("org.robolectric:robolectric:4.3") { @@ -82,6 +85,7 @@ task updateDependencies() { from configurations.embedding from configurations.embeddingTesting from configurations.embeddingTesting_v16 + from configurations.embeddingTesting_v26 into destinationDir } doLast { diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 698be1c1a8b57..80ece7a89e679 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -6,6 +6,8 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; @@ -499,7 +501,27 @@ private boolean isRestartAlwaysRequired() { 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"); + if (!keyboardName.contains("Samsung")) { + return false; + } + + final long versionCode; + try { + final PackageInfo packageInfo = + mView.getContext().getPackageManager().getPackageInfo("com.sec.android.inputmethod", 0); + versionCode = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ? packageInfo.getLongVersionCode() + : packageInfo.versionCode; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "com.sec.android.inputmethod is not installed."); + return false; + } + + // 3.3.23.33 is a known version that's free of the aforementioned bug. + // 3.0.24.96 still has this bug. + // TODO(LongCatIsLooong): Find the minimum version that has the fix. + return versionCode < 332333999; } @VisibleForTesting 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 19a8b1b5be5f2..04776d714b4f0 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -18,6 +18,7 @@ import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageInfo; import android.content.res.AssetManager; import android.graphics.Insets; import android.graphics.Rect; @@ -66,6 +67,7 @@ import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -73,6 +75,7 @@ import org.robolectric.shadows.ShadowAutofillManager; import org.robolectric.shadows.ShadowBuild; import org.robolectric.shadows.ShadowInputMethodManager; +import org.robolectric.shadows.ShadowPackageManager; @Config( manifest = Config.NONE, @@ -341,14 +344,73 @@ public void setTextInputEditingState_alwaysSetEditableWhenDifferent() { assertTrue(textInputPlugin.getEditable().toString().equals("Shibuyawoo")); } - // 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. + // See also: https://github.com/flutter/flutter/issues/29341 and + // https://github.com/flutter/flutter/issues/31512. + // Some recent versions of Samsung keybords are affected including non-korean + // languages and thus needed the restart. @Test - public void setTextInputEditingState_alwaysRestartsOnAffectedDevices2() { - // Initialize a TextInputPlugin that needs to be always restarted. + public void setTextInputEditingState_alwaysRestartsOnAffectedDevices() { + // Initialize a TextInputPlugin with a Samsung keypad. + ShadowBuild.setManufacturer("samsung"); + final ShadowPackageManager packageManager = + Shadows.shadowOf( + RuntimeEnvironment.application.getApplicationContext().getPackageManager()); + final PackageInfo info = new PackageInfo(); + info.packageName = "com.sec.android.inputmethod"; + info.versionCode = 200000000; + packageManager.addPackage(info); + 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); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + TextInputChannel.TextCapitalization.NONE, + null, + null, + 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, -1, -1)); + + // Move the cursor. + assertEquals(1, testImm.getRestartCount(testView)); + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + + // Verify that we've restarted the input. + assertEquals(2, testImm.getRestartCount(testView)); + } + + // Verifies the above test does not crash on lower API levels. + @Config(sdk = 26) + @Test + public void setTextInputEditingState_alwaysRestartsOnAffectedDevicesSDK26() { + // Initialize a TextInputPlugin with a Samsung keypad. ShadowBuild.setManufacturer("samsung"); + final ShadowPackageManager packageManager = + Shadows.shadowOf( + RuntimeEnvironment.application.getApplicationContext().getPackageManager()); + final PackageInfo info = new PackageInfo(); + info.packageName = "com.sec.android.inputmethod"; + info.versionCode = 200000000; + packageManager.addPackage(info); InputMethodSubtype inputMethodSubtype = new InputMethodSubtype(0, 0, /*locale=*/ "en", "", "", false, false); Settings.Secure.putString( @@ -388,6 +450,111 @@ public void setTextInputEditingState_alwaysRestartsOnAffectedDevices2() { assertEquals(2, testImm.getRestartCount(testView)); } + // Regression test for https://github.com/flutter/flutter/issues/73433. + // The restart workaround seems to have caused #73433 and it's no longer + // needed on newer versions of Samsung keyboard. + @Test + public void setTextInputEditingState_DontForceRestartOnNewSamsungKeyboard() { + // Initialize a TextInputPlugin with a Samsung keypad. + ShadowBuild.setManufacturer("samsung"); + final ShadowPackageManager packageManager = + Shadows.shadowOf( + RuntimeEnvironment.application.getApplicationContext().getPackageManager()); + final PackageInfo info = new PackageInfo(); + info.packageName = "com.sec.android.inputmethod"; + info.versionCode = 333183070; + packageManager.addPackage(info); + 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); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + TextInputChannel.TextCapitalization.NONE, + null, + null, + 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, -1, -1)); + + // Move the cursor. + assertEquals(1, testImm.getRestartCount(testView)); + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + + // Verify that we've NOT restarted the input. + assertEquals(1, testImm.getRestartCount(testView)); + } + + // Verifies the above test does not crash on lower API levels. + @Config(sdk = 26) + @Test + public void setTextInputEditingState_DontForceRestartOnNewSamsungKeyboardSDK26() { + // Initialize a TextInputPlugin with a Samsung keypad. + ShadowBuild.setManufacturer("samsung"); + final ShadowPackageManager packageManager = + Shadows.shadowOf( + RuntimeEnvironment.application.getApplicationContext().getPackageManager()); + final PackageInfo info = new PackageInfo(); + info.packageName = "com.sec.android.inputmethod"; + info.versionCode = 333183070; + packageManager.addPackage(info); + 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); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + TextInputChannel.TextCapitalization.NONE, + null, + null, + 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, -1, -1)); + + // Move the cursor. + assertEquals(1, testImm.getRestartCount(testView)); + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + + // Verify that we've NOT restarted the input. + assertEquals(1, testImm.getRestartCount(testView)); + } + @Test public void setTextInputEditingState_doesNotRestartOnUnaffectedDevices() { // Initialize a TextInputPlugin that needs to be always restarted. @@ -427,7 +594,7 @@ public void setTextInputEditingState_doesNotRestartOnUnaffectedDevices() { textInputPlugin.setTextInputEditingState( testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - // Verify that we've restarted the input. + // Verify that we've NOT restarted the input. assertEquals(1, testImm.getRestartCount(testView)); }