Merge "Fix flaky gesturescope.sendClickTest" into androidx-main
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
index e9e03f9..7584d2e 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendClickTest.kt
@@ -33,15 +33,12 @@
 import androidx.compose.ui.test.performGesture
 import androidx.compose.ui.test.util.ClickableTestBox
 import androidx.compose.ui.test.util.RecordingFilter
-import androidx.test.filters.FlakyTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-// TODO(b/176953083): Fix this flaky test.
-@FlakyTest
 @MediumTest
 @RunWith(Parameterized::class)
 class SendClickTest(private val config: TestConfig) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index e769f7a..8a8ff4f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -18,7 +18,6 @@
 
 import android.view.View
 import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
@@ -34,8 +33,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -49,7 +46,7 @@
         lateinit var ownerView: View
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(
                 modifier = Modifier
                     .focusRequester(focusRequester)
@@ -76,7 +73,7 @@
         var focusState = Inactive
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
@@ -104,7 +101,7 @@
         var focusState = Inactive
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
@@ -131,7 +128,7 @@
         var focusState = Inactive
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
@@ -161,7 +158,7 @@
         var focusState = Inactive
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(
                 modifier = Modifier
                     .onFocusChanged { focusState = it }
@@ -185,19 +182,22 @@
     }
 
     @Test
-    fun clickingOnNonClickableSpaceInApp_bringsViewInFocus() {
+    fun clickingOnNonClickableSpaceInAppWhenViewIsFocused_doesNotChangeViewFocus() {
         // Arrange.
         val nonClickable = "notClickable"
-        val viewFocused = CountDownLatch(1)
+        var didViewFocusChange = false
         lateinit var ownerView: View
         rule.setFocusableContent {
-            ownerView = getOwner()
+            ownerView = AmbientView.current
             Box(Modifier.testTag(nonClickable))
         }
-        rule.runOnIdle { assertThat(ownerView.isFocused).isFalse() }
+        rule.runOnIdle {
+            ownerView.requestFocus()
+            assertThat(ownerView.isFocused).isTrue()
+        }
         ownerView.setOnFocusChangeListener { _, hasFocus ->
             if (hasFocus) {
-                viewFocused.countDown()
+                didViewFocusChange = true
             }
         }
 
@@ -206,11 +206,35 @@
 
         // Assert.
         rule.runOnIdle {
-            viewFocused.await(1L, TimeUnit.SECONDS)
+            assertThat(didViewFocusChange).isFalse()
             assertThat(ownerView.isFocused).isTrue()
         }
     }
 
-    @Composable
-    private fun getOwner() = AmbientView.current
+    @Test
+    fun clickingOnNonClickableSpaceInAppWhenViewIsNotFocused_doesNotChangeViewFocus() {
+        // Arrange.
+        val nonClickable = "notClickable"
+        var didViewFocusChange = false
+        lateinit var ownerView: View
+        rule.setFocusableContent {
+            ownerView = AmbientView.current
+            Box(Modifier.testTag(nonClickable))
+        }
+        rule.runOnIdle { assertThat(ownerView.isFocused).isFalse() }
+        ownerView.setOnFocusChangeListener { _, hasFocus ->
+            if (hasFocus) {
+                didViewFocusChange = true
+            }
+        }
+
+        // Act.
+        rule.onNodeWithTag(nonClickable).performClick()
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(didViewFocusChange).isFalse()
+            assertThat(ownerView.isFocused).isFalse()
+        }
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index 3e2826d..dbf0813 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -18,6 +18,9 @@
 
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState.Active
+import androidx.compose.ui.focus.FocusState.ActiveParent
+import androidx.compose.ui.focus.FocusState.Captured
+import androidx.compose.ui.focus.FocusState.Disabled
 import androidx.compose.ui.focus.FocusState.Inactive
 import androidx.compose.ui.gesture.PointerInputModifierImpl
 import androidx.compose.ui.gesture.TapGestureFilter
@@ -50,17 +53,9 @@
     private val passThroughClickModifier = PointerInputModifierImpl(
         TapGestureFilter().apply {
             onTap = {
-                if (focusModifier.focusState == Inactive) {
-
-                    // This view does not have focus, and the user clicked on some non-clickable
-                    // part of the screen. This is an indication that the user wants to bring
-                    // this view (this app) in focus.
-                    focusModifier.focusNode.requestFocus(propagateFocus = false)
-                } else {
-                    // The user clicked on a non-clickable part of the screen when something was
-                    // focused. This is an indication that the user wants to clear focus.
-                    clearFocus()
-                }
+                // The user clicked on a non-clickable part of the screen when something was
+                // focused. This is an indication that the user wants to clear focus.
+                clearFocus()
             }
             consumeChanges = false
         }
@@ -110,7 +105,15 @@
      * component.
      */
     override fun clearFocus(forcedClear: Boolean) {
-        if (focusModifier.focusNode.clearFocus(forcedClear)) {
+        // If this hierarchy had focus before clearing it, it indicates that the host view has
+        // focus. So after clearing focus within the compose hierarchy, we should reset the root
+        // focus modifier to "Active" to maintain consistency with the host view.
+        val rootWasFocused = when (focusModifier.focusState) {
+            Active, ActiveParent, Captured -> true
+            Disabled, Inactive -> false
+        }
+
+        if (focusModifier.focusNode.clearFocus(forcedClear) && rootWasFocused) {
             focusModifier.focusState = Active
         }
     }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
index 8bf5916..6e3fafd 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
@@ -71,7 +71,7 @@
     }
 
     @Test
-    fun clearFocus_changesStateToInactive() {
+    fun releaseFocus_changesStateToInactive() {
         // Arrange.
         focusModifier.focusState = initialFocusState
         if (initialFocusState == ActiveParent) {
@@ -94,6 +94,56 @@
     }
 
     @Test
+    fun clearFocus_forced() {
+        // Arrange.
+        focusModifier.focusState = initialFocusState
+        if (initialFocusState == ActiveParent) {
+            val childLayoutNode = LayoutNode()
+            val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Active))
+            focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
+            focusModifier.focusedChild = child
+        }
+
+        // Act.
+        focusManager.clearFocus(forcedClear = true)
+
+        // Assert.
+        assertThat(focusModifier.focusState).isEqualTo(
+            when (initialFocusState) {
+                // If the initial state was focused, assert that after clearing the hierarchy,
+                // the root is set to Active.
+                Active, ActiveParent, Captured -> Active
+                Disabled, Inactive -> initialFocusState
+            }
+        )
+    }
+
+    @Test
+    fun clearFocus_notForced() {
+        // Arrange.
+        focusModifier.focusState = initialFocusState
+        if (initialFocusState == ActiveParent) {
+            val childLayoutNode = LayoutNode()
+            val child = ModifiedFocusNode(InnerPlaceable(childLayoutNode), FocusModifier(Active))
+            focusModifier.focusNode.layoutNode._children.add(childLayoutNode)
+            focusModifier.focusedChild = child
+        }
+
+        // Act.
+        focusManager.clearFocus(forcedClear = false)
+
+        // Assert.
+        assertThat(focusModifier.focusState).isEqualTo(
+            when (initialFocusState) {
+                // If the initial state was focused, assert that after clearing the hierarchy,
+                // the root is set to Active.
+                Active, ActiveParent -> Active
+                Captured, Disabled, Inactive -> initialFocusState
+            }
+        )
+    }
+
+    @Test
     fun clearFocus_childIsCaptured() {
         // Arrange.
         focusModifier.focusState = ActiveParent