Merge "Added WindowInsets IME animation source and target" into androidx-main
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 75303a4..f788920 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -310,6 +310,8 @@
     method public static boolean getConsumeWindowInsets(androidx.compose.ui.platform.ComposeView);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getImeAnimationSource(androidx.compose.foundation.layout.WindowInsets.Companion);
+    method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getImeAnimationTarget(androidx.compose.foundation.layout.WindowInsets.Companion);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
     method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBarsIgnoringVisibility(androidx.compose.foundation.layout.WindowInsets.Companion);
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml b/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
index 9f5c83b..32edf28 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
@@ -23,5 +23,9 @@
             android:name="androidx.compose.foundation.layout.WindowInsetsActivity"
             android:windowSoftInputMode="adjustResize"
             android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
+        <activity
+            android:name="androidx.compose.foundation.layout.WindowInsetsActionBarActivity"
+            android:windowSoftInputMode="adjustResize"
+            android:theme="@android:style/Theme.Material.Light" />
     </application>
 </manifest>
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
index cc38754..245cff1 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
@@ -20,7 +20,7 @@
 import androidx.core.view.WindowCompat
 import java.util.concurrent.CountDownLatch
 
-class WindowInsetsActivity : ComponentActivity() {
+open class WindowInsetsActivity : ComponentActivity() {
     val createdLatch = CountDownLatch(1)
     val attachedToWindowLatch = CountDownLatch(1)
 
@@ -35,3 +35,5 @@
         super.onAttachedToWindow()
     }
 }
