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