[go: nahoru, domu]

Merge "Remove onSurfaceVariant2 from Compose for Wear OS Material Theme Color class" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
index 5b27d06..4ec1db8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/CreateLibraryBuildInfoFileTask.kt
@@ -17,8 +17,8 @@
 package androidx.build
 
 import androidx.build.gitclient.Commit
-import androidx.build.gitclient.GitClient
 import androidx.build.gitclient.GitCommitRange
+import androidx.build.gitclient.GitRunnerGitClient
 import androidx.build.jetpad.LibraryBuildInfoFile
 import com.google.gson.GsonBuilder
 import org.gradle.api.DefaultTask
@@ -233,13 +233,8 @@
          * of the build that is released.  Thus, we use frameworks/support to get the sha
          */
         private fun Project.getFrameworksSupportCommitShaAtHead(): String {
-            val gitClient = GitClient.create(
-                project.getSupportRootFolder(),
-                logger,
-                GitClient.getChangeInfoPath(project).get()
-            )
             val commitList: List<Commit> =
-                gitClient
+                GitRunnerGitClient(project.getSupportRootFolder(), logger)
                 .getGitLog(
                     GitCommitRange(
                         fromExclusive = "",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 31b60cb..ac41dbe 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -18,7 +18,7 @@
 
 import androidx.build.dependencyTracker.AffectedModuleDetector.Companion.ENABLE_ARG
 import androidx.build.getDistributionDirectory
-import androidx.build.gitclient.GitClient
+import androidx.build.gitclient.GitRunnerGitClient
 import androidx.build.gradle.isRoot
 import java.io.File
 import org.gradle.api.Action
@@ -146,8 +146,6 @@
             if (baseCommitOverride != null) {
                 logger.info("using base commit override $baseCommitOverride")
             }
-            val changeInfoPath = GitClient.getChangeInfoPath(rootProject)
-                .forUseAtConfigurationTime()
             gradle.taskGraph.whenReady {
                 logger.lifecycle("projects evaluated")
                 val projectGraph = ProjectGraph(rootProject)
@@ -161,7 +159,6 @@
                         params.dependencyTracker = dependencyTracker
                         params.log = outputFile
                         params.baseCommitOverride = baseCommitOverride
-                        params.changeInfoPath = changeInfoPath
                     }
                 )
                 logger.info("using real detector")
@@ -263,7 +260,6 @@
         var alwaysBuildIfExists: Set<String>?
         var ignoredPaths: Set<String>?
         var baseCommitOverride: String?
-        var changeInfoPath: Provider<String>
     }
 
     val detector: AffectedModuleDetector by lazy {
@@ -275,10 +271,9 @@
             if (baseCommitOverride != null) {
                 logger.info("using base commit override $baseCommitOverride")
             }
-            val gitClient = GitClient.create(
-                rootProjectDir = parameters.rootDir,
-                logger = logger,
-                changeInfoPath = parameters.changeInfoPath.get()
+            val gitClient = GitRunnerGitClient(
+                workingDir = parameters.rootDir,
+                logger = logger
             )
             val changedFilesProvider: ChangedFilesProvider = {
                 val baseSha = baseCommitOverride ?: gitClient.findPreviousSubmittedChange()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
index 9137ebf..a30fa85 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -19,9 +19,6 @@
 import androidx.build.releasenotes.getBuganizerLink
 import androidx.build.releasenotes.getChangeIdAOSPLink
 import java.io.File
-import org.gradle.api.Project
-import org.gradle.api.provider.Provider
-import org.gradle.api.logging.Logger
 
 interface GitClient {
     fun findChangedFilesSince(
@@ -50,24 +47,6 @@
          */
         fun executeAndParse(command: String): List<String>
     }
-
-    companion object {
-        fun getChangeInfoPath(project: Project): Provider<String> {
-            return project.providers.environmentVariable("CHANGE_INFO")
-        }
-        fun create(rootProjectDir: File, logger: Logger, changeInfoPath: String): GitClient {
-            if (changeInfoPath != "") {
-                val changeInfoFile = File(changeInfoPath)
-                if (changeInfoFile.exists()) {
-                    val text = changeInfoFile.readText()
-                    logger.info("Using ChangeInfoGitClient, config = " + changeInfoPath)
-                    return ChangeInfoGitClient(text)
-                }
-            }
-            logger.info("UsingGitRunnerGitClient")
-            return GitRunnerGitClient(rootProjectDir, logger)
-        }
-    }
 }
 
 enum class CommitType {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
new file mode 100644
index 0000000..1a270eb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 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.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyLayoutTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun lazyListShowsCombinedItems() {
+        val counter = mutableStateOf(0)
+        var remeasureCount = 0
+        val policy = LazyMeasurePolicy { _, _ ->
+            remeasureCount++
+            object : LazyLayoutMeasureResult {
+                override val visibleItemsInfo: List<LazyLayoutItemInfo> = emptyList()
+                override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
+                override val height: Int = 10
+                override val width: Int = 10
+                override fun placeChildren() {}
+            }
+        }
+        val itemsProvider = {
+            object : LazyLayoutItemsProvider {
+                override fun getContent(index: Int): @Composable () -> Unit = {}
+                override val itemsCount: Int = 0
+                override fun getKey(index: Int) = Unit
+                override val keyToIndexMap: Map<Any, Int> = emptyMap()
+            }
+        }
+
+        rule.setContent {
+            counter.value // just to trigger recomposition
+            LazyLayout(
+                itemsProvider = itemsProvider,
+                measurePolicy = policy,
+                // this will return a new object everytime causing LazyLayout recomposition
+                // without causing remeasure
+                modifier = Modifier.composed { Modifier }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasureCount).isEqualTo(1)
+            counter.value++
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasureCount).isEqualTo(1)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index ae44ce4..251138b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -21,6 +21,7 @@
 import androidx.activity.ComponentActivity
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -60,7 +61,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.width
 import androidx.test.espresso.matcher.BoundedMatcher
@@ -87,16 +87,6 @@
     @get:Rule
     val rule = createAndroidComposeRule<ComponentActivity>()
 
-    private val composeViewAbsolutePos: IntOffset get() {
-        // Get the view position on screen
-        val positionArray = IntArray(2)
-        view.getLocationOnScreen(positionArray)
-        return IntOffset(
-            positionArray[0],
-            positionArray[1]
-        )
-    }
-
     private lateinit var view: View
 
     private val textContent = "Text Demo Text"
@@ -166,7 +156,7 @@
             // Long Press "m" in "Demo", and "Demo" should be selected.
             createSelectionContainer()
             val characterSize = fontSize.toPx()
-            val expectedLeftX = fontSize.toDp().times(textContent.indexOf('D')) - 25.dp
+            val expectedLeftX = fontSize.toDp().times(textContent.indexOf('D'))
             val expectedLeftY = fontSize.toDp()
             val expectedRightX = fontSize.toDp().times(textContent.indexOf('o') + 1)
             val expectedRightY = fontSize.toDp()
@@ -188,20 +178,14 @@
                     times(1)
                 ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
             }
-            rule.doubleSelectionHandleMatches(
-                0,
-                matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedRightX,
-                    composeViewAbsolutePos.y.toDp() + expectedRightY
-                )
-            )
-            rule.doubleSelectionHandleMatches(
-                1,
-                matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedLeftX,
-                    composeViewAbsolutePos.y.toDp() + expectedLeftY
-                )
-            )
+
+            // Check the position of the anchors of the selection handles. We don't need to compare
+            // to the absolute position since the semantics report selection relative to the
+            // container composable, not the screen.
+            rule.onNode(isSelectionHandle(Handle.SelectionStart))
+                .assertHandlePositionMatches(expectedLeftX, expectedLeftY)
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+                .assertHandlePositionMatches(expectedRightX, expectedRightY)
         }
     }
 
@@ -212,8 +196,7 @@
             // Long Press "m" in "Demo", and "Demo" should be selected.
             createSelectionContainer(isRtl = true)
             val characterSize = fontSize.toPx()
-            val expectedLeftX =
-                rule.rootWidth() - fontSize.toDp().times(textContent.length) - 25.dp
+            val expectedLeftX = rule.rootWidth() - fontSize.toDp().times(textContent.length)
             val expectedLeftY = fontSize.toDp()
             val expectedRightX = rule.rootWidth() - fontSize.toDp().times(" Demo Text".length)
             val expectedRightY = fontSize.toDp()
@@ -239,20 +222,14 @@
                     times(1)
                 ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
             }
