Merge "Include self for FocusOwner Key and Rotary Traversal" into androidx-main
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
index 85b3f11..f446af8 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
@@ -20,8 +20,29 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.InputMode
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.input.key.SoftKeyboardInterceptionModifierNode
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.pointer.elementFor
+import androidx.compose.ui.input.rotary.RotaryInputModifierNode
+import androidx.compose.ui.input.rotary.RotaryScrollEvent
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.platform.LocalInputModeManager
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.test.performRotaryScrollInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -980,6 +1001,300 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
+    @Test
+    fun focusTarget_nodeThatIsKeyInputNodeKind_implementing_receivesKeyEventsWhenFocused() {
+        class FocusTargetAndKeyInputNode : DelegatingNode(), KeyInputModifierNode {
+            val keyEvents = mutableListOf<KeyEvent>()
+            val focusTargetNode = FocusTargetNode()
+
+            init {
+                delegate(focusTargetNode)
+            }
+
+            override fun onKeyEvent(event: KeyEvent): Boolean {
+                keyEvents.add(event)
+                return true
+            }
+
+            override fun onPreKeyEvent(event: KeyEvent) = false
+        }
+
+        val focusTargetAndKeyInputNode = FocusTargetAndKeyInputNode()
+        val focusTargetAndKeyInputModifier = elementFor(key1 = null, focusTargetAndKeyInputNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+        lateinit var inputModeManager: InputModeManager
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            inputModeManager = LocalInputModeManager.current
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndKeyInputModifier)
+            )
+        }
+
+        rule.runOnUiThread {
+            inputModeManager.requestInputMode(InputMode.Keyboard)
+            focusRequester.requestFocus()
+        }
+
+        assertThat(focusTargetAndKeyInputNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        rule.onNodeWithTag(targetTestTag).performKeyInput { keyDown(Key.Enter) }
+
+        assertThat(focusTargetAndKeyInputNode.keyEvents).hasSize(1)
+        assertThat(focusTargetAndKeyInputNode.keyEvents[0].key).isEqualTo(Key.Enter)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
+    @Test
+    fun focusTarget_nodeThatIsKeyInputNodeKind_delegating_receivesKeyEventsWhenFocused() {
+        class FocusTargetAndKeyInputNode : DelegatingNode() {
+            val keyEvents = mutableListOf<KeyEvent>()
+            val focusTargetNode = FocusTargetNode()
+            val keyInputNode = object : KeyInputModifierNode, Modifier.Node() {
+                override fun onKeyEvent(event: KeyEvent): Boolean {
+                    keyEvents.add(event)
+                    return true
+                }
+
+                override fun onPreKeyEvent(event: KeyEvent) = false
+            }
+
+            init {
+                delegate(focusTargetNode)
+                delegate(keyInputNode)
+            }
+        }
+
+        val focusTargetAndKeyInputNode = FocusTargetAndKeyInputNode()
+        val focusTargetAndKeyInputModifier = elementFor(key1 = null, focusTargetAndKeyInputNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+        lateinit var inputModeManager: InputModeManager
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            inputModeManager = LocalInputModeManager.current
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndKeyInputModifier)
+            )
+        }
+
+        rule.runOnUiThread {
+            inputModeManager.requestInputMode(InputMode.Keyboard)
+            focusRequester.requestFocus()
+        }
+
+        assertThat(focusTargetAndKeyInputNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        rule.onNodeWithTag(targetTestTag).performKeyInput { keyDown(Key.Enter) }
+
+        assertThat(focusTargetAndKeyInputNode.keyEvents).hasSize(1)
+        assertThat(focusTargetAndKeyInputNode.keyEvents[0].key).isEqualTo(Key.Enter)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun focusTarget_nodeThatIsSoftKeyInputNodeKind_implementing_receivesSoftKeyEventsWhenFocused() {
+        class FocusTargetAndSoftKeyboardNode : DelegatingNode(),
+            SoftKeyboardInterceptionModifierNode {
+            val keyEvents = mutableListOf<KeyEvent>()
+            val focusTargetNode = FocusTargetNode()
+
+            init {
+                delegate(focusTargetNode)
+            }
+
+            override fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent) = keyEvents.add(event)
+
+            override fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent) = false
+        }
+
+        val focusTargetAndSoftKeyboardNode = FocusTargetAndSoftKeyboardNode()
+        val focusTargetAndSoftKeyboardModifier =
+            elementFor(key1 = null, focusTargetAndSoftKeyboardNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndSoftKeyboardModifier)
+            )
+        }
+
+        rule.runOnUiThread { focusRequester.requestFocus() }
+        assertThat(focusTargetAndSoftKeyboardNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        // This test specifically uses performKeyPress over performKeyInput as performKeyPress calls
+        // sendKeyEvent, which in turn notifies FocusOwner that there's a
+        // SoftKeyboardInterceptionModifierNode-interceptable key event first. performKeyInput goes
+        // through dispatchKeyEvent which does not notify SoftKeyboardInterceptionModifierNodes.
+        rule.onRoot().performKeyPress(
+            KeyEvent(
+                NativeKeyEvent(
+                    android.view.KeyEvent.ACTION_DOWN,
+                    android.view.KeyEvent.KEYCODE_ENTER
+                )
+            )
+        )
+
+        assertThat(focusTargetAndSoftKeyboardNode.keyEvents).hasSize(1)
+        assertThat(focusTargetAndSoftKeyboardNode.keyEvents[0].key).isEqualTo(Key.Enter)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun focusTarget_nodeThatIsSoftKeyInputNodeKind_delegating_receivesSoftKeyEventsWhenFocused() {
+        class FocusTargetAndSoftKeyboardNode : DelegatingNode() {
+            val keyEvents = mutableListOf<KeyEvent>()
+            val focusTargetNode = FocusTargetNode()
+            val softKeyboardInterceptionNode = object : SoftKeyboardInterceptionModifierNode,
+                Modifier.Node() {
+                override fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent) =
+                    keyEvents.add(event)
+
+                override fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent) = false
+            }
+
+            init {
+                delegate(focusTargetNode)
+                delegate(softKeyboardInterceptionNode)
+            }
+        }
+
+        val focusTargetAndSoftKeyboardNode = FocusTargetAndSoftKeyboardNode()
+        val focusTargetAndSoftKeyboardModifier =
+            elementFor(key1 = null, focusTargetAndSoftKeyboardNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndSoftKeyboardModifier)
+            )
+        }
+
+        rule.runOnUiThread { focusRequester.requestFocus() }
+        assertThat(focusTargetAndSoftKeyboardNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        // This test specifically uses performKeyPress over performKeyInput as performKeyPress calls
+        // sendKeyEvent, which in turn notifies FocusOwner that there's a
+        // SoftKeyboardInterceptionModifierNode-interceptable key event first. performKeyInput goes
+        // through dispatchKeyEvent which does not notify SoftKeyboardInterceptionModifierNodes.
+        rule.onRoot().performKeyPress(
+            KeyEvent(
+                NativeKeyEvent(
+                    android.view.KeyEvent.ACTION_DOWN,
+                    android.view.KeyEvent.KEYCODE_ENTER
+                )
+            )
+        )
+
+        assertThat(focusTargetAndSoftKeyboardNode.keyEvents).hasSize(1)
+        assertThat(focusTargetAndSoftKeyboardNode.keyEvents[0].key).isEqualTo(Key.Enter)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun focusTarget_nodeThatIsRotaryInputNodeKind_implementing_receivesRotaryEventsWhenFocused() {
+        class FocusTargetAndRotaryNode : DelegatingNode(), RotaryInputModifierNode {
+            val events = mutableListOf<RotaryScrollEvent>()
+            val focusTargetNode = FocusTargetNode()
+
+            init {
+                delegate(focusTargetNode)
+            }
+
+            override fun onRotaryScrollEvent(event: RotaryScrollEvent) = events.add(event)
+
+            override fun onPreRotaryScrollEvent(event: RotaryScrollEvent) = false
+        }
+
+        val focusTargetAndRotaryNode = FocusTargetAndRotaryNode()
+        val focusTargetAndRotaryModifier = elementFor(key1 = null, focusTargetAndRotaryNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndRotaryModifier)
+            )
+        }
+
+        rule.runOnUiThread { focusRequester.requestFocus() }
+        assertThat(focusTargetAndRotaryNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        rule.onNodeWithTag(targetTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(100f)
+        }
+
+        assertThat(focusTargetAndRotaryNode.events).hasSize(1)
+        assertThat(focusTargetAndRotaryNode.events[0].verticalScrollPixels).isEqualTo(100f)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun focusTarget_nodeThatIsRotaryInputNodeKind_delegating_receivesRotaryEventsWhenFocused() {
+        class FocusTargetAndRotaryNode : DelegatingNode() {
+            val events = mutableListOf<RotaryScrollEvent>()
+            val focusTargetNode = FocusTargetNode()
+            val rotaryInputNode = object : RotaryInputModifierNode, Modifier.Node() {
+                override fun onRotaryScrollEvent(event: RotaryScrollEvent) = events.add(event)
+                override fun onPreRotaryScrollEvent(event: RotaryScrollEvent) = false
+            }
+
+            init {
+                delegate(focusTargetNode)
+                delegate(rotaryInputNode)
+            }
+        }
+
+        val focusTargetAndRotaryNode = FocusTargetAndRotaryNode()
+        val focusTargetAndRotaryModifier = elementFor(key1 = null, focusTargetAndRotaryNode)
+
+        val focusRequester = FocusRequester()
+        val targetTestTag = "target"
+
+        rule.setFocusableContent(extraItemForInitialFocus = false) {
+            Box(
+                modifier = Modifier
+                    .testTag(targetTestTag)
+                    .focusRequester(focusRequester)
+                    .then(focusTargetAndRotaryModifier)
+            )
+        }
+
+        rule.runOnUiThread { focusRequester.requestFocus() }
+        assertThat(focusTargetAndRotaryNode.focusTargetNode.focusState.isFocused).isTrue()
+
+        rule.onNodeWithTag(targetTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(100f)
+        }
+
+        assertThat(focusTargetAndRotaryNode.events).hasSize(1)
+        assertThat(focusTargetAndRotaryNode.events[0].verticalScrollPixels).isEqualTo(100f)
+    }
+
     private inline fun Modifier.thenIf(condition: Boolean, block: () -> Modifier): Modifier {
         return if (condition) then(block()) else this
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index c78b8ef..257922e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -42,6 +42,7 @@
 import androidx.compose.ui.node.ancestors
 import androidx.compose.ui.node.dispatchForKind
 import androidx.compose.ui.node.nearestAncestor
+import androidx.compose.ui.node.visitAncestors
 import androidx.compose.ui.node.visitLocalDescendants
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.LayoutDirection
@@ -266,10 +267,10 @@
 
         val activeFocusTarget = rootFocusNode.findActiveFocusNode()
         val focusedKeyInputNode = activeFocusTarget?.lastLocalKeyInputNode()
-            ?: activeFocusTarget?.nearestAncestor(Nodes.KeyInput)?.node
+            ?: activeFocusTarget?.nearestAncestorIncludingSelf(Nodes.KeyInput)?.node
             ?: rootFocusNode.nearestAncestor(Nodes.KeyInput)?.node
 
-        focusedKeyInputNode?.traverseAncestors(
+        focusedKeyInputNode?.traverseAncestorsIncludingSelf(
             type = Nodes.KeyInput,
             onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
             onVisit = { if (onFocusedItem.invoke()) return true },
@@ -285,9 +286,9 @@
         }
 
         val focusedSoftKeyboardInterceptionNode = rootFocusNode.findActiveFocusNode()
-            ?.nearestAncestor(Nodes.SoftKeyboardKeyInput)
+            ?.nearestAncestorIncludingSelf(Nodes.SoftKeyboardKeyInput)
 
-        focusedSoftKeyboardInterceptionNode?.traverseAncestors(
+        focusedSoftKeyboardInterceptionNode?.traverseAncestorsIncludingSelf(
             type = Nodes.SoftKeyboardKeyInput,
             onPreVisit = { if (it.onPreInterceptKeyBeforeSoftKeyboard(keyEvent)) return true },
             onVisit = { /* TODO(b/320510084): dispatch soft keyboard events to embedded views. */ },
@@ -305,9 +306,9 @@
         }
 
         val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
-            ?.nearestAncestor(Nodes.RotaryInput)
+            ?.nearestAncestorIncludingSelf(Nodes.RotaryInput)
 
-        focusedRotaryInputNode?.traverseAncestors(
+        focusedRotaryInputNode?.traverseAncestorsIncludingSelf(
             type = Nodes.RotaryInput,
             onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
             onVisit = { /* TODO(b/320510084): dispatch rotary events to embedded views. */ },
@@ -341,7 +342,7 @@
         }
     }
 
-    private inline fun <reified T : DelegatableNode> DelegatableNode.traverseAncestors(
+    private inline fun <reified T : DelegatableNode> DelegatableNode.traverseAncestorsIncludingSelf(
         type: NodeKind<T>,
         onPreVisit: (T) -> Unit,
         onVisit: () -> Unit,
@@ -355,6 +356,15 @@
         ancestors?.fastForEach(onPostVisit)
     }
 
+    private inline fun <reified T : Any> DelegatableNode.nearestAncestorIncludingSelf(
+        type: NodeKind<T>
+    ): T? {
+        visitAncestors(type, includeSelf = true) {
+            return it
+        }
+        return null
+    }
+
     /**
      * Searches for the currently focused item, and returns its coordinates as a rect.
      */