Merge "Add an API to check if the window is in focus" into androidx-master-dev
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index 49a4317..378ab68 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -76,6 +76,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Uptime
 import androidx.compose.ui.unit.milliseconds
+import androidx.compose.ui.platform.WindowManager
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.test.runBlockingTest
@@ -405,6 +406,8 @@
             get() = TODO("Not yet implemented")
         override val focusManager: FocusManager
             get() = TODO("Not yet implemented")
+        override val windowManager: WindowManager
+            get() = TODO("Not yet implemented")
         override val fontLoader: Font.ResourceLoader
             get() = TODO("Not yet implemented")
         override val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index fb53e39..1288834 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2027,6 +2027,7 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    method public androidx.compose.ui.platform.WindowManager getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2052,6 +2053,7 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2122,6 +2124,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2314,6 +2317,15 @@
     property public abstract float touchSlop;
   }
 
+  @androidx.compose.runtime.Stable public interface WindowManager {
+    method public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
+  }
+
+  public final class WindowManagerKt {
+    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index fb53e39..1288834 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2027,6 +2027,7 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    method public androidx.compose.ui.platform.WindowManager getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2052,6 +2053,7 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2122,6 +2124,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2314,6 +2317,15 @@
     property public abstract float touchSlop;
   }
 
+  @androidx.compose.runtime.Stable public interface WindowManager {
+    method public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
+  }
+
+  public final class WindowManagerKt {
+    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 5f72ebc..4ecf54f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2089,6 +2089,7 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    method public androidx.compose.ui.platform.WindowManager getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2114,6 +2115,7 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2184,6 +2186,7 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2376,6 +2379,15 @@
     property public abstract float touchSlop;
   }
 