-            rule.doubleSelectionHandleMatches(
-                0,
-                matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedRightX,
-                    composeViewAbsolutePos.y.toDp() + expectedRightY
-                )
-            )
-            rule.doubleSelectionHandleMatches(
-                1,
-                matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedLeftX,
-                    composeViewAbsolutePos.y.toDp() + expectedLeftY
-                )
-            )
+
+            // Check the position of the anchors of the selection handles. We don't need to compare
+            // to the absolute position since the semantics report selection relative to the
+            // container composable, not the screen.
+            rule.onNode(isSelectionHandle(Handle.SelectionStart))
+                .assertHandlePositionMatches(expectedLeftX, expectedLeftY)
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+                .assertHandlePositionMatches(expectedRightX, expectedRightY)
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
index 99a3a68..69c23d1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest.kt
@@ -18,13 +18,14 @@
 
 import android.view.View
 import android.view.WindowManager
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.Handle
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalView
@@ -32,12 +33,9 @@
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrain
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.isPopupLayout
 import androidx.test.espresso.Espresso
@@ -55,7 +53,6 @@
 import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
-import kotlin.math.max
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -76,17 +73,20 @@
            y = offset.y
         */
         with(rule.density) {
-            val expectedPositionX = offset.x.toDp() - HandleWidth
+            val expectedAnchorPositionX = offset.x.toDp()
+            val expectedPopupPositionX = expectedAnchorPositionX - HandleWidth
             val expectedPositionY = offset.y.toDp()
 
             createSelectionHandle(isStartHandle = true)
 
             rule.singleSelectionHandleMatches(
                 matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedPositionX,
+                    composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
                     composeViewAbsolutePos.y.toDp() + expectedPositionY
                 )
             )
