Merge "Add support for lazy with commit" into androidx-main
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index 5d4cdac..8500c36 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -18,11 +18,22 @@
 
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ComposeUiFlags
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -48,6 +59,8 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import kotlin.test.Ignore
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -742,6 +755,103 @@
         rule.runOnIdle { verifyNoMoreInteractions(am) }
     }
 
+    @Test
+    @SmallTest
+    fun autofillManager_lazyColumnScroll_callsCommit() {
+        lateinit var state: LazyListState
+        lateinit var coroutineScope: CoroutineScope
+        val am: PlatformAutofillManager = mock()
+        val count = 100
+
+        rule.setContent {
+            coroutineScope = rememberCoroutineScope()
+            state = rememberLazyListState()
+            (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+
+            LazyColumn(Modifier.fillMaxWidth().height(50.dp), state) {
+                item {
+                    Box(
+                        Modifier.semantics {
+                                contentType = ContentType.Username
+                                contentDataType = ContentDataType.Text
+                            }
+                            .size(10.dp)
+                    )
+                }
+                items(count) { Box(Modifier.size(10.dp)) }
+            }
+        }
+
+        rule.runOnIdle { coroutineScope.launch { state.scrollToItem(10) } }
+
+        rule.runOnIdle { verify(am).commit() }
+    }
+
+    @Test
+    @SmallTest
+    fun autofillManager_columnScroll_doesNotCallCommit() {
+        lateinit var scrollState: ScrollState
+        lateinit var coroutineScope: CoroutineScope
+        val am: PlatformAutofillManager = mock()
+
+        rule.setContent {
+            coroutineScope = rememberCoroutineScope()
+            scrollState = rememberScrollState()
+            (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+
+            Column(Modifier.fillMaxWidth().height(50.dp).verticalScroll(scrollState)) {
+                Row {
+                    Box(
+                        Modifier.semantics {
+                                contentType = ContentType.Username
+                                contentDataType = ContentDataType.Text
+                            }
+                            .size(10.dp)
+                    )
+                }
+                repeat(50) { Box(Modifier.size(10.dp)) }
+            }
+        }
+
+        rule.runOnIdle { coroutineScope.launch { scrollState.scrollTo(scrollState.maxValue / 2) } }
+
+        // Scrolling past an element in a column is not enough to call commit
+        rule.runOnIdle { verify(am, never()).commit() }
+    }
+
+    @Test
+    @SmallTest
+    fun autofillManager_column_nodesDisappearingCallsCommit() {
+        lateinit var scrollState: ScrollState
+        val am: PlatformAutofillManager = mock()
+        var autofillComponentsVisible by mutableStateOf(true)
+
+        rule.setContent {
+            (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+            scrollState = rememberScrollState()
+
+            Column(Modifier.fillMaxWidth().height(50.dp).verticalScroll(scrollState)) {
+                if (autofillComponentsVisible) {
+                    Row {
+                        Box(
+                            Modifier.semantics {
+                                    contentType = ContentType.Username
+                                    contentDataType = ContentDataType.Text
+                                }
+                                .size(10.dp)
+                        )
+                    }
+                }
+                repeat(50) { Box(Modifier.size(10.dp)) }
+            }
+        }
+
+        rule.runOnIdle { autofillComponentsVisible = false }
+
+        // A column disappearing will call commit
+        rule.runOnIdle { verify(am).commit() }
+    }
+
     // ============================================================================================
     // Helper functions
     // ============================================================================================
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 3a2f75d..d3fed6e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -227,9 +227,27 @@
         }
     }
 
-    internal fun onDetach(semanticsInfo: SemanticsInfo) {
+    internal fun onPostLayoutNodeReused(semanticsInfo: SemanticsInfo, previousSemanticsId: Int) {
+        if (currentlyDisplayedIDs.remove(previousSemanticsId)) {
+            pendingChangesToDisplayedIds = true
+        }
         if (semanticsInfo.semanticsConfiguration?.isRelatedToAutofill() == true) {
-            currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)
+            currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
+            pendingChangesToDisplayedIds = true
+        }
+    }
+
+    internal fun onLayoutNodeDeactivated(semanticsInfo: SemanticsInfo) {
+        if (currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)) {
+            pendingChangesToDisplayedIds = true
+            // TODO(MNUZEN): Notify autofill manager that a node has been removed.
+            // platformAutofillManager
+            //     .notifyViewVisibilityChanged(view, semanticsInfo.semanticsId, false)
+        }
+    }
+
+    internal fun onDetach(semanticsInfo: SemanticsInfo) {
+        if (currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)) {
             pendingChangesToDisplayedIds = true
             // TODO(MNUZEN): Notify autofill manager that a node has been removed.
             // platformAutofillManager
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 8872548..5197336 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -1802,14 +1802,25 @@
         if (ComposeUiFlags.isRectTrackingEnabled) {
             rectManager.remove(layoutNode)
         }
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+            _autofillManager?.onLayoutNodeDeactivated(layoutNode)
+        }
     }
 
-    override fun onLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
+    override fun onPreLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
         // Keep the mapping up to date when the semanticsId changes
         layoutNodes.remove(oldSemanticsId)
         layoutNodes[layoutNode.semanticsId] = layoutNode
     }
 
+    override fun onPostLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+            _autofillManager?.onPostLayoutNodeReused(layoutNode, oldSemanticsId)
+        }
+    }
+
     override fun onInteropViewLayoutChange(view: InteropView) {
         isPendingInteropViewLayoutChangeDispatch = true
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index bc99aece..5f60779 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -1377,7 +1377,7 @@
         }
         val oldSemanticsId = semanticsId
         semanticsId = generateSemanticsId()
-        owner?.onLayoutNodeReused(this, oldSemanticsId)
+        owner?.onPreLayoutNodeReused(this, oldSemanticsId)
         // resetModifierState detaches all nodes, so we need to re-attach them upon reuse.
         nodes.markAsAttached()
         nodes.runAttachLifecycle()
@@ -1386,6 +1386,7 @@
             invalidateSemantics()
         }
         rescheduleRemeasureOrRelayout(this)
+        owner?.onPostLayoutNodeReused(this, oldSemanticsId)
     }
 
     override fun onDeactivate() {
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 2f4b773..a75bf87 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
@@ -289,8 +289,18 @@
 
     fun onLayoutNodeDeactivated(layoutNode: LayoutNode)
 
-    /** Called to do internal upkeep when a [layoutNode] is reused. */
-    fun onLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
+    /**
+     * Called to do internal upkeep when a [layoutNode] is reused. The modifier nodes of this layout
+     * node have not been attached by the time this method finishes running.
+     */
+    fun onPreLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
+
+    /**
+     * Called to do internal upkeep when a [layoutNode] is reused. This is only called after
+     * [onPreLayoutNodeReused], at which point the modifier nodes of this layout node have been
+     * attached.
+     */
+    fun onPostLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
 
     /**
      * The position and/or size of an interop view (typically, an android.view.View) has changed. On