Merge "Request WindowInsets when they are first needed." into androidx-main
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 41e064f..e9bf1a3 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
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.annotation.RequiresApi
+import androidx.core.view.WindowCompat
 import java.util.concurrent.CountDownLatch
 
 class WindowInsetsActivity : ComponentActivity() {
@@ -26,7 +27,7 @@
 
     @RequiresApi(Build.VERSION_CODES.R)
     override fun onCreate(savedInstanceState: Bundle?) {
-        window.setDecorFitsSystemWindows(false)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
         super.onCreate(savedInstanceState)
         createdLatch.countDown()
     }
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
index a35618c..8d50d89 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
@@ -17,7 +17,14 @@
 
 import android.os.Build
 import android.os.SystemClock
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.RequiresApi
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -28,25 +35,42 @@
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.children
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
 class WindowInsetsDeviceTest {
     @get:Rule
     val rule = createAndroidComposeRule<WindowInsetsActivity>()
 
+    @Before
+    fun setup() {
+        rule.activity.createdLatch.await(1, TimeUnit.SECONDS)
+    }
+
     @OptIn(ExperimentalLayoutApi::class)
     @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
     fun disableConsumeDisablesAnimationConsumption() {
         var imeInset1 = 0
         var imeInset2 = 0
@@ -122,10 +146,194 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.R)
     private fun pokeIME() {
         // This appears to be necessary for cuttlefish devices to show the keyboard
         val controller = rule.activity.window.insetsController
         controller?.show(android.view.WindowInsets.Type.ime())
         controller?.hide(android.view.WindowInsets.Type.ime())
     }
+
+    @Test
+    fun insetsUsedAfterInitialComposition() {
+        var useInsets by mutableStateOf(false)
+        var systemBarsInsets by mutableStateOf(Insets.NONE)
+
+        rule.setContent {
+            val view = LocalView.current
+            DisposableEffect(Unit) {
+                // Ensure that the system bars are shown
+                val window = rule.activity.window
+
+                @Suppress("RedundantNullableReturnType") // nullable on some versions
+                val controller: WindowInsetsControllerCompat? =
+                    WindowCompat.getInsetsController(window, view)
+                controller?.show(WindowInsetsCompat.Type.systemBars())
+                onDispose { }
+            }
+            Box(Modifier.fillMaxSize()) {
+                if (useInsets) {
+                    val systemBars = WindowInsets.systemBars
+                    val density = LocalDensity.current
+                    val left = systemBars.getLeft(density, LayoutDirection.Ltr)
+                    val top = systemBars.getTop(density)
+                    val right = systemBars.getRight(density, LayoutDirection.Ltr)
+                    val bottom = systemBars.getBottom(density)
+                    systemBarsInsets = Insets.of(left, top, right, bottom)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            useInsets = true
+        }
+
+        rule.runOnIdle {
+            assertThat(systemBarsInsets).isNotEqualTo(Insets.NONE)
+        }
+    }
+
+    @Test
+    fun insetsAfterStopWatching() {
+        var useInsets by mutableStateOf(true)
+        var hasStatusBarInsets = false
+
+        rule.setContent {
+            val view = LocalView.current
+            DisposableEffect(Unit) {
+                // Ensure that the status bars are shown
+                val window = rule.activity.window
+
+                @Suppress("RedundantNullableReturnType") // nullable on some versions
+                val controller: WindowInsetsControllerCompat? =
+                    WindowCompat.getInsetsController(window, view)
+                controller?.hide(WindowInsetsCompat.Type.statusBars())
+                onDispose { }
+            }
+            Box(Modifier.fillMaxSize()) {
+                if (useInsets) {
+                    val statusBars = WindowInsets.statusBars
+                    val density = LocalDensity.current
+                    val left = statusBars.getLeft(density, LayoutDirection.Ltr)
+                    val top = statusBars.getTop(density)
+                    val right = statusBars.getRight(density, LayoutDirection.Ltr)
+                    val bottom = statusBars.getBottom(density)
+                    hasStatusBarInsets = left != 0 || top != 0 || right != 0 || bottom != 0
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.waitUntil(1000) { !hasStatusBarInsets }
+
+        // disable watching the insets
+        rule.runOnIdle {
+            useInsets = false
+        }
+
+        val statusBarsWatcher = StatusBarsShowListener()
+
+        // show the insets while we're not watching
+        rule.runOnIdle {
+            ViewCompat.setOnApplyWindowInsetsListener(
+                rule.activity.window.decorView,
+                statusBarsWatcher
+            )
+            @Suppress("RedundantNullableReturnType")
+            val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+                rule.activity.window,
+                rule.activity.window.decorView
+            )
+            controller?.show(WindowInsetsCompat.Type.statusBars())
+        }
+
+        assertThat(statusBarsWatcher.latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        // Now look at the insets
+        rule.runOnIdle {
+            useInsets = true
+        }
+
+        rule.runOnIdle {
+            assertThat(hasStatusBarInsets).isTrue()
+        }
+    }
+
+    @Test
+    fun insetsAfterReattachingView() {
+        var hasStatusBarInsets = false
+
+        // hide the insets
+        rule.runOnUiThread {
+            @Suppress("RedundantNullableReturnType")
+            val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+                rule.activity.window,
+                rule.activity.window.decorView
+            )
+            controller?.hide(WindowInsetsCompat.Type.statusBars())
+        }
+
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                val statusBars = WindowInsets.statusBars
+                val density = LocalDensity.current
+                val left = statusBars.getLeft(density, LayoutDirection.Ltr)
+                val top = statusBars.getTop(density)
+                val right = statusBars.getRight(density, LayoutDirection.Ltr)
+                val bottom = statusBars.getBottom(density)
+                hasStatusBarInsets = left != 0 || top != 0 || right != 0 || bottom != 0
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.waitUntil(1000) { !hasStatusBarInsets }
+
+        val contentView = rule.activity.findViewById<ViewGroup>(android.R.id.content)
+        val composeView = contentView.children.first()
+
+        // remove the view
+        rule.runOnUiThread {
+            contentView.removeView(composeView)
+        }
+
+        val statusBarsWatcher = StatusBarsShowListener()
+
+        // show the insets while we're not watching
+        rule.runOnUiThread {
+            ViewCompat.setOnApplyWindowInsetsListener(
+                rule.activity.window.decorView,
+                statusBarsWatcher
+            )
+            @Suppress("RedundantNullableReturnType")
+            val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+                rule.activity.window,
+                rule.activity.window.decorView
+            )
+            controller?.show(WindowInsetsCompat.Type.statusBars())
+        }
+
+        assertThat(statusBarsWatcher.latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        // Now add the view back again
+        rule.runOnUiThread {
+            contentView.addView(composeView)
+        }
+
+        rule.waitUntil(1000) { hasStatusBarInsets }
+    }
+
+    class StatusBarsShowListener : OnApplyWindowInsetsListener {
+        val latch = CountDownLatch(1)
+
+        override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+            val statusBars = insets.getInsets(WindowInsetsCompat.Type.statusBars())
+            if (statusBars != Insets.NONE) {
+                latch.countDown()
+                ViewCompat.setOnApplyWindowInsetsListener(v, null)
+            }
+            return insets
+        }
+    }
 }
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 eef1555..5f7031d 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
@@ -19,6 +19,7 @@
 import androidx.core.graphics.Insets as AndroidXInsets
 import android.os.Build
 import android.view.View
+import android.view.View.OnAttachStateChangeListener
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.compose.runtime.Composable
@@ -453,6 +454,11 @@
             // add listeners
             ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener)
 
+            if (view.isAttachedToWindow) {
+                view.requestApplyInsets()
+            }
+            view.addOnAttachStateChangeListener(insetsListener)
+
             // We don't need animation callbacks on earlier versions, so don't bother adding
             // the listener. ViewCompat calls the animation callbacks superfluously.
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -473,6 +479,7 @@
             // remove listeners
             ViewCompat.setOnApplyWindowInsetsListener(view, null)
             ViewCompat.setWindowInsetsAnimationCallback(view, null)
+            view.removeOnAttachStateChangeListener(insetsListener)
         }
     }
 
@@ -616,9 +623,9 @@
 
 private class InsetsListener(
     val composeInsets: WindowInsetsHolder,
-) : Runnable, OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(
+) : WindowInsetsAnimationCompat.Callback(
     if (composeInsets.consumes) DISPATCH_MODE_STOP else DISPATCH_MODE_CONTINUE_ON_SUBTREE
-) {
+), Runnable, OnApplyWindowInsetsListener, OnAttachStateChangeListener {
     /**
      * When [android.view.WindowInsetsController.controlWindowInsetsAnimation] is called,
      * the [onApplyWindowInsets] is called after [onPrepare] with the target size. We
@@ -697,4 +704,11 @@
             }
         }
     }
+
+    override fun onViewAttachedToWindow(view: View) {
+        view.requestApplyInsets()
+    }
+
+    override fun onViewDetachedFromWindow(v: View) {
+    }
 }