+            rule.onNode(isSelectionHandle(Handle.SelectionStart))
+                .assertHandlePositionMatches(expectedAnchorPositionX, expectedPositionY)
         }
     }
 
@@ -97,17 +97,20 @@
            y = offset.y
         */
         with(rule.density) {
-            val expectedPositionX = offset.x.toDp() - HandleWidth
+            val expectedAnchorPositionX = offset.x.toDp()
+            val expectedPopupPositionX = expectedAnchorPositionX - HandleWidth
             val expectedPositionY = offset.y.toDp()
 
             createSelectionHandle(isStartHandle = true, isRtl = true)
 
             rule.singleSelectionHandleMatches(
                 matchesPosition(
-                    composeViewAbsolutePos.x.toDp() + expectedPositionX,
+                    composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
                     composeViewAbsolutePos.y.toDp() + expectedPositionY
                 )
             )
+            rule.onNode(isSelectionHandle(Handle.SelectionStart))
+                .assertHandlePositionMatches(expectedAnchorPositionX, expectedPositionY)
         }
     }
 
@@ -129,6 +132,8 @@
                     composeViewAbsolutePos.y.toDp() + expectedPositionY
                 )
             )
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+                .assertHandlePositionMatches(expectedPositionX, expectedPositionY)
         }
     }
 
@@ -149,17 +154,80 @@
                     composeViewAbsolutePos.y.toDp() + expectedPositionY
                 )
             )
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+                .assertHandlePositionMatches(expectedPositionX, expectedPositionY)
         }
     }
 
