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); }