Skip to content

Commit 0abc2ec

Browse files
Renzo-OlivaresRenzo Olivares
andauthored
Fix previous text input connection interrupts enter/space (flutter#171973)
Fixes an issue where a previously closed text input connection may cause interference with hardware keyboard on Samsung devices. On the Samsung Galaxy S10 Tab the Samsung IME will request the operating system to show the IME when pressing enter/space on components like a radio option or a checkbox if there was a previously opened text input connection. It seems the Samsung Keyboard is caching some text input related state, and when it detects an enter/space key it acts on this previously cached state even if the text input connection has been closed. This prevents the radio option/checkbox and framework as a whole from responding to the key events. To solve this issue we should reset the input method manager when the IME has been hidden, and when the framework closes the input connection we should also reset the input method manager if the IME is hidden at the time of closing. This effectively resets any state cached by the Samsung keyboard and it no longer acts on stale state. Fixes flutter#168099 Fixes flutter#51478 Fixes flutter#70546 Related: flutter#136745 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares <[email protected]>
1 parent 75b6c0f commit 0abc2ec

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed

engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import androidx.annotation.NonNull;
1616
import androidx.annotation.RequiresApi;
1717
import androidx.annotation.VisibleForTesting;
18+
import androidx.core.view.ViewCompat;
19+
import androidx.core.view.WindowInsetsCompat;
1820
import java.util.List;
1921

2022
// Loosely based off of
@@ -52,6 +54,7 @@ class ImeSyncDeferringInsetsCallback {
5254
private WindowInsets lastWindowInsets;
5355
private AnimationCallback animationCallback;
5456
private InsetsListener insetsListener;
57+
private ImeVisibilityListener imeVisibilityListener;
5558

5659
// True when an animation that matches deferredInsetTypes is active.
5760
//
@@ -89,6 +92,11 @@ void remove() {
8992
view.setOnApplyWindowInsetsListener(null);
9093
}
9194

95+
// Set a listener to be notified when the IME visibility changes.
96+
void setImeVisibilityListener(ImeVisibilityListener imeVisibilityListener) {
97+
this.imeVisibilityListener = imeVisibilityListener;
98+
}
99+
92100
@VisibleForTesting
93101
View.OnApplyWindowInsetsListener getInsetsListener() {
94102
return insetsListener;
@@ -99,6 +107,11 @@ WindowInsetsAnimation.Callback getAnimationCallback() {
99107
return animationCallback;
100108
}
101109

110+
@VisibleForTesting
111+
ImeVisibilityListener getImeVisibilityListener() {
112+
return imeVisibilityListener;
113+
}
114+
102115
// WindowInsetsAnimation.Callback was introduced in API level 30. The callback
103116
// subclass is separated into an inner class in order to avoid warnings from
104117
// the Android class loader on older platforms.
@@ -173,6 +186,11 @@ public void onEnd(WindowInsetsAnimation animation) {
173186
view.dispatchApplyWindowInsets(lastWindowInsets);
174187
}
175188
}
189+
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
190+
if (insets != null && imeVisibilityListener != null) {
191+
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
192+
imeVisibilityListener.onImeVisibilityChanged(imeVisible);
193+
}
176194
}
177195
}
178196

@@ -200,4 +218,9 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
200218
return view.onApplyWindowInsets(windowInsets);
201219
}
202220
}
221+
222+
// Listener for IME visibility changes.
223+
public interface ImeVisibilityListener {
224+
void onImeVisibilityChanged(boolean visible);
225+
}
203226
}

engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import androidx.annotation.NonNull;
2828
import androidx.annotation.Nullable;
2929
import androidx.annotation.VisibleForTesting;
30+
import androidx.core.view.ViewCompat;
31+
import androidx.core.view.WindowInsetsCompat;
3032
import androidx.core.view.inputmethod.EditorInfoCompat;
3133
import io.flutter.Log;
3234
import io.flutter.embedding.android.KeyboardManager;
@@ -90,6 +92,18 @@ public TextInputPlugin(
9092
if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
9193
imeSyncCallback = new ImeSyncDeferringInsetsCallback(view);
9294
imeSyncCallback.install();
95+
96+
// When the IME is hidden, we need to restart the input method manager to accomodate
97+
// some keyboards like the Samsung keyboard that may be caching old state.
98+
imeSyncCallback.setImeVisibilityListener(
99+
new ImeSyncDeferringInsetsCallback.ImeVisibilityListener() {
100+
@Override
101+
public void onImeVisibilityChanged(boolean visible) {
102+
if (!visible) {
103+
mImm.restartInput(mView);
104+
}
105+
}
106+
});
93107
}
94108