+  @androidx.compose.runtime.Stable public interface WindowManager {
+    method public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
+  }
+
+  public final class WindowManagerKt {
+    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
   public final class WrapperKt {
     method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index e38c20a..021cf64 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -43,6 +43,8 @@
 import androidx.compose.ui.demos.viewinterop.ViewInteropDemo
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
+import androidx.compose.ui.demos.focus.FocusInDialog
+import androidx.compose.ui.demos.focus.FocusInPopup
 
 private val GestureDemos = DemoCategory(
     "Gestures",
@@ -93,6 +95,8 @@
     "Focus",
     listOf(
         ComposableDemo("Focusable Siblings") { FocusableDemo() },
+        ComposableDemo("Focus Within Dialog") { FocusInDialog() },
+        ComposableDemo("Focus Within Popup") { FocusInPopup() },
         ComposableDemo("Reuse Focus Requester") { ReuseFocusRequester() }
     )
 )
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
new file mode 100644
index 0000000..2769825
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 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.ui.demos.focus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color.Companion.LightGray
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+
+@Composable
+fun FocusInDialog() {
+    var showDialog by remember { mutableStateOf(false) }
+    var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+    var dialogText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+    val windowManager = AmbientWindowManager.current
+
+    Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+        Text("Click the button to show the dialog. Click outside the dialog to dismiss it.")
+        Spacer(Modifier.height(10.dp))
+        Button(onClick = { showDialog = true }) {
+            Text("Show Dialog")
+        }
+
+        Spacer(Modifier.height(50.dp))
+
+        Text("Click this text field to bring the main app in focus.")
+        TextField(value = mainText, onValueChange = { mainText = it })
+        FocusStatus()
+
+        if (showDialog) {
+            Dialog(onDismissRequest = { showDialog = false }) {
+                Column(Modifier.background(White)) {
+                    Text("Click this text field to bring the dialog in focus")
+                    TextField(value = dialogText, onValueChange = { dialogText = it })
+                    FocusStatus()
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun FocusStatus() {
+    val windowManager = AmbientWindowManager.current
+    Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+}
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
new file mode 100644
index 0000000..e040b3c
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 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.ui.demos.focus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color.Companion.LightGray
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+
+@Composable
+fun FocusInPopup() {
+    var showPopup by remember { mutableStateOf(false) }
+    var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+    var popupText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+    val windowManager = AmbientWindowManager.current
+
+    Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+        Text("Click the button to show the popup. Click outside the popup to dismiss it.")
+        Spacer(Modifier.height(10.dp))
+        Button(onClick = { showPopup = true }) {
+            Text("Show Popup")
+        }
+
+        Spacer(Modifier.height(50.dp))
+
+        Text("Click this text field to bring the main app in focus.")
+        TextField(value = mainText, onValueChange = { mainText = it })
+        FocusStatus()
+
+        if (showPopup) {
+            Popup(
+                alignment = Alignment.Center,
+                isFocusable = true,
+                onDismissRequest = { showPopup = false }
+            ) {
+                Column(Modifier.background(White)) {
+                    Text("Click this text field to bring the popup in focus")
+                    TextField(value = popupText, onValueChange = { popupText = it })
+                    FocusStatus()
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun FocusStatus() {
+    val windowManager = AmbientWindowManager.current
+    Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 1ac3201..05c2735 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -50,6 +50,7 @@
 import androidx.compose.ui.unit.Uptime
 import androidx.compose.ui.unit.milliseconds
 import androidx.compose.ui.unit.minus
+import androidx.compose.ui.platform.WindowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -3154,6 +3155,8 @@
         get() = TODO("Not yet implemented")
     override val focusManager: FocusManager
         get() = TODO("Not yet implemented")
+    override val windowManager: WindowManager
+        get() = TODO("Not yet implemented")
     override val fontLoader: Font.ResourceLoader
         get() = TODO("Not yet implemented")
     override val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
new file mode 100644
index 0000000..b086688
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2020 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.ui.platform
+
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.Popup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit.SECONDS
+
+@MediumTest
+@OptIn(ExperimentalFocus::class)
+@RunWith(AndroidJUnit4::class)
+class WindowManagerAmbientTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Ignore("Flaky Test b/173088588")
+    @Test
+    fun windowIsFocused_onLaunch() {
+        // Arrange.
+        lateinit var windowManager: WindowManager
+        val windowFocusGain = CountDownLatch(1)
+        rule.setContent {
+            BasicText("Main Window")
+            windowManager = AmbientWindowManager.current
+            WindowFocusObserver { if (it) windowFocusGain.countDown() }
+        }
+
+        // Act.
+        rule.waitForIdle()
+
+        // Assert.
+        windowFocusGain.await(5, SECONDS)
+        assertThat( windowManager.isWindowFocused ).isTrue()
+    }
+
+    @Test
+    fun mainWindowIsNotFocused_whenPopupIsVisible() {
+        // Arrange.
+        lateinit var mainWindowManager: WindowManager
+        lateinit var popupWindowManager: WindowManager
+        val mainWindowFocusLoss = CountDownLatch(1)
+        val popupFocusGain = CountDownLatch(1)
+        val showPopup = mutableStateOf(false)
+        rule.setContent {
+            BasicText("Main Window")
+            mainWindowManager = AmbientWindowManager.current
+            WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+            if (showPopup.value) {
+                Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
+                    BasicText("Popup Window")
+                    popupWindowManager = AmbientWindowManager.current
+                    WindowFocusObserver { if (it) popupFocusGain.countDown() }
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { showPopup.value = true }
+
+        // Assert.
+        rule.waitForIdle()
+        assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
+        assertThat(popupFocusGain.await(5, SECONDS)).isTrue()
+        assertThat( mainWindowManager.isWindowFocused ).isFalse()
+        assertThat( popupWindowManager.isWindowFocused ).isTrue()
+    }
+
+    @Test
+    fun windowIsFocused_whenPopupIsDismissed() {
+        // Arrange.
+        lateinit var mainWindowManager: WindowManager
+        var mainWindowFocusGain = CountDownLatch(1)
+        val popupFocusGain = CountDownLatch(1)
+        val showPopup = mutableStateOf(false)
+        rule.setContent {
+            BasicText(text = "Main Window")
+            mainWindowManager = AmbientWindowManager.current
+            WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+            if (showPopup.value) {
+                Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
+                    BasicText(text = "Popup Window")
+                    WindowFocusObserver { if (it) popupFocusGain.countDown() }
+                }
+            }
+        }
+        rule.runOnIdle { showPopup.value = true }
+        rule.waitForIdle()
+        assertThat(popupFocusGain.await(5, SECONDS)).isTrue()
+        mainWindowFocusGain = CountDownLatch(1)
+
+        // Act.
+        rule.runOnIdle { showPopup.value = false }
+
+        // Assert.
+        rule.waitForIdle()
+        assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
+        assertThat(mainWindowManager.isWindowFocused).isTrue()
+    }
+
+    @Test
+    fun mainWindowIsNotFocused_whenDialogIsVisible() {
+        // Arrange.
+        lateinit var mainWindowManager: WindowManager
+        lateinit var dialogWindowManager: WindowManager
+        val mainWindowFocusLoss = CountDownLatch(1)
+        val dialogFocusGain = CountDownLatch(1)
+        val showDialog = mutableStateOf(false)
+        rule.setContent {
+            BasicText("Main Window")
+            mainWindowManager = AmbientWindowManager.current
+            WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+            if (showDialog.value) {
+                Dialog(onDismissRequest = { showDialog.value = false }) {
+                    BasicText("Popup Window")
+                    dialogWindowManager = AmbientWindowManager.current
+                    WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { showDialog.value = true }
+
+        // Assert.
+        rule.waitForIdle()
+        assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
+        assertThat(dialogFocusGain.await(5, SECONDS)).isTrue()
+        assertThat(mainWindowManager.isWindowFocused).isFalse()
+        assertThat(dialogWindowManager.isWindowFocused).isTrue()
+    }
+
+    @Test
+    fun windowIsFocused_whenDialogIsDismissed() {
+        // Arrange.
+        lateinit var mainWindowManager: WindowManager
+        var mainWindowFocusGain = CountDownLatch(1)
+        val dialogFocusGain = CountDownLatch(1)
+        val showDialog = mutableStateOf(false)
+        rule.setContent {
+            BasicText(text = "Main Window")
+            mainWindowManager = AmbientWindowManager.current
+            WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+            if (showDialog.value) {
+                Dialog(onDismissRequest = { showDialog.value = false }) {
+                    BasicText(text = "Popup Window")
+                    WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+                }
+            }
+        }
+        rule.runOnIdle { showDialog.value = true }
+        rule.waitForIdle()
+        assertThat(dialogFocusGain.await(5, SECONDS)).isTrue()
+        mainWindowFocusGain = CountDownLatch(1)
+
+        // Act.
+        rule.runOnIdle { showDialog.value = false }
+
+        // Assert.
+        rule.waitForIdle()
+        assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
+        assertThat(mainWindowManager.isWindowFocused).isTrue()
+    }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 6805cc6..3466b37 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -114,6 +114,10 @@
     override val focusManager: FocusManager
         get() = _focusManager
 
+    private val _windowManager: WindowManagerImpl = WindowManagerImpl()
+    override val windowManager: WindowManager
+        get() = _windowManager
+
     private val keyInputModifier = KeyInputModifier(null, null)
 
     private val canvasHolder = CanvasHolder()
@@ -294,6 +298,11 @@
         }
     }
 
+    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
+        _windowManager.isWindowFocused = hasWindowFocus
+        super.onWindowFocusChanged(hasWindowFocus)
+    }
+
     override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
         return keyInputModifier.processKeyInput(keyEvent)
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 5f9067a..f5107fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.platform.WindowManager
 
 /**
  * Owner implements the connection to the underlying view system. On Android, this connects
@@ -86,6 +87,11 @@
     @ExperimentalFocus
     val focusManager: FocusManager
 
+    /**
+     * Provide information about the window that hosts this [Owner].
+     */
+    val windowManager: WindowManager
+
     val fontLoader: Font.ResourceLoader
 
     val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index 277b3de..bfbb003 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -287,6 +287,11 @@
  */
 val AmbientViewConfiguration = staticAmbientOf<ViewConfiguration>()
 
+/**
+ * The ambient that provides information about the window that hosts the current [Owner].
+ */
+val AmbientWindowManager = staticAmbientOf<WindowManager>()
+
 @OptIn(ExperimentalFocus::class)
 @Composable
 internal fun ProvideCommonAmbients(
@@ -309,6 +314,7 @@
         AmbientTextToolbar provides owner.textToolbar,
         AmbientUriHandler provides uriHandler,
         AmbientViewConfiguration provides owner.viewConfiguration,
+        AmbientWindowManager provides owner.windowManager,
         content = content
     )
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
new file mode 100644
index 0000000..0c20424
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 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.ui.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshots.snapshotFlow
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Provides information about the Window that is hosting this compose hierarchy.
+ */
+@Stable
+interface WindowManager {
+    /**
+     * Indicates whether the window hosting this compose hierarchy is in focus.
+     *
+     * When there are multiple windows visible, either in a multi-window environment or if a
+     * popup or dialog is visible, this property can be used to determine if the current window
+     * is in focus.
+     */
+    val isWindowFocused: Boolean
+}
+
+/**
+ * Provides a callback that is called whenever the window gains or loses focus.
+ */
+@OptIn(
+    ExperimentalComposeApi::class,
+    InternalCoroutinesApi::class
+)
+@Composable
+fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean) -> Unit) {
+    val windowManager = AmbientWindowManager.current
+    val callback = rememberUpdatedState(onWindowFocusChanged)
+    LaunchedEffect(windowManager) {
+        snapshotFlow { windowManager.isWindowFocused }.collect { callback.value(it) }
+    }
+}
+
+internal class WindowManagerImpl : WindowManager {
+    private val _isWindowFocused = mutableStateOf(false)
+    override var isWindowFocused: Boolean
+        set(value) { _isWindowFocused.value = value }
+        get() = _isWindowFocused.value
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 61d96f6..c499c3c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -87,6 +87,11 @@
     override val focusManager: FocusManager
         get() = _focusManager
 
+    // TODO: set/clear _windowManager.isWindowFocused when the window gains/loses focus.
+    private val _windowManager: WindowManagerImpl = WindowManagerImpl()
+    override val windowManager: WindowManager
+        get() = _windowManager
+
     private val keyInputModifier = KeyInputModifier(null, null)
 
     override val root = LayoutNode().also {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 2f30912..9f7b77ab 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -50,6 +50,7 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.platform.WindowManager
 import androidx.compose.ui.zIndex
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.anyOrNull
@@ -1708,6 +1709,8 @@
         get() = TODO("Not yet implemented")
     override val focusManager: FocusManager
         get() = TODO("Not yet implemented")
+    override val windowManager: WindowManager
+        get() = TODO("Not yet implemented")
     override val fontLoader: Font.ResourceLoader
         get() = TODO("Not yet implemented")
     override val layoutDirection: LayoutDirection