+
+class WindowInsetsActionBarActivity : WindowInsetsActivity()
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
new file mode 100644
index 0000000..894b35c
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.layout
+
+import android.view.View
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.test.filters.MediumTest
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class WindowInsetsAnimationTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<WindowInsetsActionBarActivity>()
+
+    @Before
+    fun setup() {
+        rule.activity.createdLatch.await(1, TimeUnit.SECONDS)
+        rule.activity.attachedToWindowLatch.await(1, TimeUnit.SECONDS)
+    }
+
+    @After
+    fun teardown() {
+        rule.runOnUiThread {
+            val window = rule.activity.window
+            val view = window.decorView
+            WindowInsetsControllerCompat(window, view).hide(WindowInsetsCompat.Type.ime())
+        }
+    }
+
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun imeAnimationWhenShowingIme() {
+        val imeAnimationSourceValues = mutableListOf<Int>()
+        val imeAnimationTargetValues = mutableListOf<Int>()
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            val density = LocalDensity.current
+            val source = WindowInsets.imeAnimationSource
+            val target = WindowInsets.imeAnimationTarget
+            val sourceBottom = source.getBottom(density)
+            imeAnimationSourceValues += sourceBottom
+            val targetBottom = target.getBottom(density)
+            imeAnimationTargetValues += targetBottom
+            BasicTextField(
+                value = "Hello World",
+                onValueChange = {},
+                Modifier.focusRequester(focusRequester)
+            )
+        }
+
+        rule.waitForIdle()
+        rule.runOnUiThread {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitForIdle()
+        rule.waitUntil(timeoutMillis = 3000) {
+            imeAnimationSourceValues.last() > imeAnimationTargetValues.first()
+        }
+
+        rule.waitUntil(timeoutMillis = 3000) {
+            imeAnimationTargetValues.last() == imeAnimationSourceValues.last()
+        }
+    }
+
+    @OptIn(ExperimentalLayoutApi::class)
+    @Test
+    fun imeAnimationWhenHidingIme() {
+        val imeAnimationSourceValues = mutableListOf<Int>()
+        val imeAnimationTargetValues = mutableListOf<Int>()
+        val focusRequester = FocusRequester()
+        lateinit var view: View
+        rule.setContent {
+            view = LocalView.current
+            val density = LocalDensity.current
+            val source = WindowInsets.imeAnimationSource
+            val target = WindowInsets.imeAnimationTarget
+            val sourceBottom = source.getBottom(density)
+            imeAnimationSourceValues += sourceBottom
+            val targetBottom = target.getBottom(density)
+            imeAnimationTargetValues += targetBottom
+            BasicTextField(
+                value = "Hello World",
+                onValueChange = {},
+                Modifier.focusRequester(focusRequester)
+            )
+        }
+
+        rule.waitForIdle()
+        rule.runOnUiThread {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitUntil(timeoutMillis = 3000) {
+            val target = imeAnimationTargetValues.last()
+            val source = imeAnimationSourceValues.last()
+            target > imeAnimationSourceValues.first() && target == source
+        }
+
+        rule.runOnUiThread {
+            val window = rule.activity.window
+            WindowInsetsControllerCompat(window, view).hide(WindowInsetsCompat.Type.ime())
+        }
+
+        rule.waitForIdle()
+
+        rule.waitUntil(timeoutMillis = 3000) {
+            imeAnimationTargetValues.last() == imeAnimationSourceValues.first()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
index 1ea0f98..c77441a 100644
--- a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
@@ -373,6 +373,36 @@
     get() = WindowInsetsHolder.current().tappableElement.isVisible
 
 /**
+ * The [WindowInsets] for the IME before the IME started animating in. The current
+ * animated value is [WindowInsets.Companion.ime].
+ *
+ * This will be the same as [imeAnimationTarget] when there is no IME animation
+ * in progress.
+ */
+@ExperimentalLayoutApi
+val WindowInsets.Companion.imeAnimationSource: WindowInsets
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @ExperimentalLayoutApi
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().imeAnimationSource
+
+/**
+ * The [WindowInsets] for the IME when the animation completes, if it is allowed
+ * to complete successfully. The current animated value is [WindowInsets.Companion.ime].
+ *
+ * This will be the same as [imeAnimationSource] when there is no IME animation
+ * in progress.
+ */
+@ExperimentalLayoutApi
+val WindowInsets.Companion.imeAnimationTarget: WindowInsets
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @ExperimentalLayoutApi
+    @Composable
+    @NonRestartableComposable
+    get() = WindowInsetsHolder.current().imeAnimationTarget
+
+/**
  * The insets for various values in the current window.
  */
 internal class WindowInsetsHolder private constructor(insets: WindowInsetsCompat?, view: View) {
@@ -427,6 +457,16 @@
         WindowInsetsCompat.Type.tappableElement(),
         "tappableElementIgnoringVisibility"
     )
+    val imeAnimationTarget = valueInsetsIgnoringVisibility(
+        insets,
+        WindowInsetsCompat.Type.ime(),
+        "imeAnimationTarget"
+    )
+    val imeAnimationSource = valueInsetsIgnoringVisibility(
+        insets,
+        WindowInsetsCompat.Type.ime(),
+        "imeAnimationSource"
+    )
 
     /**
      * `true` unless the `ComposeView` [ComposeView.consumeWindowInsets] is set to `false`.
@@ -527,6 +567,24 @@
         Snapshot.sendApplyNotifications()
     }
 
+    /**
+     * Updates [WindowInsets.Companion.imeAnimationSource]. It should be called prior to
+     * [update].
+     */
+    fun updateImeAnimationSource(windowInsets: WindowInsetsCompat) {
+        imeAnimationSource.value =
+            windowInsets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
+    }
+
+    /**
+     * Updates [WindowInsets.Companion.imeAnimationTarget]. It should be called prior to
+     * [update].
+     */
+    fun updateImeAnimationTarget(windowInsets: WindowInsetsCompat) {
+        imeAnimationTarget.value =
+            windowInsets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
+    }
+
     companion object {
         /**
          * A mapping of AndroidComposeView to ComposeWindowInsets. Normally a tag is a great
@@ -649,6 +707,8 @@
         runningAnimation = false
         val insets = savedInsets
         if (animation.durationMillis != 0L && insets != null) {
+            composeInsets.updateImeAnimationSource(insets)
+            composeInsets.updateImeAnimationTarget(insets)
             composeInsets.update(insets)
         }
         savedInsets = null
@@ -659,6 +719,7 @@
         // Keep track of the most recent insets we've seen, to ensure onEnd will always use the
         // most recently acquired insets
         savedInsets = insets
+        composeInsets.updateImeAnimationTarget(insets)
         if (prepared) {
             // There may be no callback on R if the animation is canceled after onPrepare(),
             // so we won't know if the onPrepare() was canceled or if this is an
@@ -671,6 +732,7 @@
             // If an animation is running, rely on onProgress() to update the insets
             // On APIs less than 30 where the IME animation is backported, this avoids reporting
             // the final insets for a frame while the animation is running.
+            composeInsets.updateImeAnimationSource(insets)
             composeInsets.update(insets)
         }
         return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
@@ -688,6 +750,7 @@
             prepared = false
             runningAnimation = false
             savedInsets?.let {
+                composeInsets.updateImeAnimationSource(it)
                 composeInsets.update(it)
                 savedInsets = null
             }