Remove extraneous window inset call on IME animation (#21213)

diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index c11d264..62abf76 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -199,7 +199,17 @@
 
     private View view;
     private WindowInsets lastWindowInsets;
-    private boolean started = false;
+    // True when an animation that matches deferredInsetTypes is active.
+    //
+    // While this is active, this class will capture the initial window inset
+    // sent into lastWindowInsets by flagging needsSave to true, and will hold
+    // onto the intitial inset until the animation is completed, when it will
+    // re-dispatch the inset change.
+    private boolean animating = false;
+    // When an animation begins, android sends a WindowInset with the final
+    // state of the animation. When needsSave is true, we know to capture this
+    // initial WindowInset.
+    private boolean needsSave = false;
 
     ImeSyncDeferringInsetsCallback(
         @NonNull View view, int overlayInsetTypes, int deferredInsetTypes) {
@@ -212,34 +222,38 @@
     @Override
     public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
       this.view = view;
-      if (started) {
+      if (needsSave) {
+        // Store the view and insets for us in onEnd() below. This captured inset
+        // is not part of the animation and instead, represents the final state
+        // of the inset after the animation is completed. Thus, we defer the processing
+        // of this WindowInset until the animation completes.
+        lastWindowInsets = windowInsets;
+        needsSave = false;
+      }
+      if (animating) {
         // While animation is running, we consume the insets to prevent disrupting
         // the animation, which skips this implementation and calls the view's
         // onApplyWindowInsets directly to avoid being consumed here.
         return WindowInsets.CONSUMED;
       }
 
-      // Store the view and insets for us in onEnd() below
-      lastWindowInsets = windowInsets;
-
       // If no animation is happening, pass the insets on to the view's own
       // inset handling.
       return view.onApplyWindowInsets(windowInsets);
     }
 
     @Override
-    public WindowInsetsAnimation.Bounds onStart(
-        WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) {
+    public void onPrepare(WindowInsetsAnimation animation) {
       if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
-        started = true;
+        animating = true;
+        needsSave = true;
       }
-      return bounds;
     }
 
     @Override
     public WindowInsets onProgress(
         WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
-      if (!started) {
+      if (!animating || needsSave) {
         return insets;
       }
       boolean matching = false;
@@ -280,10 +294,10 @@
 
     @Override
     public void onEnd(WindowInsetsAnimation animation) {
-      if (started && (animation.getTypeMask() & deferredInsetTypes) != 0) {
+      if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
         // If we deferred the IME insets and an IME animation has finished, we need to reset
         // the flags
-        started = false;
+        animating = false;
 
         // And finally dispatch the deferred insets to the view now.
         // Ideally we would just call view.requestApplyInsets() and let the normal dispatch
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 562f1f5..b038eb5 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
@@ -669,6 +669,8 @@
     WindowInsets.Builder builder = new WindowInsets.Builder();
     WindowInsets noneInsets = builder.build();
 
+    // imeInsets0, 1, and 2 contain unique IME bottom insets, and are used
+    // to distinguish which insets were sent at each stage.
     builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100));
     builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
     WindowInsets imeInsets0 = builder.build();
@@ -677,6 +679,10 @@
     builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
     WindowInsets imeInsets1 = builder.build();
 
+    builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50));
+    builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 40));
+    WindowInsets imeInsets2 = builder.build();
+
     builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 200));
     builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(10, 10, 10, 0));
     WindowInsets deferredInsets = builder.build();
@@ -696,6 +702,8 @@
     imeSyncCallback.onPrepare(animation);
     imeSyncCallback.onApplyWindowInsets(testView, deferredInsets);
     imeSyncCallback.onStart(animation, null);
+    // Only the final state call is saved, extra calls are passed on.
+    imeSyncCallback.onApplyWindowInsets(testView, imeInsets2);
 
     verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
     // No change, as deferredInset is stored to be passed in onEnd()
@@ -723,7 +731,7 @@
     imeSyncCallback.onEnd(animation);
 
     verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
-    // Values should be of deferredInsets
+    // Values should be of deferredInsets, not imeInsets2
     assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom);
     assertEquals(10, viewportMetricsCaptor.getValue().paddingTop);
     assertEquals(200, viewportMetricsCaptor.getValue().viewInsetBottom);