-    private fun createSelectionHandle(isStartHandle: Boolean, isRtl: Boolean = false) {
+    @Test
+    fun leftHandle_semanticsPosition_isRelativeToContainer() {
+        with(rule.density) {
+            val containerOffsetX = 20.dp
+            val containerOffsetY = 30.dp
+            // Since the semantics position is relative to the container, it shouldn't include the
+            // container offset.
+            val expectedSemanticsPositionX = offset.x.toDp()
+            val expectedSemanticsPositionY = offset.y.toDp()
+            // However, the popup position, which is in absolute screen coordinates, should include
+            // the container offset.
+            val expectedPopupPositionX = containerOffsetX + expectedSemanticsPositionX - HandleWidth
+            val expectedPopupPositionY = containerOffsetY + expectedSemanticsPositionY
+
+            createSelectionHandle(
+                isStartHandle = true,
+                containerModifier = Modifier.offset(containerOffsetX, containerOffsetY)
+            )
+
+            rule.singleSelectionHandleMatches(
+                matchesPosition(
+                    composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
+                    composeViewAbsolutePos.y.toDp() + expectedPopupPositionY
+                )
+            )
+            rule.onNode(isSelectionHandle(Handle.SelectionStart))
+                .assertHandlePositionMatches(expectedSemanticsPositionX, expectedSemanticsPositionY)
+        }
+    }
+
+    @Test
+    fun rightHandle_semanticsPosition_isRelativeToContainer() {
+        with(rule.density) {
+            val containerOffsetX = 20.dp
+            val containerOffsetY = 30.dp
+            // Since the semantics position is relative to the container, it shouldn't include the
+            // container offset.
+            val expectedSemanticsPositionX = offset.x.toDp()
+            val expectedSemanticsPositionY = offset.y.toDp()
+            // However, the popup position, which is in absolute screen coordinates, should include
+            // the container offset.
+            val expectedPopupPositionX = containerOffsetX + expectedSemanticsPositionX
+            val expectedPopupPositionY = containerOffsetY + expectedSemanticsPositionY
+
+            createSelectionHandle(
+                isStartHandle = false,
+                containerModifier = Modifier.offset(containerOffsetX, containerOffsetY)
+            )
+
+            rule.singleSelectionHandleMatches(
+                matchesPosition(
+                    composeViewAbsolutePos.x.toDp() + expectedPopupPositionX,
+                    composeViewAbsolutePos.y.toDp() + expectedPopupPositionY
+                )
+            )
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd))
+                .assertHandlePositionMatches(expectedSemanticsPositionX, expectedSemanticsPositionY)
+        }
+    }
+
+    private fun createSelectionHandle(
+        isStartHandle: Boolean,
+        containerModifier: Modifier = Modifier,
+        isRtl: Boolean = false
+    ) {
         val measureLatch = CountDownLatch(1)
 
-        with(rule.density) {
-            val parentWidthDp = parentSizeWidth
-            val parentHeightDp = parentSizeHeight
-
-            rule.setContent {
+        rule.setContent {
+            Box(containerModifier) {
                 // Get the compose view position on screen
                 val composeView = LocalView.current
                 val positionArray = IntArray(2)
@@ -174,7 +242,7 @@
                 val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
                 CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
                     SimpleLayout {
-                        SimpleContainer(width = parentWidthDp, height = parentHeightDp) {}
+                        Spacer(Modifier.size(parentSizeWidth, parentSizeHeight))
                         SelectionHandle(
                             position = offset,
                             isStartHandle = isStartHandle,
@@ -194,48 +262,48 @@
 
     private fun matchesPosition(expectedPositionX: Dp, expectedPositionY: Dp):
         BoundedMatcher<View, View> {
-            return object : BoundedMatcher<View, View>(View::class.java) {
-                // (-1, -1) no position found
-                var positionFound = IntOffset(-1, -1)
+        return object : BoundedMatcher<View, View>(View::class.java) {
+            // (-1, -1) no position found
+            var positionFound = IntOffset(-1, -1)
 
-                override fun matchesSafely(item: View?): Boolean {
-                    with(rule.density) {
-                        val position = IntArray(2)
-                        item?.getLocationOnScreen(position)
-                        positionFound = IntOffset(position[0], position[1])
+            override fun matchesSafely(item: View?): Boolean {
+                with(rule.density) {
+                    val position = IntArray(2)
+                    item?.getLocationOnScreen(position)
+                    positionFound = IntOffset(position[0], position[1])
 
-                        val expectedPositionXInt = expectedPositionX.value.toInt()
-                        val expectedPositionYInt = expectedPositionY.value.toInt()
-                        val positionFoundXInt = positionFound.x.toDp().value.toInt()
-                        val positionFoundYInt = positionFound.y.toDp().value.toInt()
-                        return expectedPositionXInt == positionFoundXInt &&
-                            expectedPositionYInt == positionFoundYInt
-                    }
+                    val expectedPositionXInt = expectedPositionX.value.toInt()
+                    val expectedPositionYInt = expectedPositionY.value.toInt()
+                    val positionFoundXInt = positionFound.x.toDp().value.toInt()
+                    val positionFoundYInt = positionFound.y.toDp().value.toInt()
+                    return expectedPositionXInt == positionFoundXInt &&
+                        expectedPositionYInt == positionFoundYInt
                 }
+            }
 
-                override fun describeTo(description: Description?) {
-                    with(rule.density) {
-                        description?.appendText(
-                            "with expected position: " +
-                                "${expectedPositionX.value}, ${expectedPositionY.value} " +
-                                "but position found:" +
-                                "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
-                        )
-                    }
+            override fun describeTo(description: Description?) {
+                with(rule.density) {
+                    description?.appendText(
+                        "with expected position: " +
+                            "${expectedPositionX.value}, ${expectedPositionY.value} " +
+                            "but position found:" +
+                            "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
+                    )
                 }
             }
         }
+    }
 }
 
-internal fun ComposeTestRule.singleSelectionHandleMatches(viewMatcher: Matcher<in View>) {
+private fun ComposeTestRule.singleSelectionHandleMatches(viewMatcher: Matcher<in View>) {
     // Make sure that current measurement/drawing is finished
-    runOnIdle { }
+    waitForIdle()
     Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
         .inRoot(SingleSelectionHandleMatcher())
         .check(ViewAssertions.matches(viewMatcher))
 }
 
-internal class SingleSelectionHandleMatcher : TypeSafeMatcher<Root>() {
+private class SingleSelectionHandleMatcher : TypeSafeMatcher<Root>() {
 
     var lastSeenWindowParams: WindowManager.LayoutParams? = null
 
@@ -250,61 +318,4 @@
         }
         return matches
     }
-}
-
-/**
- * A Container Box implementation used for selection children and handle layout
- */
-@Composable
-internal fun SimpleContainer(
-    modifier: Modifier = Modifier,
-    width: Dp? = null,
-    height: Dp? = null,
-    content: @Composable () -> Unit
-) {
-    Layout(content, modifier) { measurables, incomingConstraints ->
-        val containerConstraints =
-            incomingConstraints.constrain(
-                Constraints().copy(
-                    width?.roundToPx() ?: 0,
-                    width?.roundToPx() ?: Constraints.Infinity,
-                    height?.roundToPx() ?: 0,
-                    height?.roundToPx() ?: Constraints.Infinity
-                )
-            )
-        val childConstraints = containerConstraints.copy(minWidth = 0, minHeight = 0)
-        var placeable: Placeable? = null
-        val containerWidth = if (
-            containerConstraints.hasFixedWidth
-        ) {
-            containerConstraints.maxWidth
-        } else {
-            placeable = measurables.firstOrNull()?.measure(childConstraints)
-            max((placeable?.width ?: 0), containerConstraints.minWidth)
-        }
-        val containerHeight = if (
-            containerConstraints.hasFixedHeight
-        ) {
-            containerConstraints.maxHeight
-        } else {
-            if (placeable == null) {
-                placeable = measurables.firstOrNull()?.measure(childConstraints)
-            }
-            max((placeable?.height ?: 0), containerConstraints.minHeight)
-        }
-        layout(containerWidth, containerHeight) {
-            val p = placeable ?: measurables.firstOrNull()?.measure(childConstraints)
-            p?.let {
-                val position = Alignment.Center.align(
-                    IntSize(it.width, it.height),
-                    IntSize(containerWidth, containerHeight),
-                    layoutDirection
-                )
-                it.placeRelative(
-                    position.x,
-                    position.y
-                )
-            }
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index 119d458..3a4a843 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -16,41 +16,46 @@
 
 package androidx.compose.foundation.text.selection
 
-import android.view.View
-import androidx.compose.ui.platform.ViewRootForTest
-import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.window.isPopupLayout
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.Root
-import androidx.test.espresso.assertion.ViewAssertions
-import org.hamcrest.CoreMatchers
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
+import androidx.compose.foundation.text.Handle
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.unit.Dp
+import com.google.common.truth.Truth.assertWithMessage
 
-internal fun ComposeTestRule.doubleSelectionHandleMatches(
-    index: Int,
-    viewMatcher: Matcher<in View>
-) {
-    // Make sure that current measurement/drawing is finished
-    runOnIdle { }
-    Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
-        .inRoot(DoubleSelectionHandleMatcher(index))
-        .check(ViewAssertions.matches(viewMatcher))
-}
-
-internal class DoubleSelectionHandleMatcher(val index: Int) : TypeSafeMatcher<Root>() {
-    var popupsMatchedSoFar: Int = 0
-
-    override fun describeTo(description: Description?) {
-        description?.appendText("DoubleSelectionHandleMatcher")
+/**
+ * Matches selection handles by looking for the [SelectionHandleInfoKey] property that has a
+ * [SelectionHandleInfo] with the given [handle]. If [handle] is null (the default), then all
+ * handles are matched.
+ */
+internal fun isSelectionHandle(handle: Handle? = null) =
+    SemanticsMatcher("is ${handle ?: "any"} handle") { node ->
+        if (handle == null) {
+            SelectionHandleInfoKey in node.config
+        } else {
+            node.config.getOrNull(SelectionHandleInfoKey)?.handle == handle
+        }
     }
 
-    override fun matchesSafely(item: Root?): Boolean {
-        val matches = item != null && isPopupLayout(item.decorView)
-        if (matches) {
-            popupsMatchedSoFar++
-        }
-        return matches && popupsMatchedSoFar == index + 1
+/**
+ * Asserts about the [SelectionHandleInfo.position] for the matching node. This is the position of
+ * the handle's _anchor_, not the position of the popup itself. E.g. for a cursor handle this is the
+ * position of the bottom of the cursor, which will be in the center of the popup.
+ */
+internal fun SemanticsNodeInteraction.assertHandlePositionMatches(
+    expectedX: Dp,
+    expectedY: Dp
+) {
+    val node = fetchSemanticsNode()
+    with(node.layoutInfo.density) {
+        val positionFound = node.config[SelectionHandleInfoKey].position
+        val positionFoundX = positionFound.x.toDp()
+        val positionFoundY = positionFound.y.toDp()
+        val message = "Expected position ($expectedX, $expectedY), " +
+            "but found ($positionFoundX, $positionFoundY)"
+        assertWithMessage(message).that(positionFoundX.value)
+            .isWithin(5f).of(expectedX.value)
+        assertWithMessage(message).that(positionFoundY.value)
+            .isWithin(5f).of(expectedY.value)
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index 190322c..a6da521 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -46,7 +46,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import org.junit.Rule
 import org.junit.Test
@@ -246,6 +246,29 @@
     }
 
     @Test
+    fun textField_onValueChangeRecomposeTest() {
+        // sample code in b/200577798
+        val value = mutableStateOf(TextFieldValue(""))
+        var lastNewValue: TextFieldValue? = null
+        val onValueChange: (TextFieldValue) -> Unit = { newValue ->
+            lastNewValue = newValue
+            if (newValue.text.isBlank() || newValue.text.startsWith("z")) {
+                value.value = newValue
+            }
+        }
+
+        keysSequenceTest(value = value,  {
+            // based on repro steps in the ticket, one of the values would become "aa"
+            // check 10 times to make sure it is not "aa"
+            repeat(10) {
+                Key.A.downAndUp()
+                // should always be "a" and buffer should not accumulate
+                assertThat(lastNewValue?.text).isEqualTo("a")
+            }
+        }
+    }
+
+    @Test
     fun textField_pageNavigation() {
         keysSequenceTest(
             initText = "1\n2\n3\n4\n5",
@@ -275,13 +298,13 @@
 
         fun expectedText(text: String) {
             rule.runOnIdle {
-                Truth.assertThat(state.value.text).isEqualTo(text)
+                assertThat(state.value.text).isEqualTo(text)
             }
         }
 
         fun expectedSelection(selection: TextRange) {
             rule.runOnIdle {
-                Truth.assertThat(state.value.selection).isEqualTo(selection)
+                assertThat(state.value.selection).isEqualTo(selection)
             }
         }
     }
@@ -289,33 +312,39 @@
     private fun keysSequenceTest(
         initText: String = "",
         modifier: Modifier = Modifier.fillMaxSize(),
-        sequence: SequenceScope.() -> Unit
+        sequence: SequenceScope.() -> Unit,
+    ) {
+        val value = mutableStateOf(TextFieldValue(initText))
+        keysSequenceTest(value = value, modifier = modifier, sequence = sequence)
+    }
+
+    private fun keysSequenceTest(
+        value: MutableState<TextFieldValue>,
+        modifier: Modifier = Modifier.fillMaxSize(),
+        onValueChange: (TextFieldValue) -> Unit = { value.value = it },
+        sequence: SequenceScope.() -> Unit,
     ) {
         val inputService = TextInputService(mock())
-
-        val state = mutableStateOf(TextFieldValue(initText))
         val focusFequester = FocusRequester()
         rule.setContent {
             CompositionLocalProvider(
                 LocalTextInputService provides inputService
             ) {
                 BasicTextField(
-                    value = state.value,
+                    value = value.value,
                     textStyle = TextStyle(
                         fontFamily = TEST_FONT_FAMILY,
                         fontSize = 10.sp
                     ),
                     modifier = modifier.focusRequester(focusFequester),
-                    >
-                        state.value = it
-                    }
+                    >
                 )
             }
         }
 
         rule.runOnIdle { focusFequester.requestFocus() }
 
-        sequence(SequenceScope(state) { rule.onNode(hasSetTextAction()) })
+        sequence(SequenceScope(value) { rule.onNode(hasSetTextAction()) })
     }
 }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index ff02ea8..50b289d 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -30,9 +30,11 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TextFieldScrollerPosition
 import androidx.compose.foundation.text.TextLayoutResultProxy
 import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.selection.isSelectionHandle
 import androidx.compose.foundation.text.textFieldScroll
 import androidx.compose.foundation.text.textFieldScrollable
 import androidx.compose.foundation.verticalScroll
@@ -576,10 +578,7 @@
         rule.onNodeWithTag(tag)
             .performClick()
 
-        // Check that handle is displayed (if it's not, we can't check if it gets hidden).
-        rule.onNode(isPopup())
-            .assertIsDisplayed()
-        // TODO(b/139861182) Assert handle position is within field bounds?
+        rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
 
         // Scroll up by twice the height to move the cursor out of the visible area.
         rule.onNodeWithTag(tag)
@@ -589,7 +588,7 @@
             }
 
         // Check that cursor is hidden.
-        rule.onNode(isPopup()).assertDoesNotExist()
+        rule.onAllNodes(isSelectionHandle()).assertCountEquals(0)
 
         // Scroll back and make sure the handles are shown again.
         rule.onNodeWithTag(tag)
@@ -597,7 +596,7 @@
                 moveBy(Offset(x = 0f, y = size * 2f))
             }
 
-        rule.onNode(isPopup()).assertIsDisplayed()
+        rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
     }
 
     @OptIn(ExperimentalTestApi::class)
@@ -606,19 +605,6 @@
         val size = 200
         val tag = "Text"
 
-        fun assertHandlesDisplayed() {
-            rule.onAllNodes(isPopup())
-                .assertCountEquals(2)
-                .apply {
-                    (0 until 2)
-                        .map(::get)
-                        .forEach {
-                            it.assertIsDisplayed()
-                            // TODO(b/139861182) Assert handle position is within field bounds?
-                        }
-                }
-        }
-
         with(rule.density) {
             rule.setContent {
                 BasicTextField(
@@ -644,7 +630,8 @@
             }
 
         // Check that both handles are displayed (if not, we can't check that they get hidden).
-        assertHandlesDisplayed()
+        rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
+        rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
 
         // Scroll up by twice the height to move the cursor out of the visible area.
         rule.onNodeWithTag(tag)
@@ -663,7 +650,8 @@
                 moveBy(Offset(x = 0f, y = size * 2f))
             }
 
-        assertHandlesDisplayed()
+        rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
+        rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
     }
 
     private fun ComposeContentTestRule.setupHorizontallyScrollableContent(
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
index 7b9119d..2e36f5e 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
@@ -18,6 +18,10 @@
 
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopLeft
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopMiddle
+import androidx.compose.foundation.text.selection.HandleReferencePoint.TopRight
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -34,6 +38,7 @@
 import androidx.compose.ui.graphics.ImageBitmapConfig
 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
 import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
@@ -61,10 +66,21 @@
     } else {
         HandleReferencePoint.TopLeft
     }
+
     HandlePopup(position = position, handleReferencePoint = handleReferencePoint) {
         if (content == null) {
             DefaultSelectionHandle(
-                modifier = modifier,
+                modifier = modifier
+                    .semantics {
+                        this[SelectionHandleInfoKey] = SelectionHandleInfo(
+                            handle = if (isStartHandle) {
+                                Handle.SelectionStart
+                            } else {
+                                Handle.SelectionEnd
+                            },
+                            position = position
+                        )
+                    },
                 isStartHandle = isStartHandle,
                 direction = direction,
                 handlesCrossed = handlesCrossed
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 4ad43a3..10add68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -43,23 +43,28 @@
 
     SubcomposeLayout(
         subcomposeLayoutState,
-        modifier.then(state.remeasurementModifier)
-    ) { constraints ->
-        itemContentFactory.onBeforeMeasure(this, constraints)
+        modifier.then(state.remeasurementModifier),
+        remember(itemContentFactory, state, measurePolicy) {
+            { constraints ->
+                itemContentFactory.onBeforeMeasure(this, constraints)
 
-        val placeablesProvider = LazyLayoutPlaceablesProvider(
-            state.itemsProvider(),
-            itemContentFactory,
-            this
-        )
-        val measureResult = with(measurePolicy) { measure(placeablesProvider, constraints) }
+                val placeablesProvider = LazyLayoutPlaceablesProvider(
+                    state.itemsProvider(),
+                    itemContentFactory,
+                    this
+                )
+                val measureResult = with(measurePolicy) { measure(placeablesProvider, constraints) }
 
-        state.onPostMeasureListener?.apply { onPostMeasure(measureResult, placeablesProvider) }
-        state.layoutInfoState.value = measureResult
-        state.layoutInfoNonObservable = measureResult
+                state.onPostMeasureListener?.apply {
+                    onPostMeasure(measureResult, placeablesProvider)
+                }
+                state.layoutInfoState.value = measureResult
+                state.layoutInfoNonObservable = measureResult
 
-        measureResult
-    }
+                measureResult
+            }
+        }
+    )
 }
 
 private const val MaxItemsToRetainForReuse = 2
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 68083e0..74dd799 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.SelectionHandleInfo
+import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
 import androidx.compose.foundation.text.selection.SimpleLayout
 import androidx.compose.foundation.text.selection.TextFieldSelectionHandle
 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
@@ -486,6 +488,7 @@
             state = state,
             manager = manager,
             value = value,
+            >
             editable = !readOnly,
             singleLine = maxLines == 1,
             offsetMapping = offsetMapping,
@@ -878,9 +881,16 @@
         val position = manager.getCursorPosition(LocalDensity.current)
         CursorHandle(
             handlePosition = position,
-            modifier = Modifier.pointerInput(observer) {
-                detectDragGesturesWithObserver(observer)
-            },
+            modifier = Modifier
+                .pointerInput(observer) {
+                    detectDragGesturesWithObserver(observer)
+                }
+                .semantics {
+                    this[SelectionHandleInfoKey] = SelectionHandleInfo(
+                        handle = Handle.Cursor,
+                        position = position
+                    )
+                },
             content = null
         )
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
index c4217c8..69751f3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -56,6 +56,7 @@
     val offsetMapping: OffsetMapping = OffsetMapping.Identity,
     val undoManager: UndoManager? = null,
     private val keyMapping: KeyMapping = platformDefaultKeyMapping,
+    private val onValueChange: (TextFieldValue) -> Unit = {}
 ) {
     private fun List<EditCommand>.apply() {
         val newTextFieldValue = state.processor.apply(
@@ -63,12 +64,8 @@
                 add(0, FinishComposingTextCommand())
             }
         )
-        @OptIn(InternalFoundationTextApi::class)
-        if (newTextFieldValue.annotatedString.text != state.textDelegate.text.text) {
-            // Text has been changed, enter the HandleState.None and hide the cursor handle.
-            state.handleState = HandleState.None
-        }
-        state.onValueChange(newTextFieldValue)
+
+        onValueChange(newTextFieldValue)
     }
 
     private fun EditCommand.apply() {
@@ -192,10 +189,10 @@
                 KeyCommand.DESELECT -> deselect()
                 KeyCommand.UNDO -> {
                     undoManager?.makeSnapshot(value)
-                    undoManager?.undo()?.let { this@TextFieldKeyInput.state.onValueChange(it) }
+                    undoManager?.undo()?.let { this@TextFieldKeyInput.onValueChange(it) }
                 }
                 KeyCommand.REDO -> {
-                    undoManager?.redo()?.let { this@TextFieldKeyInput.state.onValueChange(it) }
+                    undoManager?.redo()?.let { this@TextFieldKeyInput.onValueChange(it) }
                 }
                 KeyCommand.CHARACTER_PALETTE -> { showCharacterPalette() }
             }
@@ -215,7 +212,7 @@
         if (preparedSelection.selection != value.selection ||
             preparedSelection.annotatedString != value.annotatedString
         ) {
-            state.onValueChange(preparedSelection.value)
+            onValueChange(preparedSelection.value)
         }
     }
 }
@@ -225,6 +222,7 @@
     state: TextFieldState,
     manager: TextFieldSelectionManager,
     value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit = {},
     editable: Boolean,
     singleLine: Boolean,
     offsetMapping: OffsetMapping,
@@ -239,7 +237,8 @@
         singleLine = singleLine,
         offsetMapping = offsetMapping,
         preparedSelectionState = preparedSelectionState,
-        undoManager = undoManager
+        undoManager = undoManager,
+        >
     )
     Modifier.onKeyEvent(processor::process)
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
index 157ac29..97a367d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
@@ -16,15 +16,37 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.compose.foundation.text.Handle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.SemanticsPropertyKey
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.unit.dp
 
 internal val HandleWidth = 25.dp
 internal val HandleHeight = 25.dp
 
+/**
+ * [SelectionHandleInfo]s for the nodes representing selection handles. These nodes are in popup
+ * windows, and will respond to drag gestures.
+ */
+internal val SelectionHandleInfoKey =
+    SemanticsPropertyKey<SelectionHandleInfo>("SelectionHandleInfo")
+
+/**
+ * Information about a single selection handle popup.
+ *
+ * @param handle Which selection [Handle] this is about.
+ * @param position The position that the handle is anchored to relative to the selectable content.
+ * This position is not necessarily the position of the popup itself, it's the position that the
+ * handle "points" to (so e.g. top-middle for [Handle.Cursor]).
+ */
+internal data class SelectionHandleInfo(
+    val handle: Handle,
+    val position: Offset
+)
+
 @Composable
 internal expect fun SelectionHandle(
     position: Offset,
diff --git a/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt b/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
index 72c542f..744276e 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/PersistableBundle.kt
@@ -31,8 +31,10 @@
  */
 @RequiresApi(21)
 fun persistableBundleOf(vararg pairs: Pair<String, Any?>): PersistableBundle {
-    val persistableBundle = Api21Impl.createPersistableBundle(pairs.size)
-    pairs.forEach { (key, value) -> Api21Impl.putValue(persistableBundle, key, value) }
+    val persistableBundle = PersistableBundleApi21ImplKt.createPersistableBundle(pairs.size)
+    pairs.forEach { (key, value) ->
+        PersistableBundleApi21ImplKt.putValue(persistableBundle, key, value)
+    }
     return persistableBundle
 }
 
@@ -46,17 +48,20 @@
  */
 @RequiresApi(21)
 fun Map<String, Any?>.toPersistableBundle(): PersistableBundle {
-    val persistableBundle = Api21Impl.createPersistableBundle(this.size)
+    val persistableBundle = PersistableBundleApi21ImplKt.createPersistableBundle(this.size)
 
     for ((key, value) in this) {
-        Api21Impl.putValue(persistableBundle, key, value)
+        PersistableBundleApi21ImplKt.putValue(persistableBundle, key, value)
     }
 
     return persistableBundle
 }
 
+// These classes ends up being top-level even though they're private. The PersistableBundle prefix
+// helps prevent clashes with other ApiImpls in androidx.core.os. And the Kt suffix is used by
+// Jetifier to keep them grouped with other members of the core-ktx module.
 @RequiresApi(21)
-private object Api21Impl {
+private object PersistableBundleApi21ImplKt {
     @DoNotInline
     @JvmStatic
     fun createPersistableBundle(capacity: Int): PersistableBundle = PersistableBundle(capacity)
@@ -71,7 +76,7 @@
                 // Scalars
                 is Boolean -> {
                     if (Build.VERSION.SDK_INT >= 22) {
-                        Api22Impl.putBoolean(this, key, value)
+                        PersistableBundleApi22ImplKt.putBoolean(this, key, value)
                     } else {
                         throw IllegalArgumentException(
                             "Illegal value type boolean for key \"$key\""
@@ -88,7 +93,7 @@
                 // Scalar arrays
                 is BooleanArray -> {
                     if (Build.VERSION.SDK_INT >= 22) {
-                        Api22Impl.putBooleanArray(this, key, value)
+                        PersistableBundleApi22ImplKt.putBooleanArray(this, key, value)
                     } else {
                         throw IllegalArgumentException(
                             "Illegal value type boolean[] for key \"$key\""
@@ -126,7 +131,7 @@
 }
 
 @RequiresApi(22)
-private object Api22Impl {
+private object PersistableBundleApi22ImplKt {
     @DoNotInline
     @JvmStatic
     fun putBoolean(persistableBundle: PersistableBundle, key: String?, value: Boolean) {