95109
this.textInputChannel = textInputChannel;
@@ -583,6 +597,12 @@ void clearTextInputClient() {
583597
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
584598
unlockPlatformViewInputConnection();
585599
lastClientRect = null;
600+
// When the IME is hidden, we need to restart the input method manager to accomodate
601+
// some keyboards like the Samsung keyboard that may be caching old state.
602+
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(mView);
603+
if (insets != null && !insets.isVisible(WindowInsetsCompat.Type.ime())) {
604+
mImm.restartInput(mView);
605+
}
586606
}
587607

588608
private static class InputTarget {

engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,121 @@ public void setTextInputEditingState_nullInputMethodSubtype() {
12301230
assertEquals(1, testImm.getRestartCount(testView));
12311231
}
12321232

1233+
@Test
1234+
public void imeVisibilityListener_restartsImmWhenIMEHidden() {
1235+
// Initialize a general TextInputPlugin.
1236+
InputMethodSubtype inputMethodSubtype = mock(InputMethodSubtype.class);
1237+
TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
1238+
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
1239+
View testView = new View(ctx);
1240+
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
1241+
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
1242+
TextInputPlugin textInputPlugin =
1243+
new TextInputPlugin(
1244+
testView,
1245+
textInputChannel,
1246+
scribeChannel,
1247+
mock(PlatformViewsController.class),
1248+
mock(PlatformViewsController2.class));
1249+
textInputPlugin.setTextInputClient(
1250+
0,
1251+
new TextInputChannel.Configuration(
1252+
false,
1253+
false,
1254+
true,
1255+
true,
1256+
false,
1257+
TextInputChannel.TextCapitalization.NONE,
1258+
null,
1259+
null,
1260+
null,
1261+
null,
1262+
null,
1263+
null,
1264+
null));
1265+
// There's a pending restart since we initialized the text input client. Flush that now.
1266+
textInputPlugin.setTextInputEditingState(
1267+
testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
1268+
assertEquals(1, testImm.getRestartCount(testView));
1269+
1270+
// Imm restarts when IME is hidden.
1271+
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
1272+
imeSyncCallback.getImeVisibilityListener().onImeVisibilityChanged(false);
1273+
assertEquals(2, testImm.getRestartCount(testView));
1274+
}
1275+
1276+
@Test
1277+
public void clearTextInputClient_restartsImmWhenIMEHidden() {
1278+
try (ActivityScenario<Activity> scenario = ActivityScenario.launch(Activity.class)) {
1279+
scenario.onActivity(
1280+
activity -> {
1281+
FlutterView testView = spy(new FlutterView(activity));
1282+
1283+
InputMethodSubtype inputMethodSubtype = mock(InputMethodSubtype.class);
1284+
TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
1285+
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
1286+
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
1287+
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
1288+
TextInputPlugin textInputPlugin =
1289+
new TextInputPlugin(
1290+
testView,
1291+
textInputChannel,
1292+
scribeChannel,
1293+
mock(PlatformViewsController.class),
1294+
mock(PlatformViewsController2.class));
1295+
textInputPlugin.setTextInputClient(
1296+
0,
1297+
new TextInputChannel.Configuration(
1298+
false,
1299+
false,
1300+
true,
1301+
true,
1302+
false,
1303+
TextInputChannel.TextCapitalization.NONE,
1304+
null,
1305+
null,
1306+
null,
1307+
null,
1308+
null,
1309+
null,
1310+
null));
1311+
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
1312+
FlutterEngine flutterEngine =
1313+
spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
1314+
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
1315+
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);
1316+
assertEquals(0, testImm.getRestartCount(testView));
1317+
// FlutterView restarts the input method to inform the Android framework that an engine
1318+
// has
1319+
// been attached.
1320+
testView.attachToFlutterEngine(flutterEngine);
1321+
assertEquals(1, testImm.getRestartCount(testView));
1322+
1323+
ArgumentCaptor<FlutterRenderer.ViewportMetrics> viewportMetricsCaptor =
1324+
ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class);
1325+
1326+
WindowInsets.Builder builder = new WindowInsets.Builder();
1327+
1328+
// There's a pending restart since we initialized the text input client. Flush that now.
1329+
textInputPlugin.setTextInputEditingState(
1330+
testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
1331+
assertEquals(2, testImm.getRestartCount(testView));
1332+
1333+
// Set the initial insets and verify that they were set and the bottom view inset is
1334+
// correct.
1335+
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0));
1336+
when(testView.getRootWindowInsets()).thenReturn(builder.build());
1337+
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build());
1338+
verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
1339+
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom);
1340+
1341+
// Imm restarts when clearTextInputClient is called while the IME is hidden.
1342+
textInputPlugin.clearTextInputClient();
1343+
assertEquals(3, testImm.getRestartCount(testView));
1344+
});
1345+
}
1346+
}
1347+
12331348
@Test
12341349
public void destroy_clearTextInputMethodHandler() {
12351350
View testView = new View(ctx);

0 commit comments

Comments
 (0)