From c97a0deccbc139a133fcb7830fc82d330568e4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=98=B1?= <30322203+ksballetba@users.noreply.github.com> Date: Fri, 5 May 2023 10:55:05 +0800 Subject: [PATCH] Close connection on keyboard close (#41500) Fix: - https://github.com/flutter/flutter/issues/123523 - https://github.com/flutter/flutter/issues/124890 ### Before this patch: https://user-images.githubusercontent.com/30322203/228413196-29c57bb0-3220-495b-9e73-f58777de440f.mp4 ### After this patch: https://user-images.githubusercontent.com/30322203/228413249-fc06f49d-6579-4476-9788-90f12a53b8c3.mp4 --- .../systemchannels/TextInputChannel.java | 8 +++++ .../ImeSyncDeferringInsetsCallback.java | 36 ++++++++++++++++++- .../plugin/editing/TextInputPlugin.java | 15 ++++++++ .../plugin/editing/TextInputPluginTest.java | 13 +++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index e0f95681f3b9c..687d1f29a2a30 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -367,6 +367,14 @@ public void performPrivateCommand( "TextInputClient.performPrivateCommand", Arrays.asList(inputClientId, json)); } + /** Instructs Flutter to execute a "onConnectionClosed" action. */ + public void onConnectionClosed(int inputClientId) { + Log.v(TAG, "Sending 'onConnectionClosed' message."); + channel.invokeMethod( + "TextInputClient.onConnectionClosed", + Arrays.asList(inputClientId, "TextInputClient.onConnectionClosed")); + } + /** * Sets the {@link TextInputMethodHandler} which receives all events and requests that are parsed * from the underlying platform channel. diff --git a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java index 0a6f0ef3f64db..58694c17631bf 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java +++ b/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java @@ -14,6 +14,8 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.List; // Loosely based off of @@ -41,7 +43,9 @@ // a no-op. When onEnd indicates the end of the animation, the deferred call is // dispatched again, this time avoiding any flicker since the animation is now // complete. -@VisibleForTesting + +// This class should have "package private" visibility cause it's called from TextInputPlugin. +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @TargetApi(30) @RequiresApi(30) @SuppressLint({"NewApi", "Override"}) @@ -54,6 +58,7 @@ class ImeSyncDeferringInsetsCallback { private WindowInsets lastWindowInsets; private AnimationCallback animationCallback; private InsetsListener insetsListener; + private ImeVisibleListener imeVisibleListener; // True when an animation that matches deferredInsetTypes is active. // @@ -88,6 +93,11 @@ void remove() { view.setOnApplyWindowInsetsListener(null); } + // Set a listener to be notified when the IME visibility changes. + void setImeVisibleListener(ImeVisibleListener imeVisibleListener) { + this.imeVisibleListener = imeVisibleListener; + } + @VisibleForTesting View.OnApplyWindowInsetsListener getInsetsListener() { return insetsListener; @@ -98,6 +108,11 @@ WindowInsetsAnimation.Callback getAnimationCallback() { return animationCallback; } + @VisibleForTesting + ImeVisibleListener getImeVisibleListener() { + return imeVisibleListener; + } + // WindowInsetsAnimation.Callback was introduced in API level 30. The callback // subclass is separated into an inner class in order to avoid warnings from // the Android class loader on older platforms. @@ -115,6 +130,20 @@ public void onPrepare(WindowInsetsAnimation animation) { } } + @NonNull + @Override + public WindowInsetsAnimation.Bounds onStart( + @NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { + // Observe changes to software keyboard visibility and notify listener when animation start. + // See https://developer.android.com/develop/ui/views/layout/sw-keyboard. + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view); + if (insets != null && imeVisibleListener != null) { + boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + imeVisibleListener.onImeVisibleChanged(imeVisible); + } + return super.onStart(animation, bounds); + } + @Override public WindowInsets onProgress( WindowInsets insets, List runningAnimations) { @@ -199,4 +228,9 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { return view.onApplyWindowInsets(windowInsets); } } + + // Listener for IME visibility changes. + public interface ImeVisibleListener { + void onImeVisibleChanged(boolean visible); + } } diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 10cbc1b65cb61..042d3d2070c4f 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -94,6 +94,17 @@ public TextInputPlugin( WindowInsets.Type.ime() // Deferred, insets that will animate ); imeSyncCallback.install(); + + // When the IME is hidden, we need to notify the framework that close connection. + imeSyncCallback.setImeVisibleListener( + new ImeSyncDeferringInsetsCallback.ImeVisibleListener() { + @Override + public void onImeVisibleChanged(boolean visible) { + if (!visible) { + onConnectionClosed(); + } + } + }); } this.textInputChannel = textInputChannel; @@ -838,4 +849,8 @@ public void autofill(@NonNull SparseArray values) { textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues); } // -------- End: Autofill ------- + + public void onConnectionClosed() { + textInputChannel.onConnectionClosed(inputTarget.id); + } } 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 462b6ec2ff808..9459a5701f657 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2118,6 +2118,19 @@ public void ime_windowInsetsSync() { assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop); } + @Test + @TargetApi(30) + @Config(sdk = 30) + public void onConnectionClosed_imeInvisible() { + View testView = new View(ctx); + TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + imeSyncCallback.getImeVisibleListener().onImeVisibleChanged(false); + verify(textInputChannel, times(1)).onConnectionClosed(anyInt()); + } + interface EventHandler { void sendAppPrivateCommand(View view, String action, Bundle data); }