Merge "[KSP2] Fix enum tests" into androidx-main
diff --git a/binarycompatibilityvalidator/OWNERS b/binarycompatibilityvalidator/OWNERS
new file mode 100644
index 0000000..a43f7c1
--- /dev/null
+++ b/binarycompatibilityvalidator/OWNERS
@@ -0,0 +1,3 @@
+jeffrygaston@google.com
+aurimas@google.com
+fsladkey@google.com
\ No newline at end of file
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
new file mode 100644
index 0000000..aa18e365
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    implementation(libs.kotlinCompiler)
+    testImplementation(libs.truth)
+    testImplementation(libs.junit)
+}
+
+androidx {
+    name = "AndroidX Binary Compatibility Validator"
+    type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
+    inceptionYear = "2024"
+    description = "Enforce binary compatibility for klibs"
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt
new file mode 100644
index 0000000..871247d
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.AbiClassifierReference
+import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiType
+import org.jetbrains.kotlin.library.abi.AbiTypeArgument
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+
+// Convenience extensions for accessing properties that may exist without have to cast repeatedly
+// For sources with documentation see https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/LibraryAbi.kt
+
+/** A classifier reference is either a simple class or a type reference **/
+internal val AbiType.classifierReference: AbiClassifierReference?
+    get() = (this as? AbiType.Simple)?.classifierReference
+/** The class name from a regular type e.g. 'Array' **/
+internal val AbiType.className: AbiQualifiedName?
+    get() = classifierReference?.className
+/** A tag from a type type parameter reference e.g. 'T' **/
+internal val AbiType.tag: String?
+    get() = classifierReference?.tag
+/** The string representation of a type, whether it is a simple type or a type reference **/
+internal val AbiType.classNameOrTag: String?
+    get() = className?.toString() ?: tag
+internal val AbiType.nullability: AbiTypeNullability?
+    get() = (this as? AbiType.Simple)?.nullability
+internal val AbiType.arguments: List<AbiTypeArgument>?
+    get() = (this as? AbiType.Simple)?.arguments
+internal val AbiTypeArgument.type: AbiType?
+    get() = (this as? AbiTypeArgument.TypeProjection)?.type
+internal val AbiTypeArgument.variance: AbiVariance?
+    get() = (this as? AbiTypeArgument.TypeProjection)?.variance
+private val AbiClassifierReference.className: AbiQualifiedName?
+    get() = (this as? AbiClassifierReference.ClassReference)?.className
+private val AbiClassifierReference.tag: String?
+    get() = (this as? AbiClassifierReference.TypeParameterReference)?.tag
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
new file mode 100644
index 0000000..52f0e9b
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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.binarycompatibilityvalidator
+
+class Cursor private constructor(
+    private val lines: List<String>,
+    private var rowIndex: Int = 0,
+    private var columnIndex: Int = 0
+) {
+    constructor(text: String) : this(text.split("\n"))
+    val currentLine: String
+        get() = lines[rowIndex].slice(columnIndex until lines[rowIndex].length)
+    fun hasNext() = rowIndex < (lines.size - 1)
+    fun nextLine() {
+        rowIndex++
+        columnIndex = 0
+    }
+
+    fun parseSymbol(
+        pattern: String,
+        peek: Boolean = false,
+        skipInlineWhitespace: Boolean = true
+    ): String? {
+        val match = Regex(pattern).find(currentLine)
+        return match?.value?.also {
+            if (!peek) {
+                val offset = it.length + currentLine.indexOf(it)
+                columnIndex += offset
+                if (skipInlineWhitespace) {
+                    skipInlineWhitespace()
+                }
+            }
+        }
+    }
+
+    fun parseValidIdentifier(peek: Boolean = false): String? =
+        parseSymbol("^[a-zA-Z_][a-zA-Z0-9_]+", peek)
+
+    fun parseWord(peek: Boolean = false): String? = parseSymbol("[a-zA-Z]+", peek)
+
+    fun copy() = Cursor(lines, rowIndex, columnIndex)
+
+    internal fun skipInlineWhitespace() {
+        while (currentLine.firstOrNull()?.isWhitespace() == true) {
+            columnIndex++
+        }
+    }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
new file mode 100644
index 0000000..2afb33a
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2024 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.
+ */
+
+// Impl classes from kotlin.library.abi.impl are necessary to instantiate parsed declarations
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.AbiClassKind
+import org.jetbrains.kotlin.library.abi.AbiCompoundName
+import org.jetbrains.kotlin.library.abi.AbiModality
+import org.jetbrains.kotlin.library.abi.AbiPropertyKind
+import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiType
+import org.jetbrains.kotlin.library.abi.AbiTypeArgument
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiTypeParameter
+import org.jetbrains.kotlin.library.abi.AbiValueParameter
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.impl.AbiTypeParameterImpl
+import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl
+import org.jetbrains.kotlin.library.abi.impl.ClassReferenceImpl
+import org.jetbrains.kotlin.library.abi.impl.SimpleTypeImpl
+import org.jetbrains.kotlin.library.abi.impl.StarProjectionImpl
+import org.jetbrains.kotlin.library.abi.impl.TypeParameterReferenceImpl
+import org.jetbrains.kotlin.library.abi.impl.TypeProjectionImpl
+
+// This file contains Cursor methods specific to parsing klib dump files
+
+internal fun Cursor.parseAbiModality(): AbiModality? {
+    val parsed = parseAbiModalityString(peek = true)?.let {
+        AbiModality.valueOf(it)
+    }
+    if (parsed != null) {
+        parseAbiModalityString()
+    }
+    return parsed
+}
+
+internal fun Cursor.parseClassKind(peek: Boolean = false): AbiClassKind? {
+    val parsed = parseClassKindString(peek = true)?.let {
+        AbiClassKind.valueOf(it)
+    }
+    if (parsed != null && !peek) {
+        parseClassKindString()
+    }
+    return parsed
+}
+
+internal fun Cursor.parsePropertyKind(peek: Boolean = false): AbiPropertyKind? {
+    val parsed = parsePropertyKindString(peek = true)?.let {
+        AbiPropertyKind.valueOf(it)
+    }
+    if (parsed != null && !peek) {
+        parsePropertyKindString()
+    }
+    return parsed
+}
+
+internal fun Cursor.hasClassKind(): Boolean {
+    val subCursor = copy()
+    subCursor.skipInlineWhitespace()
+    subCursor.parseAbiModality()
+    subCursor.parseClassModifiers()
+    return subCursor.parseClassKind() != null
+}
+
+internal fun Cursor.hasFunctionKind(): Boolean {
+    val subCursor = copy()
+    subCursor.skipInlineWhitespace()
+    subCursor.parseAbiModality()
+    subCursor.parseFunctionModifiers()
+    return subCursor.parseFunctionKind() != null
+}
+
+internal fun Cursor.hasPropertyKind(): Boolean {
+    val subCursor = copy()
+    subCursor.skipInlineWhitespace()
+    subCursor.parseAbiModality()
+    return subCursor.parsePropertyKind() != null
+}
+
+internal fun Cursor.hasGetter() = hasPropertyAccessor(GetterOrSetter.GETTER)
+internal fun Cursor.hasSetter() = hasPropertyAccessor(GetterOrSetter.SETTER)
+
+internal fun Cursor.hasGetterOrSetter() = hasGetter() || hasSetter()
+
+internal fun Cursor.parseGetterName(peek: Boolean = false): String? {
+    val cursor = subCursor(peek)
+    cursor.parseSymbol("^<get\\-") ?: return null
+    val name = cursor.parseValidIdentifier() ?: return null
+    cursor.parseSymbol("^>") ?: return null
+    return "<get-$name>"
+}
+
+internal fun Cursor.parseSetterName(peek: Boolean = false): String? {
+    val cursor = subCursor(peek)
+    cursor.parseSymbol("^<set\\-") ?: return null
+    val name = cursor.parseValidIdentifier() ?: return null
+    cursor.parseSymbol("^>") ?: return null
+    return "<set-$name>"
+}
+
+internal fun Cursor.parseGetterOrSetterName(peek: Boolean = false) =
+    parseGetterName(peek) ?: parseSetterName(peek)
+
+internal fun Cursor.parseClassModifier(peek: Boolean = false): String? =
+    parseSymbol("^(inner|value|fun|open|annotation|enum)", peek)
+
+internal fun Cursor.parseClassModifiers(): Set<String> {
+    val modifiers = mutableSetOf<String>()
+    while (parseClassModifier(peek = true) != null) {
+        modifiers.add(parseClassModifier()!!)
+    }
+    return modifiers
+}
+
+internal fun Cursor.parseFunctionKind(peek: Boolean = false) =
+    parseSymbol("^(constructor|fun)", peek)
+
+internal fun Cursor.parseFunctionModifier(peek: Boolean = false): String? =
+    parseSymbol("^(inline|suspend)", peek)
+
+internal fun Cursor.parseFunctionModifiers(): Set<String> {
+    val modifiers = mutableSetOf<String>()
+    while (parseFunctionModifier(peek = true) != null) {
+        modifiers.add(parseFunctionModifier()!!)
+    }
+    return modifiers
+}
+
+internal fun Cursor.parseAbiQualifiedName(peek: Boolean = false): AbiQualifiedName? {
+    val symbol = parseSymbol("^[a-zA-Z0-9\\.]+\\/[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?", peek)
+        ?: return null
+    val (packageName, relativeName) = symbol.split("/")
+    return AbiQualifiedName(
+        AbiCompoundName(packageName),
+        AbiCompoundName(relativeName)
+    )
+}
+
+internal fun Cursor.parseAbiType(peek: Boolean = false): AbiType? {
+    val cursor = subCursor(peek)
+    // A type will either be a qualified name (kotlin/Array) or a type reference (#A)
+    // try to parse a qualified name and a type reference if it doesn't exist
+    val abiQualifiedName = cursor.parseAbiQualifiedName()
+        ?: return cursor.parseTypeReference()
+    val typeArgs = cursor.parseTypeArgs() ?: emptyList()
+    val nullability = cursor.parseNullability(assumeNotNull = true)
+    return SimpleTypeImpl(
+        ClassReferenceImpl(abiQualifiedName),
+        arguments = typeArgs,
+        nullability = nullability
+    )
+}
+
+internal fun Cursor.parseTypeArgs(): List<AbiTypeArgument>? {
+    val typeArgsString = parseTypeParamsString() ?: return null
+    val subCursor = Cursor(typeArgsString)
+    subCursor.parseSymbol("<") ?: return null
+    val typeArgs = mutableListOf<AbiTypeArgument>()
+    while (subCursor.parseTypeArg(peek = true) != null) {
+        typeArgs.add(subCursor.parseTypeArg()!!)
+        subCursor.parseSymbol(",")
+    }
+    return typeArgs
+}
+
+internal fun Cursor.parseTypeArg(peek: Boolean = false): AbiTypeArgument? {
+    val cursor = subCursor(peek)
+    val variance = cursor.parseAbiVariance()
+    cursor.parseSymbol("\\*")?.let {
+        return StarProjectionImpl
+    }
+    val type = cursor.parseAbiType(peek) ?: return null
+    return TypeProjectionImpl(
+        type = type,
+        variance = variance
+    )
+}
+
+internal fun Cursor.parseAbiVariance(): AbiVariance {
+    val variance = parseSymbol("^(out|in)") ?: return AbiVariance.INVARIANT
+    return AbiVariance.valueOf(variance.uppercase())
+}
+
+internal fun Cursor.parseTypeReference(): AbiType? {
+    val typeParamReference = parseTag() ?: return null
+    val typeArgs = parseTypeArgs() ?: emptyList()
+    val nullability = parseNullability()
+    return SimpleTypeImpl(
+        TypeParameterReferenceImpl(typeParamReference),
+        arguments = typeArgs,
+        nullability = nullability
+    )
+}
+
+internal fun Cursor.parseTag() = parseSymbol("^#[a-zA-Z0-9]+")?.removePrefix("#")
+
+internal fun Cursor.parseNullability(assumeNotNull: Boolean = false): AbiTypeNullability {
+    val nullable = parseSymbol("^\\?") != null
+    val definitelyNotNull = parseSymbol("^\\!\\!") != null
+    return when {
+        nullable -> AbiTypeNullability.MARKED_NULLABLE
+        definitelyNotNull -> AbiTypeNullability.DEFINITELY_NOT_NULL
+        else -> if (assumeNotNull) {
+            AbiTypeNullability.DEFINITELY_NOT_NULL
+        } else {
+            AbiTypeNullability.NOT_SPECIFIED
+        }
+    }
+}
+
+internal fun Cursor.parseSuperTypes(): MutableSet<AbiType> {
+    parseSymbol(":")
+    val superTypes = mutableSetOf<AbiType>()
+    while (parseAbiQualifiedName(peek = true) != null) {
+        superTypes.add(parseAbiType()!!)
+        parseSymbol(",")
+    }
+    return superTypes
+}
+
+fun Cursor.parseTypeParams(peek: Boolean = false): List<AbiTypeParameter>? {
+    val typeParamsString = parseTypeParamsString(peek) ?: return null
+    val subCursor = Cursor(typeParamsString)
+    subCursor.parseSymbol("^<")
+    val typeParams = mutableListOf<AbiTypeParameter>()
+    while (subCursor.parseTypeParam(peek = true) != null) {
+        typeParams.add(subCursor.parseTypeParam()!!)
+        subCursor.parseSymbol("^,")
+    }
+    return typeParams
+}
+
+fun Cursor.parseTypeParam(peek: Boolean = false): AbiTypeParameter? {
+    val cursor = subCursor(peek)
+    val tag = cursor.parseTag() ?: return null
+    cursor.parseSymbol("^:")
+    val variance = cursor.parseAbiVariance()
+    val isReified = cursor.parseSymbol("reified") != null
+    val upperBounds = mutableListOf<AbiType>()
+    if (null != cursor.parseAbiType(peek = true)) {
+        upperBounds.add(cursor.parseAbiType()!!)
+    }
+
+    return AbiTypeParameterImpl(
+        tag = tag,
+        variance = variance,
+        isReified = isReified,
+        upperBounds = upperBounds
+    )
+}
+
+internal fun Cursor.parseValueParameters(): List<AbiValueParameter>? {
+    val valueParamString = parseValueParametersString() ?: return null
+    val subCursor = Cursor(valueParamString)
+    val valueParams = mutableListOf<AbiValueParameter>()
+    subCursor.parseSymbol("\\(")
+    while (null != subCursor.parseValueParameter(peek = true)) {
+        valueParams.add(subCursor.parseValueParameter()!!)
+        subCursor.parseSymbol("^,")
+    }
+    return valueParams
+}
+
+internal fun Cursor.parseValueParameter(peek: Boolean = false): AbiValueParameter? {
+    val cursor = subCursor(peek)
+    val modifiers = cursor.parseValueParameterModifiers()
+    val isNoInline = modifiers.contains("noinline")
+    val isCrossinline = modifiers.contains("crossinline")
+    val type = cursor.parseAbiType() ?: return null
+    val isVararg = cursor.parseVarargSymbol() != null
+    val hasDefaultArg = cursor.parseDefaultArg() != null
+    return AbiValueParameterImpl(
+        type = type,
+        isVararg = isVararg,
+        hasDefaultArg = hasDefaultArg,
+        isNoinline = isNoInline,
+        isCrossinline = isCrossinline
+    )
+}
+
+internal fun Cursor.parseValueParameterModifiers(): Set<String> {
+    val modifiers = mutableSetOf<String>()
+    while (parseValueParameterModifier(peek = true) != null) {
+        modifiers.add(parseValueParameterModifier()!!)
+    }
+    return modifiers
+}
+
+internal fun Cursor.parseValueParameterModifier(peek: Boolean = false): String? =
+    parseSymbol("^(crossinline|noinline)", peek)
+
+internal fun Cursor.parseVarargSymbol() = parseSymbol("^\\.\\.\\.")
+
+internal fun Cursor.parseDefaultArg() = parseSymbol("^=\\.\\.\\.")
+
+internal fun Cursor.parseFunctionReceiver(): AbiType? {
+    val string = parseFunctionReceiverString() ?: return null
+    val subCursor = Cursor(string)
+    subCursor.parseSymbol("\\(")
+    return subCursor.parseAbiType()
+}
+
+internal fun Cursor.parseReturnType(): AbiType? {
+    parseSymbol("^:\\s")
+    return parseAbiType()
+}
+
+internal fun Cursor.parseTargets(): List<String> {
+    parseSymbol("^Targets:")
+    parseSymbol("^\\[")
+    val targets = mutableListOf<String>()
+    while (parseValidIdentifier(peek = true) != null) {
+        targets.add(parseValidIdentifier()!!)
+        parseSymbol("^,")
+    }
+    parseSymbol("^\\]")
+    return targets
+}
+
+/**
+ * Used to check if declarations after a property are getter / setter methods which should be
+ * attached to that property.
+*/
+private fun Cursor.hasPropertyAccessor(type: GetterOrSetter): Boolean {
+    val subCursor = copy()
+    subCursor.parseAbiModality()
+    subCursor.parseFunctionModifiers()
+    subCursor.parseFunctionKind() ?: return false // if it's not a function it's not a getter/setter
+    val mightHaveTypeParams = subCursor.parseGetterOrSetterName(peek = true) == null
+    if (mightHaveTypeParams) {
+        subCursor.parseTypeParams()
+    }
+    subCursor.parseFunctionReceiver()
+    return when (type) {
+        GetterOrSetter.GETTER -> subCursor.parseGetterName() != null
+        GetterOrSetter.SETTER -> subCursor.parseSetterName() != null
+    }
+}
+
+private fun Cursor.subCursor(peek: Boolean) = if (peek) { copy() } else { this }
+
+private fun Cursor.parseTypeParamsString(peek: Boolean = false): String? {
+    val cursor = subCursor(peek)
+    val result = StringBuilder()
+    cursor.parseSymbol("^<")?.let { result.append(it) } ?: return null
+    var openBracketCount = 1
+    while (openBracketCount > 0) {
+        val nextSymbol = cursor.parseSymbol(".", skipInlineWhitespace = false).also {
+            result.append(it)
+        }
+        when (nextSymbol) {
+            "<" -> openBracketCount++
+            ">" -> openBracketCount--
+        }
+    }
+    cursor.skipInlineWhitespace()
+    return result.toString()
+}
+
+private fun Cursor.parseFunctionReceiverString() =
+    parseSymbol("^\\([a-zA-Z0-9,\\/<>,#\\.\\s]+?\\)\\.")
+
+private fun Cursor.parseValueParametersString() =
+    parseSymbol("^\\(([a-zA-Z0-9,\\/<>,#\\.\\s\\?=]+)?\\)")
+
+private fun Cursor.parseAbiModalityString(peek: Boolean = false) =
+    parseSymbol("^(final|open|abstract|sealed)", peek)?.uppercase()
+
+private fun Cursor.parsePropertyKindString(peek: Boolean = false) =
+    parseSymbol("^(const\\sval|val|var)", peek)?.uppercase()?.replace(" ", "_")
+
+private fun Cursor.parseClassKindString(peek: Boolean = false) =
+    parseSymbol("^(class|interface|object|enum_class|annotation_class)", peek)?.uppercase()
+
+private enum class GetterOrSetter() {
+    GETTER,
+    SETTER
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
new file mode 100644
index 0000000..6390ea9
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
@@ -0,0 +1,509 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+import org.jetbrains.kotlin.library.abi.AbiClassKind
+import org.jetbrains.kotlin.library.abi.AbiModality
+import org.jetbrains.kotlin.library.abi.AbiPropertyKind
+import org.jetbrains.kotlin.library.abi.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiValueParameter
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.junit.Test
+
+class CursorExtensionsTest {
+
+    @Test
+    fun parseModalityFailure() {
+        val input = "something else"
+        val cursor = Cursor(input)
+        val modality = cursor.parseAbiModality()
+        assertThat(modality).isNull()
+        assertThat(cursor.currentLine).isEqualTo("something else")
+    }
+
+    @Test
+    fun parseModalitySuccess() {
+        val input = "final whatever"
+        val cursor = Cursor(input)
+        val modality = cursor.parseAbiModality()
+        assertThat(modality).isEqualTo(AbiModality.FINAL)
+        assertThat(cursor.currentLine).isEqualTo("whatever")
+    }
+
+    @Test
+    fun parseClassModifier() {
+        val input = "inner whatever"
+        val cursor = Cursor(input)
+        val modifier = cursor.parseClassModifier()
+        assertThat(modifier).isEqualTo("inner")
+        assertThat(cursor.currentLine).isEqualTo("whatever")
+    }
+
+    @Test
+    fun parseClassModifiers() {
+        val input = "inner value fun whatever"
+        val cursor = Cursor(input)
+        val modifiers = cursor.parseClassModifiers()
+        assertThat(modifiers).containsExactly("inner", "fun", "value")
+        assertThat(cursor.currentLine).isEqualTo("whatever")
+    }
+
+    @Test
+    fun parseFunctionModifiers() {
+        val input = "final inline suspend fun component1(): kotlin/Long"
+        val cursor = Cursor(input)
+        cursor.parseAbiModality()
+        val modifiers = cursor.parseFunctionModifiers()
+        assertThat(modifiers).containsExactly("inline", "suspend")
+        assertThat(cursor.currentLine).isEqualTo("fun component1(): kotlin/Long")
+    }
+
+    @Test
+    fun parseClassKindSimple() {
+        val input = "class"
+        val cursor = Cursor(input)
+        val kind = cursor.parseClassKind()
+        assertThat(kind).isEqualTo(AbiClassKind.CLASS)
+    }
+
+    @Test
+    fun parseClassKindFalsePositive() {
+        val input = "androidx.collection/objectFloatMap"
+        val cursor = Cursor(input)
+        val kind = cursor.parseClassKind()
+        assertThat(kind).isNull()
+    }
+
+    @Test
+    fun hasClassKind() {
+        val input = "final class my.lib/MyClass"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasClassKind()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test
+    fun parseFunctionKindSimple() {
+        val input = "fun hello"
+        val cursor = Cursor(input)
+        val kind = cursor.parseFunctionKind()
+        assertThat(kind).isEqualTo("fun")
+        assertThat(cursor.currentLine).isEqualTo(cursor.currentLine)
+    }
+
+    @Test fun hasFunctionKind() {
+        val input = "    final fun myFun(): kotlin/String "
+        val cursor = Cursor(input)
+        assertThat(cursor.hasFunctionKind()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test fun hasFunctionKindConstructor() {
+        val input = "    constructor <init>(kotlin/Int =...)"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasFunctionKind()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test fun parseGetterOrSetterName() {
+        val input = "<get-indices>()"
+        val cursor = Cursor(input)
+        val name = cursor.parseGetterOrSetterName()
+        assertThat(name).isEqualTo("<get-indices>")
+        assertThat(cursor.currentLine).isEqualTo("()")
+    }
+
+    @Test fun hasGetter() {
+        val input = "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasGetter()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test fun hasSetter() {
+        val input = "final inline fun <set-indices>(): kotlin.ranges/IntRange"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasSetter()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test fun hasGetterOrSetter() {
+        val inputs = listOf(
+            "final inline fun <set-indices>(): kotlin.ranges/IntRange",
+            "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+        )
+        inputs.forEach { input ->
+            assertThat(Cursor(input).hasGetterOrSetter()).isTrue()
+        }
+    }
+
+    @Test
+    fun hasPropertyKind() {
+        val input = "final const val my.lib/myProp"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasPropertyKind()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test
+    fun parsePropertyKindConstVal() {
+        val input = "const val something"
+        val cursor = Cursor(input)
+        val kind = cursor.parsePropertyKind()
+        assertThat(kind).isEqualTo(AbiPropertyKind.CONST_VAL)
+        assertThat(cursor.currentLine).isEqualTo("something")
+    }
+
+    @Test
+    fun parsePropertyKindVal() {
+        val input = "val something"
+        val cursor = Cursor(input)
+        val kind = cursor.parsePropertyKind()
+        assertThat(kind).isEqualTo(AbiPropertyKind.VAL)
+        assertThat(cursor.currentLine).isEqualTo("something")
+    }
+
+    @Test
+    fun parseNullability() {
+        val nullable = Cursor("?").parseNullability()
+        val notNull = Cursor("!!").parseNullability()
+        val unspecified = Cursor("another symbol").parseNullability()
+        assertThat(nullable).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+        assertThat(notNull).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+        assertThat(unspecified).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+    }
+
+    @Test fun parseNullabilityWhenAssumingNotNullable() {
+        val unspecified = Cursor("").parseNullability(assumeNotNull = true)
+        assertThat(unspecified).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+    }
+
+    @Test fun parseQualifiedName() {
+        val input = "androidx.collection/MutableScatterMap something"
+        val cursor = Cursor(input)
+        val qName = cursor.parseAbiQualifiedName()
+        assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+        assertThat(cursor.currentLine).isEqualTo("something")
+    }
+
+    @Test fun parseQualifiedNameKotlin() {
+        val input = "kotlin/Function2<#A1, #A, #A1>"
+        val cursor = Cursor(input)
+        val qName = cursor.parseAbiQualifiedName()
+        assertThat(qName.toString()).isEqualTo("kotlin/Function2")
+        assertThat(cursor.currentLine).isEqualTo("<#A1, #A, #A1>",)
+    }
+
+    @Test fun parseQualifie0dNameDoesNotGrabNullable() {
+        val input = "androidx.collection/MutableScatterMap? something"
+        val cursor = Cursor(input)
+        val qName = cursor.parseAbiQualifiedName()
+        assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+        assertThat(cursor.currentLine).isEqualTo("? something")
+    }
+
+    @Test
+    fun parseAbiType() {
+        val input = "androidx.collection/ScatterMap<#A, #B> something"
+        val cursor = Cursor(input)
+        val type = cursor.parseAbiType()
+        assertThat(type?.className?.toString()).isEqualTo(
+            "androidx.collection/ScatterMap"
+        )
+        assertThat(cursor.currentLine).isEqualTo("something")
+    }
+
+    @Test
+    fun parseAbiTypeWithAnotherType() {
+        val input = "androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+            "something"
+        val cursor = Cursor(input)
+        val type = cursor.parseAbiType()
+        assertThat(type?.className?.toString()).isEqualTo(
+            "androidx.collection/ScatterMap"
+        )
+        assertThat(cursor.currentLine).isEqualTo(
+            ", androidx.collection/Other<#A, #B> something"
+        )
+    }
+
+    @Test fun parseAbiTypeWithThreeParams() {
+        val input = "kotlin/Function2<#A1, #A, #A1>"
+        val cursor = Cursor(input)
+        val type = cursor.parseAbiType()
+        assertThat(type?.className?.toString()).isEqualTo("kotlin/Function2")
+    }
+
+    @Test
+    fun parseSuperTypes() {
+        val input = ": androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+            "something"
+        val cursor = Cursor(input)
+        val superTypes = cursor.parseSuperTypes().toList()
+        assertThat(superTypes).hasSize(2)
+        assertThat(superTypes.first().className?.toString()).isEqualTo(
+            "androidx.collection/ScatterMap"
+        )
+        assertThat(superTypes.last().className?.toString()).isEqualTo(
+            "androidx.collection/Other"
+        )
+        assertThat(cursor.currentLine).isEqualTo("something")
+    }
+
+    @Test fun parseReturnType() {
+        val input = ": androidx.collection/ScatterMap<#A, #B> stuff"
+        val cursor = Cursor(input)
+        val returnType = cursor.parseReturnType()
+        assertThat(returnType?.className?.toString()).isEqualTo(
+            "androidx.collection/ScatterMap"
+        )
+        assertThat(cursor.currentLine).isEqualTo("stuff")
+    }
+
+    @Test fun parseReturnTypeNullableWithTypeParamsNullable() {
+        val input = ": #B? stuff"
+        val cursor = Cursor(input)
+        val returnType = cursor.parseReturnType()
+        assertThat(returnType?.tag).isEqualTo("B")
+        assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+        assertThat(cursor.currentLine).isEqualTo("stuff")
+    }
+
+    @Test fun parseReturnTypeNullableWithTypeParamsNotSpecified() {
+        val input = ": #B stuff"
+        val cursor = Cursor(input)
+        val returnType = cursor.parseReturnType()
+        assertThat(returnType?.tag).isEqualTo("B")
+        assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+        assertThat(cursor.currentLine).isEqualTo("stuff")
+    }
+
+    @Test
+    fun parseFunctionReceiver() {
+        val input = "(androidx.collection/LongSparseArray<#A>).androidx.collection/keyIterator()"
+        val cursor = Cursor(input)
+        val receiver = cursor.parseFunctionReceiver()
+        assertThat(receiver?.className.toString()).isEqualTo(
+            "androidx.collection/LongSparseArray"
+        )
+        assertThat(cursor.currentLine).isEqualTo("androidx.collection/keyIterator()")
+    }
+
+    @Test
+    fun parseFunctionReceiver2() {
+        val input = "(androidx.collection/LongSparseArray<#A1>).<get-size>(): kotlin/Int"
+        val cursor = Cursor(input)
+        val receiver = cursor.parseFunctionReceiver()
+        assertThat(receiver?.className.toString()).isEqualTo(
+            "androidx.collection/LongSparseArray"
+        )
+        assertThat(cursor.currentLine).isEqualTo("<get-size>(): kotlin/Int")
+    }
+
+    @Test fun parseValueParamCrossinlineDefault() {
+        val input = "crossinline kotlin/Function2<#A, #B, kotlin/Int> =..."
+        val cursor = Cursor(input)
+        val valueParam = cursor.parseValueParameter()!!
+        assertThat(
+            valueParam.type.className.toString()
+        ).isEqualTo("kotlin/Function2")
+        assertThat(valueParam.hasDefaultArg).isTrue()
+        assertThat(valueParam.isCrossinline).isTrue()
+        assertThat(valueParam.isVararg).isFalse()
+    }
+
+    @Test
+    fun parseValueParamVararg() {
+        val input = "kotlin/Array<out kotlin/Pair<#A, #B>>..."
+        val cursor = Cursor(input)
+        val valueParam = cursor.parseValueParameter()
+        assertThat(
+            valueParam?.type?.className?.toString()
+        ).isEqualTo("kotlin/Array")
+        assertThat(valueParam?.hasDefaultArg).isFalse()
+        assertThat(valueParam?.isCrossinline).isFalse()
+        assertThat(valueParam?.isVararg).isTrue()
+    }
+
+    @Test fun parseValueParametersWithTypeArgs() {
+        val input = "kotlin/Array<out #A>..."
+        val cursor = Cursor(input)
+        val valueParam = cursor.parseValueParameter()
+        assertThat(valueParam?.type?.arguments).hasSize(1)
+    }
+
+    @Test fun parseValueParametersWithTwoTypeArgs() {
+        val input = "kotlin/Function1<kotlin/Double, kotlin/Boolean>)"
+        val cursor = Cursor(input)
+        val valueParam = cursor.parseValueParameter()
+        assertThat(valueParam?.type?.arguments).hasSize(2)
+    }
+
+    @Test fun parseValueParametersEmpty() {
+        val input = "() thing"
+        val cursor = Cursor(input)
+        val params = cursor.parseValueParameters()
+        assertThat(params).isEqualTo(emptyList<AbiValueParameter>())
+        assertThat(cursor.currentLine).isEqualTo("thing")
+    }
+
+    @Test fun parseValueParamsSimple() {
+        val input = "(kotlin/Function1<#A, kotlin/Boolean>)"
+        val cursor = Cursor(input)
+        val valueParams = cursor.parseValueParameters()
+        assertThat(valueParams).hasSize(1)
+    }
+
+    @Test fun parseValueParamsTwoArgs() {
+        val input = "(#A1, kotlin/Function2<#A1, #A, #A1>)"
+        val cursor = Cursor(input)
+        val valueParams = cursor.parseValueParameters()
+        assertThat(valueParams).hasSize(2)
+        assertThat(valueParams?.first()?.type?.tag).isEqualTo("A1")
+    }
+
+    @Test
+    fun parseValueParamsWithHasDefaultArg() {
+        val input = "(kotlin/Int =...)"
+        val cursor = Cursor(input)
+        val valueParams = cursor.parseValueParameters()
+        assertThat(valueParams).hasSize(1)
+        assertThat(valueParams?.single()?.hasDefaultArg).isTrue()
+    }
+
+    @Test
+    fun parseValueParamsComplex2() {
+        val input = "(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> =..., " +
+            "crossinline kotlin/Function1<#A, #B?> =..., " +
+            "crossinline kotlin/Function4<kotlin/Boolean, #A, #B, #B?, kotlin/Unit> =...)"
+        val cursor = Cursor(input)
+        val valueParams = cursor.parseValueParameters()!!
+        assertThat(valueParams).hasSize(4)
+        assertThat(valueParams.first().type.className?.toString()).isEqualTo("kotlin/Int")
+        val rest = valueParams.subList(1, valueParams.size)
+        assertThat(rest).hasSize(3)
+        assertThat(rest.all { it.hasDefaultArg }).isTrue()
+        assertThat(rest.all { it.isCrossinline }).isTrue()
+    }
+
+    @Test fun parseValueParamsComplex3() {
+        val input = "(kotlin/Array<out kotlin/Pair<#A, #B>>...)"
+        val cursor = Cursor(input)
+        val valueParams = cursor.parseValueParameters()!!
+        assertThat(valueParams).hasSize(1)
+
+        assertThat(valueParams.single().isVararg).isTrue()
+        val type = valueParams.single().type
+        assertThat(type.className.toString()).isEqualTo("kotlin/Array")
+    }
+
+    @Test fun parseTypeParams() {
+        val input = "<#A1: kotlin/Any?>"
+        val cursor = Cursor(input)
+        val typeParams = cursor.parseTypeParams()
+        assertThat(typeParams).hasSize(1)
+        val type = typeParams?.single()?.upperBounds?.single()
+        assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+        assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+        assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+        assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.INVARIANT)
+    }
+
+    @Test fun parseTypeParamsWithVariance() {
+        val input = "<#A1: out kotlin/Any?>"
+        val cursor = Cursor(input)
+        val typeParams = cursor.parseTypeParams()
+        assertThat(typeParams).hasSize(1)
+        val type = typeParams?.single()?.upperBounds?.single()
+        assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+        assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+        assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+        assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.OUT)
+    }
+
+    @Test fun parseTypeParamsWithTwo() {
+        val input = "<#A: kotlin/Any?, #B: kotlin/Any?>"
+        val cursor = Cursor(input)
+        val typeParams = cursor.parseTypeParams()
+        assertThat(typeParams).hasSize(2)
+        val type1 = typeParams?.first()?.upperBounds?.single()
+        val type2 = typeParams?.first()?.upperBounds?.single()
+        assertThat(typeParams?.first()?.tag).isEqualTo("A")
+        assertThat(typeParams?.last()?.tag).isEqualTo("B")
+        assertThat(type1?.className?.toString()).isEqualTo("kotlin/Any")
+        assertThat(type1?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+        assertThat(type2?.className?.toString()).isEqualTo("kotlin/Any")
+        assertThat(type2?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+    }
+
+    @Test fun parseTypeParamsReifed() {
+        val input = "<#A1: reified kotlin/Any?>"
+        val cursor = Cursor(input)
+        val typeParam = cursor.parseTypeParams()?.single()
+        assertThat(typeParam).isNotNull()
+        assertThat(typeParam?.isReified).isTrue()
+    }
+
+    @Test
+    fun parseTypeArgs() {
+        val input = "<out #A>"
+        val cursor = Cursor(input)
+        val typeArgs = cursor.parseTypeArgs()
+        assertThat(typeArgs).hasSize(1)
+        val typeArg = typeArgs?.single()
+        assertThat(typeArg?.type?.tag).isEqualTo("A")
+        assertThat(typeArg?.variance).isEqualTo(AbiVariance.OUT)
+    }
+
+    @Test
+    fun parseTwoTypeArgs() {
+        val input = "<kotlin/Double, kotlin/Boolean>"
+        val cursor = Cursor(input)
+        val typeArgs = cursor.parseTypeArgs()
+        assertThat(typeArgs).hasSize(2)
+        assertThat(typeArgs?.first()?.type?.className?.toString()).isEqualTo("kotlin/Double")
+        assertThat(typeArgs?.last()?.type?.className?.toString()).isEqualTo("kotlin/Boolean")
+    }
+
+    @Test
+    fun parseTypeArgsWithNestedBrackets() {
+        val input = "<androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B>>," +
+            " something else"
+        val cursor = Cursor(input)
+        val typeArgs = cursor.parseTypeArgs()
+        assertThat(typeArgs).hasSize(2)
+        assertThat(cursor.currentLine).isEqualTo(", something else")
+    }
+
+    @Test fun parseVarargSymbol() {
+        val input = "..."
+        val cursor = Cursor(input)
+        val vararg = cursor.parseVarargSymbol()
+        assertThat(vararg).isNotNull()
+    }
+
+    @Test fun parseTargets() {
+        val input = "Targets: [iosX64, linuxX64]"
+        val cursor = Cursor(input)
+        val targets = cursor.parseTargets()
+        assertThat(targets).containsExactly("linuxX64", "iosX64")
+    }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
new file mode 100644
index 0000000..f37f6c5
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 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.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CursorTest {
+
+    @Test
+    fun cursorShowsCurrentLine() {
+        val input = "one\ntwo\nthree"
+        val cursor = Cursor(input)
+        assertThat(cursor.currentLine).isEqualTo("one")
+        cursor.nextLine()
+        assertThat(cursor.currentLine).isEqualTo("two")
+        cursor.nextLine()
+        assertThat(cursor.currentLine).isEqualTo("three")
+        assertThat(cursor.hasNext()).isFalse()
+    }
+
+    @Test
+    fun cursorGetsNextWord() {
+        val input = "one two three"
+        val cursor = Cursor(input)
+        val word = cursor.parseWord()
+        assertThat(word).isEqualTo("one")
+        assertThat("two three").isEqualTo(cursor.currentLine)
+    }
+
+    @Test
+    fun parseValidIdentifierValid() {
+        val input = "oneTwo3 four"
+        val cursor = Cursor(input)
+        val symbol = cursor.parseValidIdentifier()
+        assertThat(symbol).isEqualTo("oneTwo3")
+        assertThat("four").isEqualTo(cursor.currentLine)
+    }
+
+    @Test
+    fun parseValidIdentifierValidStartsWithUnderscore() {
+        val input = "_one_Two3 four"
+        val cursor = Cursor(input)
+        val symbol = cursor.parseValidIdentifier()
+        assertThat(symbol).isEqualTo("_one_Two3")
+        assertThat("four").isEqualTo(cursor.currentLine)
+    }
+
+    @Test
+    fun parseValidIdentifierInvalid() {
+        val input = "1twothree"
+        val cursor = Cursor(input)
+        val symbol = cursor.parseValidIdentifier()
+        assertThat(symbol).isNull()
+    }
+
+    @Test
+    fun skipWhitespace() {
+        val input = "    test"
+        val cursor = Cursor(input)
+        cursor.skipInlineWhitespace()
+        assertThat(cursor.currentLine).isEqualTo("test")
+    }
+
+    @Test
+    fun skipWhitespaceOnBlankLine() {
+        val input = ""
+        val cursor = Cursor(input)
+        cursor.skipInlineWhitespace()
+        assertThat(cursor.currentLine).isEqualTo("")
+    }
+}
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 5b3af52..4b77b51 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -57,7 +57,7 @@
     implementation(project(":benchmark:benchmark-gradle-plugin"))
     implementation(project(":inspection:inspection-gradle-plugin"))
     implementation(project(":stableaidl:stableaidl-gradle-plugin"))
-    implementation(findGradleKotlinDsl())
+    implementation(project.ext.findGradleKotlinDsl())
     testImplementation(libs.junit)
     testImplementation(libs.truth)
     testImplementation(project(":internal-testutils-gradle-plugin"))
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 46f896e..02be011e 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -300,155 +300,11 @@
 
     <issue
         id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="                    rootProject.findProperty(ENABLE_ARG) != &quot;false&quot;"
-        errorLine2="                                ~~~~~~~~~~~~">
+        message="Use providers.gradleProperty instead of property"
+        errorLine1="            (this.rootProject.property(&quot;ext&quot;) as ExtraPropertiesExtension).set("
+        errorLine2="                              ~~~~~~~~">
         <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="                    rootProject.findProperty(ENABLE_ARG) != &quot;false&quot;"
-        errorLine2="                                ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
-        errorLine2="                                                          ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
-        errorLine2="                                                          ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="    override val compileSdk: Int by lazy { project.findProperty(COMPILE_SDK).toString().toInt() }"
-        errorLine2="                                                   ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="        project.findProperty(COMPILE_SDK_EXTENSION) as Int?"
-        errorLine2="                ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="        project.findProperty(TARGET_SDK_VERSION).toString().toInt()"
-        errorLine2="                ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="    project.findProperty(ALTERNATIVE_PROJECT_URL) as? String"
-        errorLine2="            ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="    return (project.findProperty(ENABLE_DOCUMENTATION) as? String)?.toBoolean() ?: true"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()"
-        errorLine2="                                                     ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="                return checkNotNull(findProperty(name)) {"
-        errorLine2="                                    ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="        parseTargetPlatformsFlag(project.findProperty(ENABLED_KMP_TARGET_PLATFORMS) as? String)"
-        errorLine2="                                         ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/KmpPlatforms.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            findProperty(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true"
-        errorLine2="            ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            val value = project.findProperty(STUDIO_TYPE)?.toString()"
-        errorLine2="                                ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            val value = project.findProperty(STUDIO_TYPE)?.toString()"
-        errorLine2="                                ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="        val group = findProperty(&quot;group&quot;) as String"
-        errorLine2="                    ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="        val artifactId = findProperty(&quot;name&quot;) as String"
-        errorLine2="                         ~~~~~~~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
     </issue>
 
     <issue
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 51df347..120a7bb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -26,7 +26,6 @@
 import org.gradle.api.Project
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition
 import org.gradle.api.attributes.Attribute
-import org.gradle.api.plugins.ExtraPropertiesExtension
 import org.gradle.api.tasks.bundling.Zip
 import org.gradle.kotlin.dsl.create
 import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
@@ -122,7 +121,7 @@
          */
         private fun Project.configureForMultiplatform() {
             // This is to allow K/N not matching the kotlinVersion
-            (this.rootProject.property("ext") as ExtraPropertiesExtension).set(
+            this.rootProject.extensions.extraProperties.set(
                 "kotlin.native.version",
                 KOTLIN_NATIVE_VERSION
             )
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 69dde1f..aeab798 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -221,7 +221,7 @@
  * Returns null if there is no alternative project url.
  */
 fun Project.getAlternativeProjectUrl(): String? =
-    project.findProperty(ALTERNATIVE_PROJECT_URL) as? String
+    project.providers.gradleProperty(ALTERNATIVE_PROJECT_URL).getOrNull()
 
 /**
  * Check that version extra meets the specified rules (version is in format major.minor.patch-extra)
@@ -277,18 +277,9 @@
 fun Project.isWriteVersionedApiFilesEnabled(): Boolean =
     findBooleanProperty(WRITE_VERSIONED_API_FILES) ?: true
 
-/** Returns whether the project should generate documentation. */
-fun Project.isDocumentationEnabled(): Boolean {
-    if (System.getenv().containsKey("ANDROIDX_PROJECTS")) {
-        val projects = System.getenv()["ANDROIDX_PROJECTS"] as String
-        if (projects != "ALL") return false
-    }
-    return (project.findProperty(ENABLE_DOCUMENTATION) as? String)?.toBoolean() ?: true
-}
-
 /** Returns whether the build is for checking forward compatibility across projects */
 fun Project.usingMaxDepVersions(): Boolean {
-    return project.hasProperty(USE_MAX_DEP_VERSIONS)
+    return project.providers.gradleProperty(USE_MAX_DEP_VERSIONS).isPresent()
 }
 
 /**
@@ -320,7 +311,7 @@
 fun Project.isCustomCompileSdkAllowed(): Boolean =
     findBooleanProperty(ALLOW_CUSTOM_COMPILE_SDK) ?: true
 
-fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()
+fun Project.findBooleanProperty(propName: String) = booleanPropertyProvider(propName).get()
 
 fun Project.booleanPropertyProvider(propName: String): Provider<Boolean> {
     return project.providers.gradleProperty(propName).map { s -> s.toBoolean() }.orElse(false)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
index 4503c7b..4bb9027 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import androidx.build.gradle.extraPropertyOrNull
 import androidx.build.gradle.isRoot
 import groovy.xml.DOMBuilder
 import java.net.URI
@@ -195,7 +196,7 @@
             }
 
             private fun Project.requireProperty(name: String): String {
-                return checkNotNull(findProperty(name)) {
+                return checkNotNull(extraPropertyOrNull(name)) {
                         "missing $name property. It must be defined in the gradle.properties file"
                     }
                     .toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt b/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
index 5759c76d..3d041e1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import androidx.build.gradle.extraPropertyOrNull
 import java.io.File
 import org.gradle.api.Project
 import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
@@ -83,7 +84,7 @@
 
     private fun Project.overrideKotlinNativeDependenciesUrlToLocalDirectory() {
         val compilerDaemonDisabled =
-            findProperty(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true
+            extraPropertyOrNull(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true
         val konanPrebuiltsFolder = getKonanPrebuiltsFolder()
         val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir
         // use relative path so it doesn't affect gradle remote cache.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
index 115050d..874ebf9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt
@@ -105,8 +105,8 @@
     androidXExtension: AndroidXExtension
 ) {
     writeVersionFile.configure {
-        val group = findProperty("group") as String
-        val artifactId = findProperty("name") as String
+        val group = project.getGroup() as String
+        val artifactId = project.getName() as String
         val version =
             if (androidXExtension.shouldPublish()) {
                 version().toString()
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 6307291..4dfd2de 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -110,9 +110,8 @@
             val instance = AffectedModuleDetectorWrapper()
             rootProject.extensions.add(ROOT_PROP_NAME, instance)
 
-            val enabled =
-                rootProject.hasProperty(ENABLE_ARG) &&
-                    rootProject.findProperty(ENABLE_ARG) != "false"
+            val enabledProvider = rootProject.providers.gradleProperty(ENABLE_ARG)
+            val enabled = enabledProvider.isPresent() && enabledProvider.get() != "false"
 
             val distDir = rootProject.getDistributionDirectory()
             val outputFile = distDir.resolve(LOG_FILE_NAME)
@@ -134,10 +133,9 @@
                 instance.wrapped = provider
                 return
             }
-            val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?
-            if (baseCommitOverride != null) {
-                logger.info("using base commit override $baseCommitOverride")
-            }
+            val baseCommitOverride: Provider<String> =
+                rootProject.providers.gradleProperty(BASE_COMMIT_ARG)
+
             gradle.taskGraph.whenReady {
                 logger.lifecycle("projects evaluated")
                 val projectGraph = ProjectGraph(rootProject)
@@ -257,7 +255,7 @@
         var cobuiltTestPaths: Set<Set<String>>?
         var alwaysBuildIfExists: Set<String>?
         var ignoredPaths: Set<String>?
-        var baseCommitOverride: String?
+        var baseCommitOverride: Provider<String>?
         var gitChangedFilesProvider: Provider<List<String>>
     }
 
@@ -266,10 +264,6 @@
         if (parameters.acceptAll) {
             AcceptAll(null)
         } else {
-            if (parameters.baseCommitOverride != null) {
-                logger.info("using base commit override ${parameters.baseCommitOverride}")
-            }
-
             AffectedModuleDetectorImpl(
                 projectGraph = parameters.projectGraph,
                 dependencyTracker = parameters.dependencyTracker,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index c73c126..b96c9d6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -27,6 +27,7 @@
 import androidx.build.getDistributionDirectory
 import androidx.build.getKeystore
 import androidx.build.getLibraryByName
+import androidx.build.getSupportRootFolder
 import androidx.build.metalava.versionMetadataUsage
 import androidx.build.multiplatformUsage
 import androidx.build.versionCatalog
@@ -521,7 +522,7 @@
                 var taskStartTime: LocalDateTime? = null
                 task.argsJsonFile.set(
                     File(
-                        project.rootProject.getDistributionDirectory(),
+                        project.getDistributionDirectory(),
                         "dackkaArgs-${project.name}.json"
                     )
                 )
@@ -542,7 +543,7 @@
                     dackkaClasspath.from(project.files(dackkaConfiguration))
                     destinationDir.set(generatedDocsDir)
                     frameworkSamplesDir.set(
-                        project.rootProject.layout.projectDirectory.dir("samples")
+                        File(project.getSupportRootFolder(), "samples")
                     )
                     samplesDeprecatedDir.set(unzippedDeprecatedSamplesSources)
                     samplesJvmDir.set(unzippedJvmSamplesSources)
@@ -550,7 +551,7 @@
                     jvmSourcesDir.set(unzippedJvmSourcesDirectory)
                     multiplatformSourcesDir.set(unzippedMultiplatformSourcesDirectory)
                     projectListsDirectory.set(
-                        project.rootProject.layout.projectDirectory.dir("docs-public/package-lists")
+                        File(project.getSupportRootFolder(), "docs-public/package-lists")
                     )
                     dependenciesClasspath.from(
                         dependencyClasspath +
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 fca8225..b2c1cf6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -38,12 +38,12 @@
  * @param baseCommitOverride optional value to use to override last merge commit
  */
 fun Project.getChangedFilesProvider(
-    baseCommitOverride: String?,
+    baseCommitOverride: Provider<String>,
 ): Provider<List<String>> {
     val changeInfoPath = System.getenv("CHANGE_INFO")
     val manifestPath = System.getenv("MANIFEST")
     return if (changeInfoPath != null && manifestPath != null) {
-        if (baseCommitOverride != null) throw GradleException(
+        if (baseCommitOverride.isPresent()) throw GradleException(
             "Overriding base commit is not supported when using CHANGE_INFO and MANIFEST"
         )
         getChangedFilesFromChangeInfoProvider(manifestPath, changeInfoPath)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
index e80fc19..1023659 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
@@ -31,7 +31,6 @@
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.internal.tasks.userinput.UserInputHandler
-import org.gradle.api.plugins.ExtraPropertiesExtension
 import org.gradle.api.tasks.Internal
 import org.gradle.api.tasks.TaskAction
 import org.gradle.internal.service.ServiceRegistry
@@ -339,7 +338,7 @@
 abstract class PlaygroundStudioTask : RootStudioTask() {
     @get:Internal
     val supportRootFolder =
-        (project.rootProject.property("ext") as ExtraPropertiesExtension).let {
+        (project.rootProject.extensions.extraProperties).let {
             it.get("supportRootFolder") as File
         }
 
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
index 685ccd6..f592083 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
@@ -18,6 +18,7 @@
 
 package androidx.build
 
+import androidx.build.gradle.extraPropertyOrNull
 import java.io.File
 import org.gradle.api.Project
 import org.gradle.api.file.FileCollection
@@ -26,13 +27,17 @@
 abstract class AndroidConfigImpl(private val project: Project) : AndroidConfig {
     override val buildToolsVersion: String = "35.0.0-rc1"
 
-    override val compileSdk: Int by lazy { project.findProperty(COMPILE_SDK).toString().toInt() }
+    override val compileSdk: Int by lazy {
+        val sdkString = project.extraPropertyOrNull(COMPILE_SDK)?.toString()
+        check(sdkString != null) { "$COMPILE_SDK is unset" }
+        sdkString.toInt()
+    }
 
     override val minSdk: Int = 21
     override val ndkVersion: String = "25.2.9519653"
 
     override val targetSdk: Int by lazy {
-        project.findProperty(TARGET_SDK_VERSION).toString().toInt()
+        project.providers.gradleProperty(TARGET_SDK_VERSION).get().toInt()
     }
 
     companion object {
@@ -95,7 +100,7 @@
 }
 
 fun Project.getPrebuiltsRoot(): File {
-    return File(project.rootProject.property("prebuiltsRoot").toString())
+    return File(project.extraPropertyOrNull("prebuiltsRoot").toString())
 }
 
 /** @return the project's Android SDK stub JAR as a File. */
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
index ab190af..74e1548 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import androidx.build.gradle.extraPropertyOrNull
 import java.util.Locale
 import org.gradle.api.Project
 import org.gradle.kotlin.dsl.create
@@ -116,7 +117,9 @@
 /** Extension used to store parsed KMP configuration information. */
 private open class KmpPlatformsExtension(project: Project) {
     val enabledKmpPlatforms =
-        parseTargetPlatformsFlag(project.findProperty(ENABLED_KMP_TARGET_PLATFORMS) as? String)
+        parseTargetPlatformsFlag(
+            project.extraPropertyOrNull(ENABLED_KMP_TARGET_PLATFORMS) as? String
+        )
 }
 
 fun Project.enableJs(): Boolean = enabledKmpPlatforms.contains(PlatformGroup.JS)
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt b/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
index ec0129f..aedc32f 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import androidx.build.gradle.extraPropertyOrNull
 import org.gradle.api.Project
 
 enum class ProjectLayoutType {
@@ -26,7 +27,7 @@
         /** Returns the project layout type for the project (PLAYGROUND or ANDROIDX) */
         @JvmStatic
         fun from(project: Project): ProjectLayoutType {
-            val value = project.findProperty(STUDIO_TYPE)?.toString()
+            val value = project.extraPropertyOrNull(STUDIO_TYPE)
             return when (value) {
                 "playground" -> ProjectLayoutType.PLAYGROUND
                 null,
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
index 9b9c338..2fa6bde 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
@@ -21,7 +21,6 @@
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.file.FileTree
-import org.gradle.api.plugins.ExtraPropertiesExtension
 
 /** Writes the appropriate SDK path to local.properties file in specified location. */
 fun Project.writeSdkPathToLocalPropertiesFile() {
@@ -50,7 +49,7 @@
 /** Returns the root project's platform-specific SDK path as a file. */
 fun Project.getSdkPath(): File {
     if (
-        rootProject.plugins.hasPlugin("AndroidXPlaygroundRootPlugin") ||
+        ProjectLayoutType.from(project) == ProjectLayoutType.PLAYGROUND ||
             System.getenv("COMPOSE_DESKTOP_GITHUB_BUILD") != null
     ) {
         // This is not full checkout, use local settings instead.
@@ -99,7 +98,7 @@
 
 /** Sets the path to the canonical root project directory, e.g. {@code frameworks/support}. */
 fun Project.setSupportRootFolder(rootDir: File?) {
-    val extension = project.property("ext") as ExtraPropertiesExtension
+    val extension = project.extensions.extraProperties
     return extension.set("supportRootFolder", rootDir)
 }
 
@@ -110,7 +109,7 @@
  * because it is generalized to also work for the "ui" project and playground projects.
  */
 fun Project.getSupportRootFolder(): File {
-    val extension = project.property("ext") as ExtraPropertiesExtension
+    val extension = project.extensions.extraProperties
     return extension.get("supportRootFolder") as File
 }
 
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt b/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
index 7d2ebd4..92bbc57 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt
@@ -20,3 +20,18 @@
 
 val Project.isRoot
     get() = this == rootProject
+
+/**
+ * Implements project.extensions.extraProperties.getOrNull(key)
+ * TODO(https://github.com/gradle/gradle/issues/28857) use simpler replacement when available
+ *
+ * Note that providers.gradleProperty() might return null in cases where this function can
+ * find a value: https://github.com/gradle/gradle/issues/23572
+ */
+fun Project.extraPropertyOrNull(key: String): Any? {
+    val container = project.extensions.extraProperties
+    var result: Any? = null
+    if (container.has(key))
+        result = container.get(key)
+    return result
+}
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
index ad474a8..cf4ffa0 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/ui/widget/Button.kt
@@ -46,7 +46,7 @@
     content: @Composable () -> Unit
 ) {
     CompositionLocalProvider(
-        LocalRippleConfiguration provides RippleConfiguration(isEnabled = enabled)
+        LocalRippleConfiguration provides if (enabled) RippleConfiguration() else null
     ) {
         FloatingActionButton(
             onClick = if (enabled) onClick else { {} },
diff --git a/car/app/app/gradle.properties b/car/app/app/gradle.properties
deleted file mode 100644
index a060082..0000000
--- a/car/app/app/gradle.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# Copyright 2023 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.
-#
-androidx.targetSdkVersion = 33
diff --git a/car/app/app/lint-baseline.xml b/car/app/app/lint-baseline.xml
index 16305d3..e10674d 100644
--- a/car/app/app/lint-baseline.xml
+++ b/car/app/app/lint-baseline.xml
@@ -362,6 +362,15 @@
     </issue>
 
     <issue
+        id="UnspecifiedRegisterReceiverFlag"
+        message="`mBroadcastReceiver` is missing `RECEIVER_EXPORTED` or `RECEIVER_NOT_EXPORTED` flag for unprotected broadcasts registered for androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"
+        errorLine1="            mContext.registerReceiver(mBroadcastReceiver, filter);"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java"/>
+    </issue>
+
+    <issue
         id="UnsafeOptInUsageError"
         message="This declaration is opt-in and its usage should be marked with `@androidx.car.app.annotations.ExperimentalCarApi` or `@OptIn(markerClass = androidx.car.app.annotations.ExperimentalCarApi.class)`"
         errorLine1="    private final List&lt;CarZone> mCarZones;"
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index e026507..f8dc027 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -1132,6 +1132,7 @@
 
   public static sealed interface LazyLayoutPrefetchState.PrefetchHandle {
     method public void cancel();
+    method public void markAsUrgent();
   }
 
   public final class Lazy_androidKt {
@@ -1156,8 +1157,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchRequestScope {
-    method public long getAvailableTimeNanos();
-    property public abstract long availableTimeNanos;
+    method public long availableTimeNanos();
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchScheduler {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index cc020da..8ffe7e9 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -1134,6 +1134,7 @@
 
   public static sealed interface LazyLayoutPrefetchState.PrefetchHandle {
     method public void cancel();
+    method public void markAsUrgent();
   }
 
   public final class Lazy_androidKt {
@@ -1158,8 +1159,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchRequestScope {
-    method public long getAvailableTimeNanos();
-    property public abstract long availableTimeNanos;
+    method public long availableTimeNanos();
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchScheduler {
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
index 087d8db..c4d5b3a 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -14,14 +14,20 @@
  * limitations under the License.
  */
 
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
@@ -58,6 +64,16 @@
     val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
     lateinit var state: LazyGridState
+    private val scheduler = TestPrefetchScheduler()
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Composable
+    fun rememberState(
+        initialFirstVisibleItemIndex: Int = 0,
+        initialFirstVisibleItemScrollOffset: Int = 0
+    ): LazyGridState = remember {
+        LazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, scheduler)
+    }
 
     @Test
     fun notPrefetchingForwardInitially() {
@@ -85,8 +101,8 @@
             }
         }
 
-        waitForPrefetch(4)
-        waitForPrefetch(5)
+        waitForPrefetch()
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertExists()
@@ -106,8 +122,8 @@
             }
         }
 
-        waitForPrefetch(2)
-        waitForPrefetch(3)
+        waitForPrefetch()
+        waitForPrefetch()
 
         rule.onNodeWithTag("2")
             .assertExists()
@@ -127,8 +143,8 @@
             }
         }
 
-        waitForPrefetch(6)
-        waitForPrefetch(7)
+        waitForPrefetch()
+        waitForPrefetch()
 
         rule.onNodeWithTag("6")
             .assertExists()
@@ -144,8 +160,8 @@
             }
         }
 
-        waitForPrefetch(0)
-        waitForPrefetch(1)
+        waitForPrefetch()
+        waitForPrefetch()
 
         rule.onNodeWithTag("0")
             .assertExists()
@@ -165,7 +181,7 @@
             }
         }
 
-        waitForPrefetch(4)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -174,7 +190,7 @@
             }
         }
 
-        waitForPrefetch(6)
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertIsDisplayed()
@@ -194,7 +210,7 @@
             }
         }
 
-        waitForPrefetch(4)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -203,7 +219,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertIsDisplayed()
@@ -225,8 +241,7 @@
             }
         }
 
-        waitForPrefetch(6)
-        waitForPrefetch(7)
+        waitForPrefetch()
 
         rule.onNodeWithTag("6")
             .assertExists()
@@ -244,8 +259,7 @@
             }
         }
 
-        waitForPrefetch(0)
-        waitForPrefetch(1)
+        waitForPrefetch()
 
         rule.onNodeWithTag("0")
             .assertExists()
@@ -283,7 +297,7 @@
             }
         }
 
-        waitForPrefetch(6)
+        waitForPrefetch()
 
         rule.onNodeWithTag("8")
             .assertExists()
@@ -296,7 +310,7 @@
             }
         }
 
-        waitForPrefetch(0)
+        waitForPrefetch()
 
         rule.onNodeWithTag("0")
             .assertExists()
@@ -316,7 +330,7 @@
             ) { constraints ->
                 val placeable = if (emit) {
                     subcompose(Unit) {
-                        state = rememberLazyGridState()
+                        state = rememberState()
                         LazyGrid(
                             2,
                             Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -355,7 +369,7 @@
     fun snappingToOtherPositionWhilePrefetchIsScheduled() {
         val composedItems = mutableListOf<Int>()
         rule.setContent {
-            state = rememberLazyGridState()
+            state = rememberState()
             LazyGrid(
                 1,
                 Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -410,7 +424,7 @@
             }
         }
 
-        waitForPrefetch(13)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking(AutoTestFrameClock()) {
@@ -424,14 +438,13 @@
         }
     }
 
-    private fun waitForPrefetch(index: Int) {
-        rule.waitUntil {
-            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
     private val activeNodes = mutableSetOf<Int>()
-    private val activeMeasuredNodes = mutableSetOf<Int>()
 
     private fun composeGrid(
         firstItem: Int = 0,
@@ -440,7 +453,7 @@
         contentPadding: PaddingValues = PaddingValues(0.dp)
     ) {
         rule.setContent {
-            state = rememberLazyGridState(
+            state = rememberState(
                 initialFirstVisibleItemIndex = firstItem,
                 initialFirstVisibleItemScrollOffset = itemOffset
             )
@@ -456,7 +469,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -465,7 +477,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index bb6d01a..5596f14 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -257,7 +257,8 @@
                     .then(modifier))
         }
         var needToCompose by mutableStateOf(false)
-        val prefetchState = LazyLayoutPrefetchState()
+        val scheduler = TestPrefetchScheduler()
+        val prefetchState = LazyLayoutPrefetchState(scheduler)
         rule.setContent {
             LazyLayout(itemProvider, prefetchState = prefetchState) {
                 val item = if (needToCompose) {
@@ -273,9 +274,10 @@
             assertThat(measureCount).isEqualTo(0)
 
             prefetchState.schedulePrefetch(0, constraints)
-        }
 
-        rule.waitUntil { measureCount == 1 }
+            scheduler.executeActiveRequests()
+            assertThat(measureCount).isEqualTo(1)
+        }
 
         rule.onNodeWithTag("0").assertIsNotDisplayed()
 
@@ -303,20 +305,18 @@
                 }
             }
         }
-        val prefetchState = LazyLayoutPrefetchState()
+        val scheduler = TestPrefetchScheduler()
+        val prefetchState = LazyLayoutPrefetchState(scheduler)
         rule.setContent {
             LazyLayout(itemProvider, prefetchState = prefetchState) {
                 layout(100, 100) {}
             }
         }
 
-        val handle = rule.runOnIdle {
-            prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
-        }
-
-        rule.waitUntil { composed }
-
         rule.runOnIdle {
+            val handle = prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
+            scheduler.executeActiveRequests()
+            assertThat(composed).isTrue()
             handle.cancel()
         }
 
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.kt
new file mode 100644
index 0000000..ad3b8a6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/layout/TestPrefetchScheduler.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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.foundation.ExperimentalFoundationApi
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TestPrefetchScheduler : PrefetchScheduler {
+
+    private var activeRequests = mutableListOf<PrefetchRequest>()
+    override fun schedulePrefetch(prefetchRequest: PrefetchRequest) {
+        activeRequests.add(prefetchRequest)
+    }
+
+    fun executeActiveRequests() {
+        activeRequests.forEach {
+            with(it) { scope.execute() }
+        }
+        activeRequests.clear()
+    }
+
+    private val scope = object : PrefetchRequestScope {
+        override fun availableTimeNanos(): Long = Long.MAX_VALUE
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
index b8a37d5..21f1531 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveCompositionCountTest.kt
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
 package androidx.compose.foundation.lazy.list
 
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.Modifier
@@ -45,6 +48,10 @@
 
     private val composedItems = mutableSetOf<Int>()
 
+    private val state = LazyListState().also {
+        it.prefetchingEnabled = false
+    }
+
     @Test
     fun moveFocus() {
         // Arrange.
@@ -52,7 +59,7 @@
         lateinit var focusManager: FocusManager
         rule.setContent {
             focusManager = LocalFocusManager.current
-            LazyRow(Modifier.size(rowSize)) {
+            LazyRow(Modifier.size(rowSize), state) {
                 items(100) { index ->
                     Box(
                         Modifier
@@ -71,7 +78,7 @@
         rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
 
         // Assert
-        rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+        rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
     }
 
     @Test
@@ -81,7 +88,7 @@
         lateinit var focusManager: FocusManager
         rule.setContent {
             focusManager = LocalFocusManager.current
-            LazyRow(Modifier.size(rowSize)) {
+            LazyRow(Modifier.size(rowSize), state) {
                 items(100) { index ->
                     Box(Modifier.size(itemSize).focusable()) {
                         Box(Modifier.size(itemSize).focusable().testTag("$index"))
@@ -97,7 +104,7 @@
         rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
 
         // Assert
-        rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+        rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
     }
 
     @Test
@@ -107,7 +114,7 @@
         lateinit var focusManager: FocusManager
         rule.setContent {
             focusManager = LocalFocusManager.current
-            LazyRow(Modifier.size(rowSize)) {
+            LazyRow(Modifier.size(rowSize), state) {
                 items(100) { index ->
                     Box(Modifier.size(itemSize).focusable()) {
                         Box(Modifier.size(itemSize).focusable()) {
@@ -125,6 +132,6 @@
         rule.runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
 
         // Assert
-        rule.runOnIdle { assertThat(composedItems).containsExactly(5, 6) }
+        rule.runOnIdle { assertThat(composedItems).containsExactly(5) }
     }
 }
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
index ebd489b..229f84a 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListNestedPrefetchingTest.kt
@@ -26,6 +26,8 @@
 import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
@@ -70,11 +72,19 @@
     private val itemsSizePx = 30
     private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
     private val activeNodes = mutableSetOf<String>()
-    private val activeMeasuredNodes = mutableSetOf<String>()
+    private val scheduler = TestPrefetchScheduler()
+
+    @OptIn(ExperimentalFoundationApi::class)
+    private val strategy = object : LazyListPrefetchStrategy by LazyListPrefetchStrategy() {
+        override val prefetchScheduler: PrefetchScheduler = scheduler
+    }
+
+    @OptIn(ExperimentalFoundationApi::class)
+    private fun createState(): LazyListState = LazyListState(prefetchStrategy = strategy)
 
     @Test
     fun nestedPrefetchingForwardAfterSmallScroll() {
-        val state = LazyListState()
+        val state = createState()
         composeList(state)
 
         val prefetchIndex = 2
@@ -85,7 +95,7 @@
                 }
             }
 
-            waitForPrefetch(tagFor(prefetchIndex))
+            waitForPrefetch()
         }
 
         // We want to make sure nested children were precomposed before the parent was premeasured
@@ -111,7 +121,7 @@
 
     @Test
     fun cancelingPrefetchCancelsItsNestedPrefetches() {
-        val state = LazyListState()
+        val state = createState()
         composeList(state)
 
         rule.runOnIdle {
@@ -122,7 +132,7 @@
             }
         }
 
-        waitForPrefetch(tagFor(3))
+        waitForPrefetch()
 
         rule.runOnIdle {
             assertThat(activeNodes).contains(tagFor(3))
@@ -141,7 +151,7 @@
             }
         }
 
-        waitForPrefetch(tagFor(7))
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking(AutoTestFrameClock()) {
@@ -160,7 +170,7 @@
     @OptIn(ExperimentalFoundationApi::class)
     @Test
     fun overridingNestedPrefetchCountIsRespected() {
-        val state = LazyListState()
+        val state = createState()
         composeList(
             state,
             createNestedLazyListState = {
@@ -177,7 +187,7 @@
                 }
             }
 
-            waitForPrefetch(tagFor(prefetchIndex))
+            waitForPrefetch()
         }
 
         // Since the nested prefetch count on the strategy is 1, we only expect index 0 to be
@@ -197,7 +207,7 @@
     fun nestedPrefetchIsMeasuredWithProvidedConstraints() {
         val nestedConstraints =
             Constraints(minWidth = 20, minHeight = 20, maxWidth = 20, maxHeight = 20)
-        val state = LazyListState()
+        val state = createState()
         composeList(
             state,
             createNestedLazyListState = {
@@ -214,7 +224,7 @@
                 }
             }
 
-            waitForPrefetch(tagFor(prefetchIndex))
+            waitForPrefetch()
         }
 
         assertThat(actions).containsExactly(
@@ -232,7 +242,7 @@
 
     @Test
     fun nestedPrefetchStartsFromFirstVisibleItemIndex() {
-        val state = LazyListState()
+        val state = createState()
         composeList(
             state,
             createNestedLazyListState = {
@@ -247,7 +257,7 @@
                 }
             }
 
-            waitForPrefetch(tagFor(prefetchIndex))
+            waitForPrefetch()
         }
 
         assertThat(actions).containsExactly(
@@ -273,9 +283,9 @@
         }
     }
 
-    private fun waitForPrefetch(tag: String) {
-        rule.waitUntil {
-            activeNodes.contains(tag) && activeMeasuredNodes.contains(tag)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
@@ -332,17 +342,14 @@
             actions?.add(Action.Compose(index, nestedIndex))
             onDispose {
                 activeNodes.remove(tag)
-                activeMeasuredNodes.remove(tag)
             }
         }
     }
 
     private fun Modifier.trackWhenMeasured(index: Int, nestedIndex: Int? = null): Modifier {
-        val tag = tagFor(index, nestedIndex)
         return this then Modifier.layout { measurable, constraints ->
             actions?.add(Action.Measure(index, nestedIndex))
             val placeable = measurable.measure(constraints)
-            activeMeasuredNodes.add(tag)
             layout(placeable.width, placeable.height) {
                 placeable.place(0, 0)
             }
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
index 31dd332..0d773c2 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
@@ -29,9 +29,10 @@
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
 import androidx.compose.foundation.lazy.list.LazyListPrefetchStrategyTest.RecordingLazyListPrefetchStrategy.Callback
 import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
@@ -74,10 +75,11 @@
     private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
     lateinit var state: LazyListState
+    private val scheduler = TestPrefetchScheduler()
 
     @Test
     fun callbacksTriggered_whenScrollForwardsWithoutVisibleItemsChanged() {
-        val strategy = RecordingLazyListPrefetchStrategy()
+        val strategy = RecordingLazyListPrefetchStrategy(scheduler)
 
         composeList(prefetchStrategy = strategy)
 
@@ -104,7 +106,7 @@
 
     @Test
     fun callbacksTriggered_whenScrollBackwardsWithoutVisibleItemsChanged() {
-        val strategy = RecordingLazyListPrefetchStrategy()
+        val strategy = RecordingLazyListPrefetchStrategy(scheduler)
 
         composeList(firstItem = 10, itemOffset = 10, prefetchStrategy = strategy)
 
@@ -131,7 +133,7 @@
 
     @Test
     fun callbacksTriggered_whenScrollWithVisibleItemsChanged() {
-        val strategy = RecordingLazyListPrefetchStrategy()
+        val strategy = RecordingLazyListPrefetchStrategy(scheduler)
 
         composeList(prefetchStrategy = strategy)
 
@@ -161,7 +163,7 @@
 
     @Test
     fun callbacksTriggered_whenItemsChangedWithoutScroll() {
-        val strategy = RecordingLazyListPrefetchStrategy()
+        val strategy = RecordingLazyListPrefetchStrategy(scheduler)
         val numItems = mutableStateOf(100)
 
         composeList(prefetchStrategy = strategy, numItems = numItems)
@@ -196,20 +198,17 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
         rule.onNodeWithTag("2")
             .assertExists()
     }
 
-    private fun waitForPrefetch(index: Int) {
-        rule.waitUntil {
-            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
-    private val activeNodes = mutableSetOf<Int>()
-    private val activeMeasuredNodes = mutableSetOf<Int>()
-
     @OptIn(ExperimentalFoundationApi::class)
     private fun composeList(
         firstItem: Int = 0,
@@ -228,13 +227,6 @@
                 state,
             ) {
                 items(numItems.value) {
-                    DisposableEffect(it) {
-                        activeNodes.add(it)
-                        onDispose {
-                            activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
-                        }
-                    }
                     Spacer(
                         Modifier
                             .mainAxisSize(itemsSizeDp)
@@ -242,7 +234,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
@@ -256,7 +247,10 @@
     /**
      * LazyListPrefetchStrategy that just records callbacks without scheduling prefetches.
      */
-    private class RecordingLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+    private class RecordingLazyListPrefetchStrategy(
+        override val prefetchScheduler: PrefetchScheduler?
+    ) : LazyListPrefetchStrategy {
+
         sealed interface Callback {
             data class OnScroll(val delta: Float, val visibleIndices: List<Int>) : Callback
             data class OnVisibleItemsUpdated(val visibleIndices: List<Int>) : Callback
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
index 70a88d8..ef1aed5 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
@@ -22,7 +22,10 @@
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
 import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
@@ -71,6 +74,13 @@
 
     lateinit var state: LazyListState
 
+    private val scheduler = TestPrefetchScheduler()
+
+    @OptIn(ExperimentalFoundationApi::class)
+    private val strategy = object : LazyListPrefetchStrategy by LazyListPrefetchStrategy() {
+        override val prefetchScheduler: PrefetchScheduler = scheduler
+    }
+
     @Test
     fun notPrefetchingForwardInitially() {
         composeList()
@@ -97,7 +107,7 @@
             }
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertExists()
@@ -115,7 +125,7 @@
             }
         }
 
-        waitForPrefetch(1)
+        waitForPrefetch()
 
         rule.onNodeWithTag("1")
             .assertExists()
@@ -134,7 +144,7 @@
             }
         }
         var prefetchIndex = initialIndex + 2
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -149,7 +159,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -167,7 +177,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -178,7 +188,7 @@
 
         val prefetchIndex = 3
 
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("${prefetchIndex - 1}")
             .assertIsDisplayed()
@@ -198,7 +208,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -207,7 +217,7 @@
             }
         }
 
-        waitForPrefetch(1)
+        waitForPrefetch()
 
         rule.onNodeWithTag("2")
             .assertIsDisplayed()
@@ -230,7 +240,7 @@
 
         var prefetchIndex = initialIndex + 2
 
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -245,7 +255,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -281,7 +291,7 @@
         }
 
         var prefetchIndex = initialIndex + 1
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("${prefetchIndex + 1}")
             .assertExists()
@@ -295,7 +305,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -458,7 +468,7 @@
             }
         }
 
-        waitForPrefetch(7)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking(AutoTestFrameClock()) {
@@ -472,14 +482,13 @@
         }
     }
 
-    private fun waitForPrefetch(index: Int) {
-        rule.waitUntil {
-            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
     private val activeNodes = mutableSetOf<Int>()
-    private val activeMeasuredNodes = mutableSetOf<Int>()
 
     private fun composeList(
         firstItem: Int = 0,
@@ -488,9 +497,11 @@
         contentPadding: PaddingValues = PaddingValues(0.dp)
     ) {
         rule.setContent {
+            @OptIn(ExperimentalFoundationApi::class)
             state = rememberLazyListState(
                 initialFirstVisibleItemIndex = firstItem,
-                initialFirstVisibleItemScrollOffset = itemOffset
+                initialFirstVisibleItemScrollOffset = itemOffset,
+                prefetchStrategy = strategy
             )
             LazyColumnOrRow(
                 Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -504,7 +515,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -514,7 +524,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
index 224a1cc..72084b7 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPrefetcherTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
 package androidx.compose.foundation.lazy.staggeredgrid
 
 import androidx.compose.foundation.AutoTestFrameClock
@@ -22,7 +24,10 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.Remeasurement
@@ -63,6 +68,20 @@
     val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
 
     internal lateinit var state: LazyStaggeredGridState
+    private val scheduler = TestPrefetchScheduler()
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Composable
+    fun rememberState(
+        initialFirstVisibleItemIndex: Int = 0,
+        initialFirstVisibleItemOffset: Int = 0
+    ): LazyStaggeredGridState = remember {
+        LazyStaggeredGridState(
+            intArrayOf(initialFirstVisibleItemIndex),
+            intArrayOf(initialFirstVisibleItemOffset),
+            scheduler
+        )
+    }
 
     @Test
     fun notPrefetchingForwardInitially() {
@@ -90,7 +109,7 @@
             }
         }
 
-        waitForPrefetch(5)
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertExists()
@@ -110,7 +129,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.onNodeWithTag("2")
             .assertExists()
@@ -130,7 +149,7 @@
             }
         }
 
-        waitForPrefetch(9)
+        waitForPrefetch()
 
         rule.onNodeWithTag("8")
             .assertExists()
@@ -145,7 +164,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.onNodeWithTag("2")
             .assertExists()
@@ -165,7 +184,7 @@
             }
         }
 
-        waitForPrefetch(4)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -174,7 +193,7 @@
             }
         }
 
-        waitForPrefetch(6)
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertIsDisplayed()
@@ -194,7 +213,7 @@
             }
         }
 
-        waitForPrefetch(4)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -203,7 +222,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.onNodeWithTag("4")
             .assertIsDisplayed()
@@ -226,7 +245,7 @@
             }
         }
 
-        waitForPrefetch(13)
+        waitForPrefetch()
 
         rule.onNodeWithTag("12")
             .assertExists()
@@ -378,7 +397,7 @@
     fun snappingToOtherPositionWhilePrefetchIsScheduled() {
         val composedItems = mutableListOf<Int>()
         rule.setContent {
-            state = rememberLazyStaggeredGridState()
+            state = rememberState()
             LazyStaggeredGrid(
                 1,
                 Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -433,7 +452,7 @@
             }
         }
 
-        waitForPrefetch(13)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking(AutoTestFrameClock()) {
@@ -450,7 +469,7 @@
     @Test
     fun scrollingWithStaggeredItemsPrefetchesCorrectly() {
         rule.setContent {
-            state = rememberLazyStaggeredGridState()
+            state = rememberState()
             LazyStaggeredGrid(
                 2,
                 Modifier.mainAxisSize(itemsSizeDp * 5f),
@@ -461,7 +480,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -471,7 +489,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
@@ -495,8 +512,8 @@
             }
         }
 
-        waitForPrefetch(7)
-        waitForPrefetch(8)
+        waitForPrefetch()
+        waitForPrefetch()
 
         // ┌─┬─┐
         // │2├─┤
@@ -520,14 +537,14 @@
         // │6├─┤
         // └─┴─┘
 
-        waitForPrefetch(9)
+        waitForPrefetch()
     }
 
     @Test
     fun fullSpanIsPrefetchedCorrectly() {
         val nodeConstraints = mutableMapOf<Int, Constraints>()
         rule.setContent {
-            state = rememberLazyStaggeredGridState()
+            state = rememberState()
             LazyStaggeredGrid(
                 2,
                 Modifier.mainAxisSize(itemsSizeDp * 5f).crossAxisSize(itemsSizeDp * 2f),
@@ -546,7 +563,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -555,7 +571,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 nodeConstraints.put(it, constraints)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
@@ -577,7 +592,7 @@
         state.scrollBy(itemsSizeDp * 5f)
         assertThat(activeNodes).contains(9)
 
-        waitForPrefetch(10)
+        waitForPrefetch()
         val expectedConstraints = if (vertical) {
             Constraints.fixedWidth(itemsSizePx * 2)
         } else {
@@ -589,7 +604,7 @@
     @Test
     fun fullSpanIsPrefetchedCorrectly_scrollingBack() {
         rule.setContent {
-            state = rememberLazyStaggeredGridState()
+            state = rememberState()
             LazyStaggeredGrid(
                 2,
                 Modifier.mainAxisSize(itemsSizeDp * 5f),
@@ -608,7 +623,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -618,7 +632,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
@@ -647,27 +660,26 @@
 
         state.scrollBy(-1.dp)
 
-        waitForPrefetch(10)
+        waitForPrefetch()
     }
 
-    private fun waitForPrefetch(index: Int) {
-        rule.waitUntil {
-            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
     private val activeNodes = mutableSetOf<Int>()
-    private val activeMeasuredNodes = mutableSetOf<Int>()
 
     private fun composeStaggeredGrid(
         firstItem: Int = 0,
         itemOffset: Int = 0,
     ) {
-        state = LazyStaggeredGridState(
-            initialFirstVisibleItemIndex = firstItem,
-            initialFirstVisibleItemOffset = itemOffset
-        )
         rule.setContent {
+            state = rememberState(
+                initialFirstVisibleItemIndex = firstItem,
+                initialFirstVisibleItemOffset = itemOffset
+            )
             LazyStaggeredGrid(
                 2,
                 Modifier.mainAxisSize(itemsSizeDp * 1.5f),
@@ -678,7 +690,6 @@
                         activeNodes.add(it)
                         onDispose {
                             activeNodes.remove(it)
-                            activeMeasuredNodes.remove(it)
                         }
                     }
                     Spacer(
@@ -688,7 +699,6 @@
                             .testTag("$it")
                             .layout { measurable, constraints ->
                                 val placeable = measurable.measure(constraints)
-                                activeMeasuredNodes.add(it)
                                 layout(placeable.width, placeable.height) {
                                     placeable.place(0, 0)
                                 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
index bf5658f..dd8c1ce 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -30,9 +30,11 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -131,13 +133,21 @@
         key: ((index: Int) -> Any)? = null,
         snapPosition: SnapPosition = config.snapPosition.first,
         flingBehavior: TargetedFlingBehavior? = null,
+        prefetchScheduler: PrefetchScheduler? = null,
         pageContent: @Composable PagerScope.(page: Int) -> Unit = { Page(index = it) }
     ) {
 
         rule.setContent {
-            val state = rememberPagerState(initialPage, initialPageOffsetFraction, pageCount).also {
-                pagerState = it
+            val state = if (prefetchScheduler == null) {
+                rememberPagerState(initialPage, initialPageOffsetFraction, pageCount)
+            } else {
+                remember {
+                    object : PagerState(initialPage, initialPageOffsetFraction, prefetchScheduler) {
+                        override val pageCount: Int get() = pageCount()
+                    }
+                }
             }
+            pagerState = state
             composeView = LocalView.current
             focusManager = LocalFocusManager.current
             CompositionLocalProvider(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
index d72d1e3..19d6748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerAccessibilityTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.pager
 
 import android.view.accessibility.AccessibilityNodeProvider
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -41,6 +42,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
+@OptIn(ExperimentalFoundationApi::class)
 @RunWith(Parameterized::class)
 class PagerAccessibilityTest(config: ParamConfig) : BasePagerTest(config = config) {
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
index 1f9cc0b..f9414d3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPrefetcherTest.kt
@@ -56,6 +56,7 @@
     var pageSizePx = 300
     val pageSizeDp = with(rule.density) { pageSizePx.toDp() }
     var touchSlope: Float = 0.0f
+    private val scheduler = TestPrefetchScheduler()
 
     @Test
     fun notPrefetchingForwardInitially() {
@@ -83,7 +84,7 @@
             }
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertExists()
@@ -102,7 +103,7 @@
             }
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertExists()
@@ -126,7 +127,7 @@
             up()
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertExists()
@@ -151,7 +152,7 @@
             up()
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertExists()
@@ -170,7 +171,7 @@
             }
         }
         var prefetchIndex = initialIndex + 2
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -185,7 +186,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -203,7 +204,7 @@
             }
         }
 
-        waitForPrefetch(2)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -214,7 +215,7 @@
 
         val prefetchIndex = 3
 
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("${prefetchIndex - 1}")
             .assertIsDisplayed()
@@ -236,7 +237,7 @@
             }
         }
 
-        waitForPrefetch(preFetchIndex)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking {
@@ -245,7 +246,7 @@
             }
         }
 
-        waitForPrefetch(preFetchIndex - 1)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$preFetchIndex")
             .assertIsDisplayed()
@@ -268,7 +269,7 @@
 
         var prefetchIndex = initialIndex + 2
 
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -283,7 +284,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -319,7 +320,7 @@
         }
 
         var prefetchIndex = initialIndex + 1
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("${prefetchIndex + 1}")
             .assertExists()
@@ -333,7 +334,7 @@
         }
 
         prefetchIndex -= 3
-        waitForPrefetch(prefetchIndex)
+        waitForPrefetch()
 
         rule.onNodeWithTag("$prefetchIndex")
             .assertExists()
@@ -457,7 +458,7 @@
             }
         }
 
-        waitForPrefetch(7)
+        waitForPrefetch()
 
         rule.runOnIdle {
             runBlocking(AutoTestFrameClock()) {
@@ -477,14 +478,13 @@
         return consumed
     }
 
-    private fun waitForPrefetch(index: Int) {
-        rule.waitUntil {
-            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+    private fun waitForPrefetch() {
+        rule.runOnIdle {
+            scheduler.executeActiveRequests()
         }
     }
 
     private val activeNodes = mutableSetOf<Int>()
-    private val activeMeasuredNodes = mutableSetOf<Int>()
 
     private fun composePager(
         initialPage: Int = 0,
@@ -499,6 +499,7 @@
             beyondViewportPageCount = paramConfig.beyondViewportPageCount,
             initialPage = initialPage,
             initialPageOffsetFraction = initialPageOffsetFraction,
+            prefetchScheduler = scheduler,
             pageCount = { 100 },
             pageSize = {
                 object : PageSize {
@@ -516,7 +517,6 @@
                 activeNodes.add(it)
                 onDispose {
                     activeNodes.remove(it)
-                    activeMeasuredNodes.remove(it)
                 }
             }
 
@@ -527,7 +527,6 @@
                     .testTag("$it")
                     .layout { measurable, constraints ->
                         val placeable = measurable.measure(constraints)
-                        activeMeasuredNodes.add(it)
                         layout(placeable.width, placeable.height) {
                             placeable.place(0, 0)
                         }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt
new file mode 100644
index 0000000..04c60b750
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/TestPrefetchScheduler.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.PrefetchRequest
+import androidx.compose.foundation.lazy.layout.PrefetchRequestScope
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TestPrefetchScheduler : PrefetchScheduler {
+
+    private var activeRequests = mutableListOf<PrefetchRequest>()
+    override fun schedulePrefetch(prefetchRequest: PrefetchRequest) {
+        activeRequests.add(prefetchRequest)
+    }
+
+    fun executeActiveRequests() {
+        activeRequests.forEach {
+            with(it) { scope.execute() }
+        }
+        activeRequests.clear()
+    }
+
+    private val scope = object : PrefetchRequestScope {
+        override fun availableTimeNanos(): Long = Long.MAX_VALUE
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
index b050f4f..fb0f59d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerContextMenuTest.kt
@@ -117,11 +117,17 @@
         expectedClipboardContent = "Text",
     )
 
+    @Test
+    fun contextMenu_onClickSelectAll() = runClickContextMenuItemTest(
+        labelToClick = ContextMenuItemLabels.SELECT_ALL,
+        expectedSelection = TextRange(0, 14),
+    )
+
     @Suppress("SameParameterValue")
     private fun runClickContextMenuItemTest(
         labelToClick: String,
         expectedSelection: TextRange,
-        expectedClipboardContent: String,
+        expectedClipboardContent: String? = null,
     ) {
         val initialClipboardText = "clip"
 
@@ -163,7 +169,8 @@
         assertThat(selection!!.toTextRange()).isEqualTo(expectedSelection)
         val clipboardContent = clipboardManager.getText()
         assertThat(clipboardContent).isNotNull()
-        assertThat(clipboardContent!!.text).isEqualTo(expectedClipboardContent)
+        assertThat(clipboardContent!!.text)
+            .isEqualTo(expectedClipboardContent ?: initialClipboardText)
     }
 
     // endregion Context Menu Item Click Tests
@@ -178,7 +185,7 @@
             cutState = ContextMenuItemState.DOES_NOT_EXIST,
             copyState = ContextMenuItemState.DISABLED,
             pasteState = ContextMenuItemState.DOES_NOT_EXIST,
-            selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+            selectAllState = ContextMenuItemState.ENABLED,
         )
     }
 
@@ -192,7 +199,7 @@
             cutState = ContextMenuItemState.DOES_NOT_EXIST,
             copyState = ContextMenuItemState.ENABLED,
             pasteState = ContextMenuItemState.DOES_NOT_EXIST,
-            selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+            selectAllState = ContextMenuItemState.ENABLED,
         )
     }
 
@@ -206,7 +213,7 @@
             cutState = ContextMenuItemState.DOES_NOT_EXIST,
             copyState = ContextMenuItemState.ENABLED,
             pasteState = ContextMenuItemState.DOES_NOT_EXIST,
-            selectAllState = ContextMenuItemState.DOES_NOT_EXIST,
+            selectAllState = ContextMenuItemState.DISABLED,
         )
     }
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
index 6c4fadc..f4e77b8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/selection/gestures/LazyColumnMultiTextRegressionTest.kt
@@ -168,7 +168,7 @@
         assertTextToolbarTopAt(pointerAreaRect.top)
 
         scrollLines(fromLine = 5, toLine = 3)
-        assertThat(textToolbarShown).isFalse()
+        assertTextToolbarTopAt(pointerAreaRect.top)
 
         scrollLines(fromLine = 5, toLine = 7)
         assertThat(textToolbarShown).isTrue()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
index 54abefd..fd93543 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.android.kt
@@ -126,16 +126,19 @@
         }
         val latestFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(view.drawingTime)
         val nextFrameNs = latestFrameVsyncNs + frameIntervalNs
-        val oneOverTimeTaskAllowed = System.nanoTime() > nextFrameNs
-        val scope = PrefetchRequestScopeImpl(nextFrameNs, oneOverTimeTaskAllowed)
+        val scope = PrefetchRequestScopeImpl(nextFrameNs)
         var scheduleForNextFrame = false
         while (prefetchRequests.isNotEmpty() && !scheduleForNextFrame) {
-            val request = prefetchRequests[0]
-            val hasMoreWorkToDo = with(request) { scope.execute() }
-            if (hasMoreWorkToDo) {
-                scheduleForNextFrame = true
+            if (scope.availableTimeNanos() > 0) {
+                val request = prefetchRequests[0]
+                val hasMoreWorkToDo = with(request) { scope.execute() }
+                if (hasMoreWorkToDo) {
+                    scheduleForNextFrame = true
+                } else {
+                    prefetchRequests.removeAt(0)
+                }
             } else {
-                prefetchRequests.removeAt(0)
+                scheduleForNextFrame = true
             }
         }
 
@@ -182,24 +185,10 @@
 
     class PrefetchRequestScopeImpl(
         private val nextFrameTimeNs: Long,
-        isOneOverTimeTaskAllowed: Boolean
     ) : PrefetchRequestScope {
 
-        private var canDoOverTimeTask = isOneOverTimeTaskAllowed
-
-        override val availableTimeNanos: Long
-            get() {
-                // This logic is meant to be temporary until we replace the isOneOverTimeTaskAllowed
-                // logic with something more general. For now, we assume that a PrefetchRequest
-                // impl will check availableTimeNanos once per task and we give it a large amount
-                // of time the first time it checks if we allow an overtime task.
-                return if (canDoOverTimeTask) {
-                    canDoOverTimeTask = false
-                    Long.MAX_VALUE
-                } else {
-                    max(0, nextFrameTimeNs - System.nanoTime())
-                }
-            }
+        override fun availableTimeNanos() =
+            max(0, nextFrameTimeNs - System.nanoTime())
     }
 
     companion object {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index 19d75d2..4f24fb9 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -78,6 +78,7 @@
     state: ContextMenuState,
 ): ContextMenuScope.() -> Unit {
     val copyString = TextContextMenuItems.Copy.resolvedString()
+    val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
     return {
         listOf(
             item(
@@ -88,7 +89,14 @@
                     state.close()
                 },
             ),
-            // TODO(b/240143283) Add select all item
+            item(
+                label = selectAllString,
+                enabled = !isEntireContainerSelected(),
+                onClick = {
+                    selectAll()
+                    state.close()
+                },
+            ),
         )
     }
 }
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
index 98160e5..ca6265d 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionFakes.kt
@@ -282,7 +282,7 @@
     var boundingBoxes: Map<Int, Rect> = emptyMap()
 
     private val selectableKey = 1L
-    private val fakeSelectAllSelection: Selection = Selection(
+    var fakeSelectAllSelection: Selection? = Selection(
         start = Selection.AnchorInfo(
             direction = ResolvedTextDirection.Ltr,
             offset = 0,
@@ -309,7 +309,7 @@
         )
     }
 
-    override fun getSelectAllSelection(): Selection {
+    override fun getSelectAllSelection(): Selection? {
         return fakeSelectAllSelection
     }
 
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
index a1b2d08..085643e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
@@ -16,16 +16,21 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.collection.LongObjectMap
 import androidx.collection.emptyLongObjectMap
 import androidx.collection.longObjectMapOf
+import androidx.collection.mutableLongObjectMapOf
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.util.fastForEach
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.fail
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -211,14 +216,14 @@
     }
 
     @Test
-    fun mergeSelections_selectAll() {
+    fun mergeSelections_selectAllInSelectable() {
         val anotherSelectableId = 100L
         val selectableAnother = mock<Selectable>()
         whenever(selectableAnother.selectableId).thenReturn(anotherSelectableId)
 
         selectionRegistrar.subscribe(selectableAnother)
 
-        selectionManager.selectAll(
+        selectionManager.selectAllInSelectable(
             selectableId = selectableId,
             previousSelection = fakeSelection
         )
@@ -805,7 +810,7 @@
             any(),
             isNull(),
             isNull(),
-            isNull()
+            any()
         )
     }
 
@@ -911,4 +916,362 @@
             times(1)
         ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
     }
+
+    // region isEntireContainerSelected Tests
+    @Test
+    fun isEntireContainerSelected_noSelectables_returnsTrue() {
+        isEntireContainerSelectedTest(expectedResult = true)
+    }
+
+    @Test
+    fun isEntireContainerSelected_singleEmptySelectable_returnsTrue() {
+        isEntireContainerSelectedTest(
+            expectedResult = true,
+            IsEntireContainerSelectedData(text = "", selection = null),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_multipleEmptySelectables_returnsTrue() {
+        isEntireContainerSelectedTest(
+            expectedResult = true,
+            IsEntireContainerSelectedData(text = "", selection = null),
+            IsEntireContainerSelectedData(text = "", selection = null),
+            IsEntireContainerSelectedData(text = "", selection = null),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_emptySurroundingNonEmpty_fullySelected_returnsTrue() {
+        isEntireContainerSelectedTest(
+            expectedResult = true,
+            IsEntireContainerSelectedData(text = "", selection = null),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "", selection = null),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_nonEmptySurroundingEmpty_fullySelected_returnsTrue() {
+        isEntireContainerSelectedTest(
+            expectedResult = true,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "", selection = TextRange(0, 0)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_nonEmptyFirstTextNotSelected_returnsFalse() {
+        isEntireContainerSelectedTest(
+            expectedResult = false,
+            IsEntireContainerSelectedData(text = "Text", selection = null),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_nonEmptyLastTextNotSelected_returnsFalse() {
+        isEntireContainerSelectedTest(
+            expectedResult = false,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = null),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_firstTextPartiallySelected_returnsFalse() {
+        isEntireContainerSelectedTest(
+            expectedResult = false,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(1, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_lastTextPartiallySelected_returnsFalse() {
+        isEntireContainerSelectedTest(
+            expectedResult = false,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 4)),
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(0, 3)),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_reversedSelectionFullySelected_returnsTrue() {
+        isEntireContainerSelectedTest(
+            expectedResult = true,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(4, 0)),
+        )
+    }
+
+    @Test
+    fun isEntireContainerSelected_reversedSelectionPartiallySelected_returnsFalse() {
+        isEntireContainerSelectedTest(
+            expectedResult = false,
+            IsEntireContainerSelectedData(text = "Text", selection = TextRange(3, 0)),
+        )
+    }
+
+    /**
+     * Data necessary to set up a [SelectionManager.isEntireContainerSelected] unit test.
+     *
+     * @param text The text for the [Selectable] to return in [Selectable.getText].
+     * @param selection The selection to be associated with the [SelectionRegistrar.subselections].
+     * Null implies "do not include this selectable in the sub-selection".
+     */
+    private data class IsEntireContainerSelectedData(
+        val text: String,
+        val selection: TextRange?,
+    )
+
+    private fun isEntireContainerSelectedTest(
+        expectedResult: Boolean,
+        vararg selectableStates: IsEntireContainerSelectedData,
+    ) {
+        val selectables = selectableStates.mapIndexed { index, item ->
+            FakeSelectable().apply {
+                selectableId = index + 1L
+                textToReturn = AnnotatedString(item.text)
+            }
+        }
+
+        val registrar = SelectionRegistrarImpl().apply {
+            selectables.fastForEach { subscribe(it) }
+            subselections = selectableStates
+                .withIndex()
+                .filter { it.value.selection != null }
+                .associate { (index, item) ->
+                    val id = index + 1L
+                    val selection = item.selection
+                    id to Selection(
+                        start = Selection.AnchorInfo(
+                            direction = ResolvedTextDirection.Ltr,
+                            offset = selection!!.start,
+                            selectableId = id
+                        ),
+                        end = Selection.AnchorInfo(
+                            direction = ResolvedTextDirection.Ltr,
+                            offset = selection.end,
+                            selectableId = id
+                        ),
+                        handlesCrossed = selection.reversed
+                    )
+                }
+                .toLongObjectMap()
+        }
+
+        val manager = SelectionManager(registrar).apply {
+            containerLayoutCoordinates = MockCoordinates()
+        }
+
+        assertThat(manager.isEntireContainerSelected()).run {
+            if (expectedResult) isTrue() else isFalse()
+        }
+    }
+    // endregion isEntireContainerSelected Tests
+
+    // region selectAll Tests
+    @Test
+    fun selectAll_noSelectables_noSelection() {
+        selectAllTest(
+            expectedSelection = null,
+            expectedSubSelectionRanges = emptyMap(),
+        )
+    }
+
+    @Test
+    fun selectAll_singleUnSelectable_noSelection() {
+        selectAllTest(
+            expectedSelection = null,
+            expectedSubSelectionRanges = emptyMap(),
+            SelectAllData(text = "Text", selection = null),
+        )
+    }
+
+    @Test
+    fun selectAll_singleSelectable_selectedAsExpected() {
+        selectAllTest(
+            expectedSelection = expectedSelection(0, 4),
+            expectedSubSelectionRanges = mapOf(1L to TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun selectAll_multiSelectable_selectedAsExpected() {
+        selectAllTest(
+            expectedSelection = expectedSelection(
+                startOffset = 0,
+                endOffset = 4,
+                startSelectableId = 1L,
+                endSelectableId = 3L,
+            ),
+            expectedSubSelectionRanges = mapOf(
+                1L to TextRange(0, 4),
+                2L to TextRange(0, 4),
+                3L to TextRange(0, 4),
+            ),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun selectAll_multiSelectable_skipFirst_selectedAsExpected() {
+        selectAllTest(
+            expectedSelection = expectedSelection(
+                startOffset = 0,
+                endOffset = 4,
+                startSelectableId = 2L,
+                endSelectableId = 3L,
+            ),
+            expectedSubSelectionRanges = mapOf(
+                2L to TextRange(0, 4),
+                3L to TextRange(0, 4),
+            ),
+            SelectAllData(text = "Text", selection = null),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun selectAll_multiSelectable_skipMiddle_selectedAsExpected() {
+        selectAllTest(
+            expectedSelection = expectedSelection(
+                startOffset = 0,
+                endOffset = 4,
+                startSelectableId = 1L,
+                endSelectableId = 3L,
+            ),
+            expectedSubSelectionRanges = mapOf(
+                1L to TextRange(0, 4),
+                3L to TextRange(0, 4),
+            ),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = null),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+        )
+    }
+
+    @Test
+    fun selectAll_multiSelectable_skipLast_selectedAsExpected() {
+        selectAllTest(
+            expectedSelection = expectedSelection(
+                startOffset = 0,
+                endOffset = 4,
+                startSelectableId = 1L,
+                endSelectableId = 2L,
+            ),
+            expectedSubSelectionRanges = mapOf(
+                1L to TextRange(0, 4),
+                2L to TextRange(0, 4),
+            ),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = TextRange(0, 4)),
+            SelectAllData(text = "Text", selection = null),
+        )
+    }
+
+    private fun expectedSelection(
+        startOffset: Int,
+        endOffset: Int,
+        startSelectableId: Long = 1L,
+        endSelectableId: Long = 1L,
+        handlesCrossed: Boolean = false,
+    ): Selection = Selection(
+        start = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = startOffset,
+            selectableId = startSelectableId
+        ),
+        end = Selection.AnchorInfo(
+            direction = ResolvedTextDirection.Ltr,
+            offset = endOffset,
+            selectableId = endSelectableId
+        ),
+        handlesCrossed = handlesCrossed
+    )
+
+    /**
+     * Data necessary to set up a [SelectionManager.selectAll] unit test.
+     *
+     * @param text The text for the [FakeSelectable] to return in [Selectable.getText].
+     * @param selection The selection for the [FakeSelectable] to return in
+     * [Selectable.getSelectAllSelection].
+     */
+    private data class SelectAllData(
+        val text: String,
+        val selection: TextRange?,
+    )
+
+    private fun selectAllTest(
+        expectedSelection: Selection?,
+        expectedSubSelectionRanges: Map<Long, TextRange>,
+        vararg selectableStates: SelectAllData,
+    ) {
+        val selectables = selectableStates.mapIndexed { index, item ->
+            val id = index + 1L
+            val range = item.selection
+            FakeSelectable().apply {
+                selectableId = id
+                textToReturn = AnnotatedString(item.text)
+                fakeSelectAllSelection = range?.let {
+                    Selection(
+                        start = Selection.AnchorInfo(
+                            direction = ResolvedTextDirection.Ltr,
+                            offset = it.start,
+                            selectableId = id
+                        ),
+                        end = Selection.AnchorInfo(
+                            direction = ResolvedTextDirection.Ltr,
+                            offset = it.end,
+                            selectableId = id
+                        ),
+                        handlesCrossed = it.reversed
+                    )
+                }
+            }
+        }
+
+        val registrar = SelectionRegistrarImpl().apply {
+            selectables.fastForEach { subscribe(it) }
+        }
+
+        val expectedSubSelections = expectedSubSelectionRanges.mapValues { (id, range) ->
+            expectedSelection(
+                startOffset = range.start,
+                endOffset = range.end,
+                startSelectableId = id,
+                endSelectableId = id,
+                handlesCrossed = range.start > range.end
+            )
+        }
+            .toLongObjectMap()
+
+        SelectionManager(registrar).apply {
+            containerLayoutCoordinates = MockCoordinates()
+            onSelectionChange = { newSelection ->
+                if (expectedSelection == null) {
+                    fail("Expected no selection update, but received one anyways.")
+                }
+                assertThat(newSelection).isEqualTo(expectedSelection)
+            }
+            selectAll()
+        }
+
+        assertThat(registrar.subselections).isEqualTo(expectedSubSelections)
+    }
+    // endregion selectAll Tests
+
+    private fun <T> Map<Long, T>.toLongObjectMap(): LongObjectMap<T> =
+        mutableLongObjectMapOf<T>().apply {
+            this@toLongObjectMap.keys.forEach { key -> put(key, this@toLongObjectMap[key]!!) }
+        }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
index 20a8d5f..556bcb0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
@@ -42,10 +42,11 @@
     }
 
     override fun calculateDistanceTo(targetIndex: Int): Float {
-        val visibleItem =
-            state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+        val layoutInfo = state.layoutInfo
+        if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+        val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
         return if (visibleItem == null) {
-            val averageSize = visibleItemsAverageSize
+            val averageSize = calculateVisibleItemsAverageSize(layoutInfo)
             val indexesDiff = targetIndex - firstVisibleItemIndex
             (averageSize * indexesDiff).toFloat() - firstVisibleItemScrollOffset
         } else {
@@ -57,11 +58,9 @@
         state.scroll(block = block)
     }
 
-    private val visibleItemsAverageSize: Int
-        get() {
-            val layoutInfo = state.layoutInfo
-            val visibleItems = layoutInfo.visibleItemsInfo
-            val itemsSum = visibleItems.fastSumBy { it.size }
-            return itemsSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
-        }
+    private fun calculateVisibleItemsAverageSize(layoutInfo: LazyListLayoutInfo): Int {
+        val visibleItems = layoutInfo.visibleItemsInfo
+        val itemsSum = visibleItems.fastSumBy { it.size }
+        return itemsSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
+    }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
index dc29f8c..83840e9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
@@ -147,21 +147,38 @@
             } else {
                 layoutInfo.visibleItemsInfo.first().index - 1
             }
-            if (indexToPrefetch != this@DefaultLazyListPrefetchStrategy.indexToPrefetch &&
-                indexToPrefetch in 0 until layoutInfo.totalItemsCount
-            ) {
-                if (wasScrollingForward != scrollingForward) {
-                    // the scrolling direction has been changed which means the last prefetched
-                    // is not going to be reached anytime soon so it is safer to dispose it.
-                    // if this item is already visible it is safe to call the method anyway
-                    // as it will be no-op
-                    currentPrefetchHandle?.cancel()
+            if (indexToPrefetch in 0 until layoutInfo.totalItemsCount) {
+                if (indexToPrefetch != this@DefaultLazyListPrefetchStrategy.indexToPrefetch) {
+                    if (wasScrollingForward != scrollingForward) {
+                        // the scrolling direction has been changed which means the last prefetched
+                        // is not going to be reached anytime soon so it is safer to dispose it.
+                        // if this item is already visible it is safe to call the method anyway
+                        // as it will be no-op
+                        currentPrefetchHandle?.cancel()
+                    }
+                    this@DefaultLazyListPrefetchStrategy.wasScrollingForward = scrollingForward
+                    this@DefaultLazyListPrefetchStrategy.indexToPrefetch = indexToPrefetch
+                    currentPrefetchHandle = schedulePrefetch(
+                        indexToPrefetch
+                    )
                 }
-                this@DefaultLazyListPrefetchStrategy.wasScrollingForward = scrollingForward
-                this@DefaultLazyListPrefetchStrategy.indexToPrefetch = indexToPrefetch
-                currentPrefetchHandle = schedulePrefetch(
-                    indexToPrefetch
-                )
+                if (scrollingForward) {
+                    val lastItem = layoutInfo.visibleItemsInfo.last()
+                    val spacing = layoutInfo.mainAxisItemSpacing
+                    val distanceToPrefetchItem =
+                        lastItem.offset + lastItem.size + spacing - layoutInfo.viewportEndOffset
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToPrefetchItem < -delta) {
+                        currentPrefetchHandle?.markAsUrgent()
+                    }
+                } else {
+                    val firstItem = layoutInfo.visibleItemsInfo.first()
+                    val distanceToPrefetchItem = layoutInfo.viewportStartOffset - firstItem.offset
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToPrefetchItem < delta) {
+                        currentPrefetchHandle?.markAsUrgent()
+                    }
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
index 78e6ac9..5fa07f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -37,27 +37,25 @@
 
     override val itemCount: Int get() = state.layoutInfo.totalItemsCount
 
-    private val visibleItemsAverageSize: Int
-        get() = calculateLineAverageMainAxisSize(state.layoutInfo)
-
     override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
         state.snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true)
     }
 
     override fun calculateDistanceTo(targetIndex: Int): Float {
-        val visibleItem =
-            state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+        val layoutInfo = state.layoutInfo
+        if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+        val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
 
         return if (visibleItem == null) {
             val slotsPerLine = state.slotsPerLine
-            val averageLineMainAxisSize = visibleItemsAverageSize
+            val averageLineMainAxisSize = calculateLineAverageMainAxisSize(layoutInfo)
             val before = targetIndex < firstVisibleItemIndex
             val linesDiff =
                 (targetIndex - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
                     slotsPerLine
             (averageLineMainAxisSize * linesDiff).toFloat() - firstVisibleItemScrollOffset
         } else {
-            if (state.layoutInfo.orientation == Orientation.Vertical) {
+            if (layoutInfo.orientation == Orientation.Vertical) {
                 visibleItem.offset.y
             } else {
                 visibleItem.offset.x
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index ab042b2..d12d075 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
 import androidx.compose.foundation.gestures.stopScroll
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -31,6 +32,7 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -81,19 +83,26 @@
  * A state object that can be hoisted to control and observe scrolling.
  *
  * In most cases, this will be created via [rememberLazyGridState].
- *
- * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
- * @param firstVisibleItemScrollOffset the initial value for
- * [LazyGridState.firstVisibleItemScrollOffset]
  */
 @OptIn(ExperimentalFoundationApi::class)
 @Stable
-class LazyGridState constructor(
+class LazyGridState internal constructor(
     firstVisibleItemIndex: Int = 0,
-    firstVisibleItemScrollOffset: Int = 0
+    firstVisibleItemScrollOffset: Int = 0,
+    prefetchScheduler: PrefetchScheduler?,
 ) : ScrollableState {
 
     /**
+     * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
+     * @param firstVisibleItemScrollOffset the initial value for
+     * [LazyGridState.firstVisibleItemScrollOffset]
+     */
+    constructor(
+        firstVisibleItemIndex: Int = 0,
+        firstVisibleItemScrollOffset: Int = 0
+    ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, null)
+
+    /**
      * The holder class for the current scroll position.
      */
     private val scrollPosition =
@@ -413,23 +422,40 @@
                 }
                 closestNextItemToPrefetch = info.visibleItemsInfo.first().index - 1
             }
-            if (lineToPrefetch != this.lineToPrefetch &&
-                closestNextItemToPrefetch in 0 until info.totalItemsCount
-            ) {
-                if (wasScrollingForward != scrollingForward) {
-                    // the scrolling direction has been changed which means the last prefetched
-                    // is not going to be reached anytime soon so it is safer to dispose it.
-                    // if this line is already visible it is safe to call the method anyway
-                    // as it will be no-op
-                    currentLinePrefetchHandles.forEach { it.cancel() }
+            if (closestNextItemToPrefetch in 0 until info.totalItemsCount) {
+                if (lineToPrefetch != this.lineToPrefetch) {
+                    if (wasScrollingForward != scrollingForward) {
+                        // the scrolling direction has been changed which means the last prefetched
+                        // is not going to be reached anytime soon so it is safer to dispose it.
+                        // if this line is already visible it is safe to call the method anyway
+                        // as it will be no-op
+                        currentLinePrefetchHandles.forEach { it.cancel() }
+                    }
+                    this.wasScrollingForward = scrollingForward
+                    this.lineToPrefetch = lineToPrefetch
+                    currentLinePrefetchHandles.clear()
+                    info.prefetchInfoRetriever(lineToPrefetch).fastForEach {
+                        currentLinePrefetchHandles.add(
+                            prefetchState.schedulePrefetch(it.first, it.second)
+                        )
+                    }
                 }
-                this.wasScrollingForward = scrollingForward
-                this.lineToPrefetch = lineToPrefetch
-                currentLinePrefetchHandles.clear()
-                info.prefetchInfoRetriever(lineToPrefetch).fastForEach {
-                    currentLinePrefetchHandles.add(
-                        prefetchState.schedulePrefetch(it.first, it.second)
-                    )
+                if (scrollingForward) {
+                    val lastItem = info.visibleItemsInfo.last()
+                    val distanceToPrefetchItem = lastItem.offsetOnMainAxis(info.orientation) +
+                        lastItem.mainAxisSizeWithSpacings - info.viewportEndOffset
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToPrefetchItem < -delta) {
+                        currentLinePrefetchHandles.forEach { it.markAsUrgent() }
+                    }
+                } else {
+                    val firstItem = info.visibleItemsInfo.first()
+                    val distanceToPrefetchItem = info.viewportStartOffset -
+                        firstItem.offsetOnMainAxis(info.orientation)
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToPrefetchItem < delta) {
+                        currentLinePrefetchHandles.forEach { it.markAsUrgent() }
+                    }
                 }
             }
         }
@@ -454,7 +480,7 @@
         }
     }
 
-    internal val prefetchState = LazyLayoutPrefetchState()
+    internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler)
 
     private val numOfItemsToTeleport: Int get() = 100 * slotsPerLine
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
index 11c2a13..20d6ca5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
@@ -79,6 +79,15 @@
          * was precomposed already it will be disposed.
          */
         fun cancel()
+
+        /**
+         * Marks this prefetch request as urgent, which is a way to communicate that the requested
+         * item is expected to be needed during the next frame.
+         *
+         * For urgent requests we can proceed with doing the prefetch even if the available time
+         * in the frame is less than we spend on similar prefetch requests on average.
+         */
+        fun markAsUrgent()
     }
 
     private inner class NestedPrefetchScopeImpl : NestedPrefetchScope {
@@ -169,6 +178,7 @@
 @ExperimentalFoundationApi
 private object DummyHandle : PrefetchHandle {
     override fun cancel() {}
+    override fun markAsUrgent() {}
 }
 
 /**
@@ -212,6 +222,7 @@
         private val isComposed get() = precomposeHandle != null
         private var hasResolvedNestedPrefetches = false
         private var nestedPrefetchController: NestedPrefetchController? = null
+        private var isUrgent = false
 
         private val isValid
             get() = !isCanceled &&
@@ -225,13 +236,24 @@
             }
         }
 
+        override fun markAsUrgent() {
+            isUrgent = true
+        }
+
+        private fun PrefetchRequestScope.shouldExecute(average: Long): Boolean {
+            val available = availableTimeNanos()
+            // even for urgent request we only do the work if we have time available, as otherwise
+            // it is better to just return early to allow the next frame to start and do the work.
+            return (isUrgent && available > 0) || average < available
+        }
+
         override fun PrefetchRequestScope.execute(): Boolean {
             if (!isValid) {
                 return false
             }
 
             if (!isComposed) {
-                if (prefetchMetrics.averageCompositionTimeNanos < availableTimeNanos) {
+                if (shouldExecute(prefetchMetrics.averageCompositionTimeNanos)) {
                     prefetchMetrics.recordCompositionTiming {
                         trace("compose:lazy:prefetch:compose") {
                             performComposition()
@@ -242,27 +264,35 @@
                 }
             }
 
-            // Nested prefetch logic is best-effort: if nested LazyLayout children are
-            // added/removed/updated after we've resolved nested prefetch states here or resolved
-            // nestedPrefetchRequests below, those changes won't be taken into account.
-            if (!hasResolvedNestedPrefetches) {
-                if (availableTimeNanos > 0) {
-                    trace("compose:lazy:prefetch:resolve-nested") {
-                        nestedPrefetchController = resolveNestedPrefetchStates()
-                        hasResolvedNestedPrefetches = true
+            // if the request is urgent we better proceed with the measuring straight away instead
+            // of spending time trying to split the work more via nested prefetch. nested prefetch
+            // is always an estimation and it could potentially do work we will not need in the end,
+            // but the measuring will only do exactly the needed work (including composing nested
+            // lazy layouts)
+            if (!isUrgent) {
+                // Nested prefetch logic is best-effort: if nested LazyLayout children are
+                // added/removed/updated after we've resolved nested prefetch states here or resolved
+                // nestedPrefetchRequests below, those changes won't be taken into account.
+                if (!hasResolvedNestedPrefetches) {
+                    if (availableTimeNanos() > 0) {
+                        trace("compose:lazy:prefetch:resolve-nested") {
+                            nestedPrefetchController = resolveNestedPrefetchStates()
+                            hasResolvedNestedPrefetches = true
+                        }
+                    } else {
+                        return true
                     }
-                } else {
+                }
+
+                val hasMoreWork =
+                    nestedPrefetchController?.run { executeNestedPrefetches() } ?: false
+                if (hasMoreWork) {
                     return true
                 }
             }
 
-            val hasMoreWork = nestedPrefetchController?.run { executeNestedPrefetches() } ?: false
-            if (hasMoreWork) {
-                return true
-            }
-
             if (!isMeasured && constraints != null) {
-                if (prefetchMetrics.averageMeasureTimeNanos < availableTimeNanos) {
+                if (shouldExecute(prefetchMetrics.averageMeasureTimeNanos)) {
                     prefetchMetrics.recordMeasureTiming {
                         trace("compose:lazy:prefetch:measure") {
                             performMeasure(constraints)
@@ -349,7 +379,7 @@
                 trace("compose:lazy:prefetch:nested") {
                     while (stateIndex < states.size) {
                         if (requestsByState[stateIndex] == null) {
-                            if (availableTimeNanos <= 0) {
+                            if (availableTimeNanos() <= 0) {
                                 // When we have time again, we'll resolve nested requests for this
                                 // state
                                 return true
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
index d3497f8..131eb4f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchScheduler.kt
@@ -75,5 +75,5 @@
      * How much time is available to do prefetch work. Implementations of [PrefetchRequest] should
      * do their best to fit their work into this time without going over.
      */
-    val availableTimeNanos: Long
+    fun availableTimeNanos(): Long
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
index 9d9853c..3b8621c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
@@ -44,16 +44,17 @@
     }
 
     override fun calculateDistanceTo(targetIndex: Int): Float {
-        val visibleItem =
-            state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+        val layoutInfo = state.layoutInfo
+        if (layoutInfo.visibleItemsInfo.isEmpty()) return 0f
+        val visibleItem = layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
         return if (visibleItem == null) {
-            val averageMainAxisItemSize = visibleItemsAverageSize
+            val averageMainAxisItemSize = calculateVisibleItemsAverageSize(layoutInfo)
 
             val laneCount = state.laneCount
             val lineDiff = targetIndex / laneCount - firstVisibleItemIndex / laneCount
             averageMainAxisItemSize * lineDiff.toFloat() - firstVisibleItemScrollOffset
         } else {
-            if (state.layoutInfo.orientation == Orientation.Vertical) {
+            if (layoutInfo.orientation == Orientation.Vertical) {
                 visibleItem.offset.y
             } else {
                 visibleItem.offset.x
@@ -65,17 +66,15 @@
         state.scroll(block = block)
     }
 
-    private val visibleItemsAverageSize: Int
-        get() {
-            val layoutInfo = state.layoutInfo
-            val visibleItems = layoutInfo.visibleItemsInfo
-            val itemSizeSum = visibleItems.fastSumBy {
-                if (layoutInfo.orientation == Orientation.Vertical) {
-                    it.size.height
-                } else {
-                    it.size.width
-                }
+    private fun calculateVisibleItemsAverageSize(layoutInfo: LazyStaggeredGridLayoutInfo): Int {
+        val visibleItems = layoutInfo.visibleItemsInfo
+        val itemSizeSum = visibleItems.fastSumBy {
+            if (layoutInfo.orientation == Orientation.Vertical) {
+                it.size.height
+            } else {
+                it.size.width
             }
-            return itemSizeSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
         }
+        return itemSizeSum / visibleItems.size + layoutInfo.mainAxisItemSpacing
+    }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index 09cb286..3a15ecb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
@@ -82,9 +83,10 @@
  * In most cases, it should be created via [rememberLazyStaggeredGridState].
  */
 @OptIn(ExperimentalFoundationApi::class)
-class LazyStaggeredGridState private constructor(
+class LazyStaggeredGridState internal constructor(
     initialFirstVisibleItems: IntArray,
     initialFirstVisibleOffsets: IntArray,
+    prefetchScheduler: PrefetchScheduler?
 ) : ScrollableState {
     /**
      * @param initialFirstVisibleItemIndex initial value for [firstVisibleItemIndex]
@@ -95,7 +97,8 @@
         initialFirstVisibleItemOffset: Int = 0
     ) : this(
         intArrayOf(initialFirstVisibleItemIndex),
-        intArrayOf(initialFirstVisibleItemOffset)
+        intArrayOf(initialFirstVisibleItemOffset),
+        null
     )
 
     /**
@@ -178,7 +181,7 @@
     internal var prefetchingEnabled: Boolean = true
 
     /** prefetch state used for precomputing items in the direction of scroll */
-    internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState()
+    internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState(prefetchScheduler)
 
     /** state controlling the scroll */
     private val scrollableState = ScrollableState { -onScroll(-it) }
@@ -584,7 +587,7 @@
                 )
             },
             restore = {
-                LazyStaggeredGridState(it[0], it[1])
+                LazyStaggeredGridState(it[0], it[1], null)
             }
         )
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index edf220c..a4310c5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.PrefetchScheduler
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.derivedStateOf
@@ -147,18 +148,26 @@
 
 /**
  * The state that can be used to control [VerticalPager] and [HorizontalPager]
- * @param currentPage The initial page to be displayed
- * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
- * the layout.
  */
 @OptIn(ExperimentalFoundationApi::class)
 @Stable
-abstract class PagerState(
+abstract class PagerState internal constructor(
     currentPage: Int = 0,
-    @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f
+    @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
+    prefetchScheduler: PrefetchScheduler? = null
 ) : ScrollableState {
 
     /**
+     * @param currentPage The initial page to be displayed
+     * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
+     * the layout.
+     */
+    constructor(
+        currentPage: Int = 0,
+        @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f
+    ) : this(currentPage, currentPageOffsetFraction, null)
+
+    /**
      * The total amount of pages present in this pager. The source of this data should be
      * observable.
      */
@@ -431,7 +440,7 @@
      */
     val currentPageOffsetFraction: Float get() = scrollPosition.currentPageOffsetFraction
 
-    internal val prefetchState = LazyLayoutPrefetchState()
+    internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler)
 
     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
 
@@ -716,21 +725,38 @@
             } else {
                 info.visiblePagesInfo.first().index - info.beyondViewportPageCount - PagesToPrefetch
             }
-            if (indexToPrefetch != this.indexToPrefetch &&
-                indexToPrefetch in 0 until pageCount
-            ) {
-                if (wasPrefetchingForward != isPrefetchingForward) {
-                    // the scrolling direction has been changed which means the last prefetched
-                    // is not going to be reached anytime soon so it is safer to dispose it.
-                    // if this item is already visible it is safe to call the method anyway
-                    // as it will be no-op
-                    currentPrefetchHandle?.cancel()
+            if (indexToPrefetch in 0 until pageCount) {
+                if (indexToPrefetch != this.indexToPrefetch) {
+                    if (wasPrefetchingForward != isPrefetchingForward) {
+                        // the scrolling direction has been changed which means the last prefetched
+                        // is not going to be reached anytime soon so it is safer to dispose it.
+                        // if this item is already visible it is safe to call the method anyway
+                        // as it will be no-op
+                        currentPrefetchHandle?.cancel()
+                    }
+                    this.wasPrefetchingForward = isPrefetchingForward
+                    this.indexToPrefetch = indexToPrefetch
+                    currentPrefetchHandle = prefetchState.schedulePrefetch(
+                        indexToPrefetch, premeasureConstraints
+                    )
                 }
-                this.wasPrefetchingForward = isPrefetchingForward
-                this.indexToPrefetch = indexToPrefetch
-                currentPrefetchHandle = prefetchState.schedulePrefetch(
-                    indexToPrefetch, premeasureConstraints
-                )
+                if (isPrefetchingForward) {
+                    val lastItem = info.visiblePagesInfo.last()
+                    val pageSize = info.pageSize + info.pageSpacing
+                    val distanceToReachNextItem =
+                        lastItem.offset + pageSize - info.viewportEndOffset
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToReachNextItem < delta) {
+                        currentPrefetchHandle?.markAsUrgent()
+                    }
+                } else {
+                    val firstItem = info.visiblePagesInfo.first()
+                    val distanceToReachNextItem = info.viewportStartOffset - firstItem.offset
+                    // if in the next frame we will get the same delta will we reach the item?
+                    if (distanceToReachNextItem < -delta) {
+                        currentPrefetchHandle?.markAsUrgent()
+                    }
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 4fad5b6..4cb66ac 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -58,6 +58,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFold
 import androidx.compose.ui.util.fastForEach
@@ -277,7 +278,7 @@
 
         selectionRegistrar.onSelectionUpdateSelectAll =
             { isInTouchMode, selectableId ->
-                val (newSelection, newSubselection) = selectAll(
+                val (newSelection, newSubselection) = selectAllInSelectable(
                     selectableId = selectableId,
                     previousSelection = selection,
                 )
@@ -409,7 +410,7 @@
         return coordinates
     }
 
-    internal fun selectAll(
+    internal fun selectAllInSelectable(
         selectableId: Long,
         previousSelection: Selection?
     ): Pair<Selection?, LongObjectMap<Selection>> {
@@ -428,6 +429,70 @@
     }
 
     /**
+     * Returns whether the selection encompasses the entire container.
+     */
+    internal fun isEntireContainerSelected(): Boolean {
+        val selectables = selectionRegistrar.sort(requireContainerCoordinates())
+
+        // If there are no selectables, then an empty selection spans the entire container.
+        if (selectables.isEmpty()) return true
+
+        // Since some text exists, we must make sure that every selectable is fully selected.
+        return selectables.fastAll {
+            val text = it.getText()
+            if (text.isEmpty()) return@fastAll true // empty text is inherently fully selected
+
+            // If a non-empty selectable isn't included in the sub-selections,
+            // then some text in the container is not selected.
+            val subSelection = selectionRegistrar.subselections[it.selectableId]
+                ?: return@fastAll false
+
+            val selectionStart = subSelection.start.offset
+            val selectionEnd = subSelection.end.offset
+
+            // The selection could be reversed,
+            // so just verify that the difference between the two offsets matches the text length
+            (selectionStart - selectionEnd).absoluteValue == text.length
+        }
+    }
+
+    /**
+     * Creates and sets a selection spanning the entire container.
+     */
+    internal fun selectAll() {
+        val selectables = selectionRegistrar.sort(requireContainerCoordinates())
+        if (selectables.isEmpty()) return
+
+        var firstSubSelection: Selection? = null
+        var lastSubSelection: Selection? = null
+        val newSubSelections = mutableLongObjectMapOf<Selection>().apply {
+            selectables.fastForEach { selectable ->
+                val subSelection = selectable.getSelectAllSelection() ?: return@fastForEach
+                if (firstSubSelection == null) firstSubSelection = subSelection
+                lastSubSelection = subSelection
+                put(selectable.selectableId, subSelection)
+            }
+        }
+
+        if (newSubSelections.isEmpty()) return
+
+        // first/last sub selections are implied to be non-null from here on out
+        val newSelection = if (firstSubSelection === lastSubSelection) {
+            firstSubSelection
+        } else {
+            Selection(
+                start = firstSubSelection!!.start,
+                end = lastSubSelection!!.end,
+                handlesCrossed = false,
+            )
+        }
+
+        selectionRegistrar.subselections = newSubSelections
+        onSelectionChange(newSelection)
+        previousSelectionLayout = null
+    }
+
+    /**
      * Returns whether the start and end anchors are equal.
      *
      * It is possible that this returns true, but the selection is still empty because it has
@@ -521,9 +586,13 @@
         }
 
         val textToolbar = textToolbar ?: return
-        if (showToolbar && isInTouchMode && isNonEmptySelection()) {
+        if (showToolbar && isInTouchMode) {
             val rect = getContentRect() ?: return
-            textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
+            textToolbar.showMenu(
+                rect = rect,
+                onCopyRequested = if (isNonEmptySelection()) ::toolbarCopy else null,
+                onSelectAllRequested = if (isEntireContainerSelected()) null else ::selectAll,
+            )
         } else if (textToolbar.status == TextToolbarStatus.Shown) {
             textToolbar.hide()
         }
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index a905398..bda7255 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -49,6 +49,19 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
+            android:label="C StaticScrollingWithChrome Init"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".TrivialStartupTracingActivity"
             android:label="C TrivialTracing"
             android:exported="true">
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
new file mode 100644
index 0000000..87810a2
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2023 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.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.trace
+
+class StaticScrollingContentWithChromeInitialCompositionActivity : ComponentActivity() {
+
+    private val onlyPerformComposition: Boolean
+        get() = intent.action == "androidx.compose.integration.macrobenchmark.target" +
+            ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            if (onlyPerformComposition) {
+                ComposeOnlyLayout {
+                    StaticScrollingContentWithChrome(
+                        modifier = Modifier
+                            .onPlaced { _ ->
+                                throw RuntimeException(
+                                    "Content was placed, but should only be composed"
+                                )
+                            }
+                            .drawWithContent {
+                                throw RuntimeException(
+                                    "Content was drawn, but should only be composed"
+                                )
+                            }
+                    )
+                }
+            } else {
+                StaticScrollingContentWithChrome()
+            }
+        }
+    }
+}
+
+/**
+ * A layout that will compose all of the [content], but will not place
+ * (and therefore not layout or draw) any of its children.
+ *
+ * This is useful for this benchmark as we care about the composition time. A major limitation
+ * of this approach is that any content in a SubcomposeLayout will not be composed
+ * and will not contribute to the overall measured time of this test.
+ */
+@Composable
+private fun ComposeOnlyLayout(
+    content: @Composable () -> Unit
+) {
+    Layout(content) { _, _ -> layout(0, 0) {} }
+}
+
+@Preview
+@Composable
+private fun StaticScrollingContentWithChrome(
+    modifier: Modifier = Modifier
+) = trace(sectionName = "StaticScrollingContentWithChrome") {
+    Column(modifier) {
+        TopBar()
+        ScrollingContent(modifier = Modifier.weight(1f))
+        BottomBar()
+    }
+}
+
+@Composable
+private fun TopBar(modifier: Modifier = Modifier) {
+    TopAppBar(
+        modifier = modifier,
+        title = {
+            Column {
+                Text(
+                    "Initial Composition Macrobench",
+                    style = MaterialTheme.typography.subtitle1,
+                    maxLines = 1
+                )
+                Text(
+                    "Static Scrolling Content w/ Chrome",
+                    style = MaterialTheme.typography.caption,
+                    maxLines = 1
+                )
+            }
+        },
+        navigationIcon = {
+            Button(onClick = {}) {
+                Icon(Icons.Default.Close, "Dismiss")
+            }
+        },
+        actions = {
+            Button(onClick = {}) {
+                Icon(Icons.Default.MoreVert, "Actions")
+            }
+        }
+    )
+}
+
+@Composable
+private fun BottomBar(modifier: Modifier = Modifier) {
+    BottomNavigation(modifier = modifier) {
+        BottomNavigationItem(
+            selected = true,
+            onClick = {},
+            icon = { Icon(Icons.Default.Home, "Home") }
+        )
+        BottomNavigationItem(
+            selected = false,
+            onClick = {},
+            icon = { Icon(Icons.Default.Add, "Add") }
+        )
+    }
+}
+
+@Composable
+private fun ScrollingContent(modifier: Modifier = Modifier) {
+    Column(
+        modifier
+            .fillMaxSize()
+            .verticalScroll(rememberScrollState())
+            .padding(vertical = 16.dp)
+    ) {
+        Item(
+            color = Color.DarkGray,
+            icon = Icons.Filled.Info,
+            modifier = Modifier
+                .padding(horizontal = 16.dp)
+                .aspectRatio(16f / 9f)
+                .fillMaxWidth()
+                .clip(RoundedCornerShape(16.dp))
+        )
+
+        repeat(5) { iteration ->
+            CardGroup(
+                title = "Group ${4 * iteration}",
+                groupIcon = Icons.Filled.Person,
+                groupColor = Color(0xFF1967D2)
+            )
+
+            CardGroup(
+                title = "Group ${4 * iteration + 1}",
+                groupIcon = Icons.Filled.Favorite,
+                groupColor = Color(0xFFC5221F)
+            )
+
+            CardGroup(
+                title = "Group ${4 * iteration + 2}",
+                groupIcon = Icons.Filled.Star,
+                groupColor = Color(0xFFF29900)
+            )
+
+            CardGroup(
+                title = "Group ${4 * iteration + 3}",
+                groupIcon = Icons.Filled.Place,
+                groupColor = Color(0xFF188038)
+            )
+        }
+    }
+}
+
+@Composable
+private fun CardGroup(
+    title: String,
+    groupIcon: ImageVector,
+    groupColor: Color,
+    modifier: Modifier = Modifier,
+    count: Int = 10
+) {
+    Column(
+        modifier = modifier
+    ) {
+        Text(
+            title,
+            style = MaterialTheme.typography.h6,
+            modifier = Modifier.padding(16.dp)
+        )
+
+        Row(
+            modifier = Modifier
+                .horizontalScroll(rememberScrollState())
+                .padding(horizontal = 12.dp)
+        ) {
+            repeat(count) {
+                Item(
+                    color = groupColor,
+                    icon = groupIcon,
+                    modifier = Modifier
+                        .padding(horizontal = 4.dp)
+                        .size(64.dp)
+                        .clip(RoundedCornerShape(4.dp))
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun Item(
+    color: Color,
+    icon: ImageVector,
+    modifier: Modifier = Modifier
+) {
+    Box(
+        modifier = modifier.background(color),
+        contentAlignment = Alignment.Center
+    ) {
+        Icon(icon, null, tint = Color.White)
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
index 003b307..c070d43 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
@@ -19,6 +19,8 @@
 import android.content.Intent
 import android.graphics.Point
 import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingGfxInfoMetric
 import androidx.benchmark.macro.FrameTimingMetric
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.platform.app.InstrumentationRegistry
@@ -41,11 +43,12 @@
         device = UiDevice.getInstance(instrumentation)
     }
 
+    @OptIn(ExperimentalMetricApi::class)
     @Test
     fun start() {
         benchmarkRule.measureRepeated(
             packageName = PACKAGE_NAME,
-            metrics = listOf(FrameTimingMetric()),
+            metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
             compilationMode = CompilationMode.Full(),
             iterations = 8,
             setupBlock = {
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
new file mode 100644
index 0000000..b573368
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 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.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.testutils.createStartupCompilationParams
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class StaticScrollingContentWithChromeInitialCompositionBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode
+) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun initialComposition() = benchmarkRule.measureStartup(
+        compilationMode = compilationMode,
+        startupMode = startupMode,
+        packageName = "androidx.compose.integration.macrobenchmark.target"
+    ) {
+        action = "androidx.compose.integration.macrobenchmark.target" +
+            ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+    }
+
+    @Test
+    fun firstFrame() = benchmarkRule.measureStartup(
+        compilationMode = compilationMode,
+        startupMode = startupMode,
+        packageName = "androidx.compose.integration.macrobenchmark.target"
+    ) {
+        action = "androidx.compose.integration.macrobenchmark.target" +
+            ".STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY"
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "startup={0},compilation={1}")
+        @JvmStatic
+        fun parameters() = createStartupCompilationParams()
+    }
+}
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 7c87844..69623fb 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -646,12 +646,10 @@
   }
 
   @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class RippleConfiguration {
-    ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+    ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
     method public long getColor();
     method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
-    method public boolean isEnabled();
     property public final long color;
-    property public final boolean isEnabled;
     property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
   }
 
@@ -662,11 +660,11 @@
   }
 
   public final class RippleKt {
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> getLocalRippleConfiguration();
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> getLocalRippleConfiguration();
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
-    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> LocalRippleConfiguration;
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> LocalRippleConfiguration;
     property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
   }
 
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 7c87844..69623fb 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -646,12 +646,10 @@
   }
 
   @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class RippleConfiguration {
-    ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+    ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
     method public long getColor();
     method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
-    method public boolean isEnabled();
     property public final long color;
-    property public final boolean isEnabled;
     property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
   }
 
@@ -662,11 +660,11 @@
   }
 
   public final class RippleKt {
-    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> getLocalRippleConfiguration();
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> getLocalRippleConfiguration();
     method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
-    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration> LocalRippleConfiguration;
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.RippleConfiguration?> LocalRippleConfiguration;
     property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
   }
 
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
index 15c1d55..c52867d 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/RippleTest.kt
@@ -1015,10 +1015,6 @@
     fun rippleConfiguration_disabled_dragged() {
         val interactionSource = MutableInteractionSource()
 
-        val rippleConfiguration = RippleConfiguration(
-            isEnabled = false
-        )
-
         var scope: CoroutineScope? = null
 
         rule.setContent {
@@ -1026,7 +1022,7 @@
             MaterialTheme {
                 Surface {
                     CompositionLocalProvider(
-                        LocalRippleConfiguration provides rippleConfiguration
+                        LocalRippleConfiguration provides null
                     ) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                             RippleBoxWithBackground(
@@ -1072,7 +1068,7 @@
 
         val contentColor = Color.Black
 
-        var rippleConfiguration by mutableStateOf(RippleConfiguration())
+        var rippleConfiguration: RippleConfiguration? by mutableStateOf(RippleConfiguration())
 
         var scope: CoroutineScope? = null
 
@@ -1119,7 +1115,6 @@
         }
 
         val newConfiguration = RippleConfiguration(
-            isEnabled = true,
             color = Color.Red,
             rippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
         )
@@ -1147,7 +1142,7 @@
         }
 
         rule.runOnUiThread {
-            rippleConfiguration = RippleConfiguration(isEnabled = false)
+            rippleConfiguration = null
         }
 
         with(rule.onNodeWithTag(Tag)) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
index 50cca67..7b371e3 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Ripple.kt
@@ -200,7 +200,8 @@
 /**
  * CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a
  * tree-local 'override' for ripples used inside components that you cannot directly control, such
- * as to change the color of a specific component's ripple, or disable it entirely.
+ * as to change the color of a specific component's ripple, or disable it entirely by providing
+ * `null`.
  *
  * In most cases you should rely on the default theme behavior for consistency with other components
  * - this exists as an escape hatch for individual components and is not intended to be used for
@@ -211,15 +212,15 @@
 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
 @get:ExperimentalMaterialApi
 @ExperimentalMaterialApi
-val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration> =
+val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration?> =
     compositionLocalOf { RippleConfiguration() }
 
 /**
  * Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases
  * the default values should be used, for custom design system use cases you should instead
- * build your own custom ripple using [createRippleModifierNode].
+ * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide
+ * `null` using [LocalRippleConfiguration].
  *
- * @param isEnabled whether the ripple is enabled. If false, no ripple will be rendered
  * @param color the color override for the ripple. If [Color.Unspecified], then the default color
  * from the theme will be used instead. Note that if the ripple has a color explicitly set with
  * the parameter on [ripple], that will always be used instead of this value.
@@ -229,7 +230,6 @@
 @Immutable
 @ExperimentalMaterialApi
 class RippleConfiguration(
-    val isEnabled: Boolean = true,
     val color: Color = Color.Unspecified,
     val rippleAlpha: RippleAlpha? = null
 ) {
@@ -237,7 +237,6 @@
         if (this === other) return true
         if (other !is RippleConfiguration) return false
 
-        if (isEnabled != other.isEnabled) return false
         if (color != other.color) return false
         if (rippleAlpha != other.rippleAlpha) return false
 
@@ -245,14 +244,13 @@
     }
 
     override fun hashCode(): Int {
-        var result = isEnabled.hashCode()
-        result = 31 * result + color.hashCode()
+        var result = color.hashCode()
         result = 31 * result + (rippleAlpha?.hashCode() ?: 0)
         return result
     }
 
     override fun toString(): String {
-        return "RippleConfiguration(enabled=$isEnabled, color=$color, rippleAlpha=$rippleAlpha)"
+        return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)"
     }
 }
 
@@ -353,13 +351,14 @@
     }
 
     /**
-     * Handles changes to [RippleConfiguration.isEnabled]. Changes to [RippleConfiguration.color] and
-     * [RippleConfiguration.rippleAlpha] are handled as part of the ripple definition.
+     * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to
+     * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of
+     * the ripple definition.
      */
     private fun updateConfiguration() {
         observeReads {
             val configuration = currentValueOf(LocalRippleConfiguration)
-            if (!configuration.isEnabled) {
+            if (configuration == null) {
                 removeRipple()
             } else {
                 if (rippleNode == null) attachNewRipple()
@@ -373,8 +372,10 @@
             if (userDefinedColor.isSpecified) {
                 userDefinedColor
             } else {
+                // If this is null, the ripple will be removed, so this should always be non-null in
+                // normal use
                 val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
-                if (rippleConfiguration.color.isSpecified) {
+                if (rippleConfiguration?.color?.isSpecified == true) {
                     rippleConfiguration.color
                 } else {
                     RippleDefaults.rippleColor(
@@ -385,8 +386,10 @@
             }
         }
         val calculateRippleAlpha = {
+            // If this is null, the ripple will be removed, so this should always be non-null in
+            // normal use
             val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
-            rippleConfiguration.rippleAlpha ?: RippleDefaults.rippleAlpha(
+            rippleConfiguration?.rippleAlpha ?: RippleDefaults.rippleAlpha(
                 contentColor = currentValueOf(LocalContentColor),
                 lightTheme = currentValueOf(LocalColors).isLight
             )
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index cfed154..83a3415 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1255,12 +1255,10 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class RippleConfiguration {
-    ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+    ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
     method public long getColor();
     method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
-    method public boolean isEnabled();
     property public final long color;
-    property public final boolean isEnabled;
     property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
   }
 
@@ -1271,11 +1269,11 @@
   }
 
   public final class RippleKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> getLocalRippleConfiguration();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> getLocalRippleConfiguration();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
-    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> LocalRippleConfiguration;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> LocalRippleConfiguration;
     property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index cfed154..83a3415 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1255,12 +1255,10 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class RippleConfiguration {
-    ctor public RippleConfiguration(optional boolean isEnabled, optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
+    ctor public RippleConfiguration(optional long color, optional androidx.compose.material.ripple.RippleAlpha? rippleAlpha);
     method public long getColor();
     method public androidx.compose.material.ripple.RippleAlpha? getRippleAlpha();
-    method public boolean isEnabled();
     property public final long color;
-    property public final boolean isEnabled;
     property public final androidx.compose.material.ripple.RippleAlpha? rippleAlpha;
   }
 
@@ -1271,11 +1269,11 @@
   }
 
   public final class RippleKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> getLocalRippleConfiguration();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> getLocalRippleConfiguration();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalUseFallbackRippleImplementation();
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(androidx.compose.ui.graphics.ColorProducer color, optional boolean bounded, optional float radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
-    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration> LocalRippleConfiguration;
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material3.RippleConfiguration?> LocalRippleConfiguration;
     property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalUseFallbackRippleImplementation;
   }
 
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
index a802cae..80cc29d 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/RippleTest.kt
@@ -503,10 +503,6 @@
     fun rippleConfiguration_disabled_dragged() {
         val interactionSource = MutableInteractionSource()
 
-        val rippleConfiguration = RippleConfiguration(
-            isEnabled = false
-        )
-
         var scope: CoroutineScope? = null
 
         rule.setContent {
@@ -514,7 +510,7 @@
             MaterialTheme {
                 Surface {
                     CompositionLocalProvider(
-                        LocalRippleConfiguration provides rippleConfiguration
+                        LocalRippleConfiguration provides null
                     ) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                             RippleBoxWithBackground(
@@ -560,7 +556,7 @@
 
         val contentColor = Color.Black
 
-        var rippleConfiguration by mutableStateOf(RippleConfiguration())
+        var rippleConfiguration: RippleConfiguration? by mutableStateOf(RippleConfiguration())
 
         var scope: CoroutineScope? = null
 
@@ -607,7 +603,6 @@
         }
 
         val newConfiguration = RippleConfiguration(
-            isEnabled = true,
             color = Color.Red,
             rippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
         )
@@ -635,7 +630,7 @@
         }
 
         rule.runOnUiThread {
-            rippleConfiguration = RippleConfiguration(isEnabled = false)
+            rippleConfiguration = null
         }
 
         with(rule.onNodeWithTag(Tag)) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
index 322b33f..485b51c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Ripple.kt
@@ -165,7 +165,8 @@
 /**
  * CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a
  * tree-local 'override' for ripples used inside components that you cannot directly control, such
- * as to change the color of a specific component's ripple, or disable it entirely.
+ * as to change the color of a specific component's ripple, or disable it entirely by providing
+ * `null`.
  *
  * In most cases you should rely on the default theme behavior for consistency with other components
  * - this exists as an escape hatch for individual components and is not intended to be used for
@@ -176,15 +177,15 @@
 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
 @get:ExperimentalMaterial3Api
 @ExperimentalMaterial3Api
-val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration> =
+val LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration?> =
     compositionLocalOf { RippleConfiguration() }
 
 /**
  * Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases
  * the default values should be used, for custom design system use cases you should instead
- * build your own custom ripple using [createRippleModifierNode].
+ * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide
+ * `null` using [LocalRippleConfiguration].
  *
- * @param isEnabled whether the ripple is enabled. If false, no ripple will be rendered
  * @param color the color override for the ripple. If [Color.Unspecified], then the default color
  * from the theme will be used instead. Note that if the ripple has a color explicitly set with
  * the parameter on [ripple], that will always be used instead of this value.
@@ -194,7 +195,6 @@
 @Immutable
 @ExperimentalMaterial3Api
 class RippleConfiguration(
-    val isEnabled: Boolean = true,
     val color: Color = Color.Unspecified,
     val rippleAlpha: RippleAlpha? = null
 ) {
@@ -202,7 +202,6 @@
         if (this === other) return true
         if (other !is RippleConfiguration) return false
 
-        if (isEnabled != other.isEnabled) return false
         if (color != other.color) return false
         if (rippleAlpha != other.rippleAlpha) return false
 
@@ -210,14 +209,13 @@
     }
 
     override fun hashCode(): Int {
-        var result = isEnabled.hashCode()
-        result = 31 * result + color.hashCode()
+        var result = color.hashCode()
         result = 31 * result + (rippleAlpha?.hashCode() ?: 0)
         return result
     }
 
     override fun toString(): String {
-        return "RippleConfiguration(enabled=$isEnabled, color=$color, rippleAlpha=$rippleAlpha)"
+        return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)"
     }
 }
 
@@ -311,13 +309,14 @@
     }
 
     /**
-     * Handles changes to [RippleConfiguration.isEnabled]. Changes to [RippleConfiguration.color] and
-     * [RippleConfiguration.rippleAlpha] are handled as part of the ripple definition.
+     * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to
+     * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of
+     * the ripple definition.
      */
     private fun updateConfiguration() {
         observeReads {
             val configuration = currentValueOf(LocalRippleConfiguration)
-            if (!configuration.isEnabled) {
+            if (configuration == null) {
                 removeRipple()
             } else {
                 if (rippleNode == null) attachNewRipple()
@@ -331,8 +330,10 @@
             if (userDefinedColor.isSpecified) {
                 userDefinedColor
             } else {
+                // If this is null, the ripple will be removed, so this should always be non-null in
+                // normal use
                 val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
-                if (rippleConfiguration.color.isSpecified) {
+                if (rippleConfiguration?.color?.isSpecified == true) {
                     rippleConfiguration.color
                 } else {
                     currentValueOf(LocalContentColor)
@@ -341,8 +342,10 @@
         }
 
         val calculateRippleAlpha = {
+            // If this is null, the ripple will be removed, so this should always be non-null in
+            // normal use
             val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
-            rippleConfiguration.rippleAlpha ?: RippleDefaults.RippleAlpha
+            rippleConfiguration?.rippleAlpha ?: RippleDefaults.RippleAlpha
         }
 
         rippleNode = delegate(createRippleModifierNode(
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
new file mode 100644
index 0000000..9b61a35
--- /dev/null
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2023 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.runtime.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.random.Random
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SlotTableIntegrationBenchmark : ComposeBenchmarkBase() {
+
+    @UiThreadTest
+    @Test
+    fun create() = runBlockingTestWithFrameClock {
+        measureCompose {
+            Column(
+                modifier = Modifier.size(width = 20.dp, height = 300.dp)
+            ) {
+                repeat(100) {
+                    key(it) {
+                        Pixel(color = Color.Blue)
+                    }
+                }
+            }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun removeManyGroups() = runBlockingTestWithFrameClock {
+        var includeGroups by mutableStateOf(true)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    if (includeGroups) {
+                        repeat(100) {
+                            key(it) {
+                                Pixel(color = Color.Blue)
+                            }
+                        }
+                    }
+                }
+            }
+            update { includeGroups = false }
+            reset { includeGroups = true }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun removeAlternatingGroups() = runBlockingTestWithFrameClock {
+        var insertAlternatingGroups by mutableStateOf(true)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    repeat(100) { index ->
+                        if (index % 2 == 0 || insertAlternatingGroups) {
+                            key(index) {
+                                Pixel(color = Color.Blue)
+                            }
+                        }
+                    }
+                }
+            }
+            update { insertAlternatingGroups = false }
+            reset { insertAlternatingGroups = true }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun removeManyReplaceGroups() = runBlockingTestWithFrameClock {
+        var insertAlternatingGroups by mutableStateOf(true)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    repeat(100) { index ->
+                        if (index % 2 == 0 || insertAlternatingGroups) {
+                            Pixel(color = Color(
+                                red = 0,
+                                green = 2 * index,
+                                blue = 0
+                            ))
+                        }
+                    }
+                }
+            }
+            update { insertAlternatingGroups = false }
+            reset { insertAlternatingGroups = true }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun insertManyGroups() = runBlockingTestWithFrameClock {
+        var includeGroups by mutableStateOf(false)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    if (includeGroups) {
+                        repeat(100) {
+                            key(it) {
+                                Pixel(color = Color.Blue)
+                            }
+                        }
+                    }
+                }
+            }
+            update { includeGroups = true }
+            reset { includeGroups = false }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun insertAlternatingGroups() = runBlockingTestWithFrameClock {
+        var insertAlternatingGroups by mutableStateOf(false)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    repeat(100) { index ->
+                        if (index % 2 == 0 || insertAlternatingGroups) {
+                            key(index) {
+                                Pixel(color = Color.Blue)
+                            }
+                        }
+                    }
+                }
+            }
+            update { insertAlternatingGroups = true }
+            reset { insertAlternatingGroups = false }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun insertManyReplaceGroups() = runBlockingTestWithFrameClock {
+        var insertAlternatingGroups by mutableStateOf(false)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    repeat(100) { index ->
+                        if (index % 2 == 0 || insertAlternatingGroups) {
+                            Pixel(color = Color(
+                                red = 0,
+                                green = 2 * index,
+                                blue = 0
+                            ))
+                        }
+                    }
+                }
+            }
+            update { insertAlternatingGroups = true }
+            reset { insertAlternatingGroups = false }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun updateManyNestedGroups() = runBlockingTestWithFrameClock {
+        var seed by mutableIntStateOf(1337)
+        measureRecomposeSuspending {
+            compose {
+                val random = remember(seed) { Random(seed) }
+                MatryoshkaLayout(
+                    depth = 100,
+                    content = {
+                        MinimalBox {
+                            Pixel(color = Color(random.nextInt()))
+                            Pixel(color = Color.Red)
+                            Pixel(color = Color.Green)
+                            Pixel(color = Color.Blue)
+                        }
+                        MinimalBox {
+                            NonRenderingText("abcdef")
+                        }
+                        NonRenderingText(
+                            text = random.nextString(),
+                            textColor = Color(random.nextInt()),
+                            textSize = random.nextInt(6, 32).dp,
+                            ellipsize = random.nextBoolean(),
+                            minLines = random.nextInt(),
+                            maxLines = random.nextInt(),
+                        )
+                    }
+                )
+            }
+            update { seed++ }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun updateDisjointGroups() = runBlockingTestWithFrameClock {
+        var seed by mutableIntStateOf(1337)
+        measureRecomposeSuspending {
+            compose {
+                MinimalBox {
+                    repeat(10) { container ->
+                        MinimalBox {
+                            MatryoshkaLayout(
+                                depth = 100,
+                                content = { depth ->
+                                    if (depth > 50) {
+                                        val random = Random(seed * container + depth)
+                                        NonRenderingText(
+                                            text = random.nextString(),
+                                            textColor = Color(random.nextInt()),
+                                            textSize = random.nextInt(6, 32).dp,
+                                            ellipsize = random.nextBoolean(),
+                                            minLines = random.nextInt(),
+                                            maxLines = random.nextInt(),
+                                        )
+                                    } else {
+                                        NonRenderingText("foo")
+                                    }
+                                }
+                            )
+                        }
+                    }
+                }
+            }
+            update { seed++ }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun updateDeepCompositionLocalHierarchy() = runBlockingTestWithFrameClock {
+        val PixelColorLocal = compositionLocalOf { Color.Unspecified }
+        var seed by mutableIntStateOf(1337)
+        measureRecomposeSuspending {
+            compose {
+                val random = remember(seed) { Random(seed) }
+                Pixel(PixelColorLocal.current)
+                CompositionLocalProvider(
+                    PixelColorLocal provides Color(random.nextInt())
+                ) {
+                    Pixel(PixelColorLocal.current)
+                    CompositionLocalProvider(
+                        PixelColorLocal provides Color(random.nextInt())
+                    ) {
+                        Pixel(PixelColorLocal.current)
+                        CompositionLocalProvider(
+                            PixelColorLocal provides Color(random.nextInt())
+                        ) {
+                            Pixel(PixelColorLocal.current)
+                            CompositionLocalProvider(
+                                PixelColorLocal provides Color(random.nextInt())
+                            ) {
+                                Pixel(PixelColorLocal.current)
+                                CompositionLocalProvider(
+                                    PixelColorLocal provides Color(random.nextInt())
+                                ) {
+                                    Pixel(PixelColorLocal.current)
+                                    CompositionLocalProvider(
+                                        PixelColorLocal provides Color(random.nextInt())
+                                    ) {
+                                        Pixel(PixelColorLocal.current)
+                                        CompositionLocalProvider(
+                                            PixelColorLocal provides Color(random.nextInt())
+                                        ) {
+                                            Pixel(PixelColorLocal.current)
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            update { seed++ }
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun reverseGroups() = runBlockingTestWithFrameClock {
+        val originalItems = (1..100).toList()
+        var keys by mutableStateOf(originalItems)
+        measureRecomposeSuspending {
+            compose {
+                Column(
+                    modifier = Modifier.size(width = 20.dp, height = 300.dp)
+                ) {
+                    keys.forEach {
+                        key(it) {
+                            Pixel(color = Color.Blue)
+                        }
+                    }
+                }
+            }
+            update { keys = keys.reversed() }
+            reset { keys = originalItems }
+        }
+    }
+}
+
+@Composable
+private fun Pixel(color: Color) {
+    Layout(
+        modifier = Modifier.background(color)
+    ) { _, _ ->
+        layout(1, 1) {}
+    }
+}
+
+@Composable
+private fun NonRenderingText(
+    text: String,
+    textColor: Color = Color.Unspecified,
+    textSize: Dp = Dp.Unspecified,
+    ellipsize: Boolean = false,
+    minLines: Int = 1,
+    maxLines: Int = Int.MAX_VALUE
+) {
+    use(text)
+    use(textColor.value.toInt())
+    use(textSize.value)
+    use(ellipsize)
+    use(minLines)
+    use(maxLines)
+    Layout { _, _ ->
+        layout(1, 1) {}
+    }
+}
+
+@Composable
+private fun MinimalBox(
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit
+) {
+    Layout(content, modifier, MinimalBoxMeasurePolicy)
+}
+
+@Composable
+private fun MatryoshkaLayout(
+    depth: Int,
+    content: @Composable (depth: Int) -> Unit
+) {
+    if (depth <= 0) {
+        content(0)
+    } else {
+        Layout(
+            content = {
+                content(depth)
+                MatryoshkaLayout(depth - 1, content)
+            },
+            measurePolicy = MinimalBoxMeasurePolicy
+        )
+    }
+}
+
+private val MinimalBoxMeasurePolicy = MeasurePolicy { measurables, constraints ->
+    val placeables = measurables.map { it.measure(constraints) }
+    val (usedWidth, usedHeight) = placeables.fold(
+        initial = IntOffset(0, 0)
+    ) { (maxWidth, maxHeight), placeable ->
+        IntOffset(
+            maxOf(maxWidth, placeable.measuredWidth),
+            maxOf(maxHeight, placeable.measuredHeight)
+        )
+    }
+
+    layout(
+        width = usedWidth,
+        height = usedHeight
+    ) {
+        placeables.forEach { it.place(0, 0) }
+    }
+}
+
+private fun Random.nextString(length: Int = 16) = buildString(length) {
+    repeat(length) { append(nextInt('A'.code, 'z'.code).toChar()) }
+}
+
+@Suppress("UNUSED_PARAMETER") private fun use(value: Any?) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Int) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Long) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Float) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Double) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Boolean) {}
diff --git a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
index 37dcfb0..88c4553 100644
--- a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
+++ b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
@@ -141,4 +141,21 @@
             -Offset(Float.NaN, Float.NaN)
         }
     }
+
+    @Test
+    fun testIsFinite() {
+        assertTrue(Offset(10.0f, 20.0f).isFinite)
+        assertTrue(Offset(0.0f, 0.0f).isFinite)
+        assertTrue(Offset(10.0f, -20.0f).isFinite)
+
+        assertFalse(Offset(10.0f, Float.POSITIVE_INFINITY).isFinite)
+        assertFalse(Offset(10.0f, Float.NEGATIVE_INFINITY).isFinite)
+        assertFalse(Offset(Float.POSITIVE_INFINITY, 20.0f).isFinite)
+        assertFalse(Offset(Float.NEGATIVE_INFINITY, 20.0f).isFinite)
+        assertFalse(Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY).isFinite)
+
+        assertFails {
+            Offset(Float.NaN, Float.NaN).isFinite
+        }
+    }
 }
diff --git a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
index 01d8f1d..a6b033c 100644
--- a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
+++ b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/SizeTest.kt
@@ -16,7 +16,10 @@
 
 package androidx.compose.ui.geometry
 
-import org.junit.Assert
+import kotlin.test.assertFails
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -27,11 +30,11 @@
 
     @Test
     fun sizeTimesInt() {
-        Assert.assertEquals(
+        assertEquals(
             Size(10f, 10f),
             Size(2.5f, 2.5f) * 4f
         )
-        Assert.assertEquals(
+        assertEquals(
             Size(10f, 10f),
             4f * Size(2.5f, 2.5f)
         )
@@ -39,7 +42,7 @@
 
     @Test
     fun sizeDivInt() {
-        Assert.assertEquals(
+        assertEquals(
             Size(10f, 10f),
             Size(40f, 40f) / 4f
         )
@@ -47,24 +50,24 @@
 
     @Test
     fun sizeTimesFloat() {
-        Assert.assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
-        Assert.assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
+        assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
+        assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
     }
 
     @Test
     fun sizeDivFloat() {
-        Assert.assertEquals(Size(10f, 10f), Size(40f, 40f) / 4f)
+        assertEquals(Size(10f, 10f), Size(40f, 40f) / 4f)
     }
 
     @Test
     fun sizeTimesDouble() {
-        Assert.assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
-        Assert.assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
+        assertEquals(Size(10f, 10f), Size(4f, 4f) * 2.5f)
+        assertEquals(Size(10f, 10f), 2.5f * Size(4f, 4f))
     }
 
     @Test
     fun sizeDivDouble() {
-        Assert.assertEquals(
+        assertEquals(
             Size(10f, 10f),
             Size(40f, 40f) / 4.0f
         )
@@ -73,23 +76,23 @@
     @Test
     fun testSizeCopy() {
         val size = Size(100f, 200f)
-        Assert.assertEquals(size, size.copy())
+        assertEquals(size, size.copy())
     }
 
     @Test
     fun testSizeCopyOverwriteWidth() {
         val size = Size(100f, 200f)
         val copy = size.copy(width = 50f)
-        Assert.assertEquals(50f, copy.width)
-        Assert.assertEquals(200f, copy.height)
+        assertEquals(50f, copy.width)
+        assertEquals(200f, copy.height)
     }
 
     @Test
     fun testSizeCopyOverwriteHeight() {
         val size = Size(100f, 200f)
         val copy = size.copy(height = 300f)
-        Assert.assertEquals(100f, copy.width)
-        Assert.assertEquals(300f, copy.height)
+        assertEquals(100f, copy.width)
+        assertEquals(300f, copy.height)
     }
 
     @Test
@@ -137,38 +140,61 @@
     fun testSizeLerp() {
         val size1 = Size(100f, 200f)
         val size2 = Size(300f, 500f)
-        Assert.assertEquals(Size(200f, 350f), lerp(size1, size2, 0.5f))
+        assertEquals(Size(200f, 350f), lerp(size1, size2, 0.5f))
     }
 
     @Test
     fun testIsSpecified() {
-        Assert.assertFalse(Size.Unspecified.isSpecified)
-        Assert.assertTrue(Size(1f, 1f).isSpecified)
+        assertFalse(Size.Unspecified.isSpecified)
+        assertTrue(Size(1f, 1f).isSpecified)
     }
 
     @Test
     fun testIsUnspecified() {
-        Assert.assertTrue(Size.Unspecified.isUnspecified)
-        Assert.assertFalse(Size(1f, 1f).isUnspecified)
+        assertTrue(Size.Unspecified.isUnspecified)
+        assertFalse(Size(1f, 1f).isUnspecified)
     }
 
     @Test
     fun testTakeOrElseTrue() {
-        Assert.assertTrue(Size(1f, 1f).takeOrElse { Size.Unspecified }.isSpecified)
+        assertTrue(Size(1f, 1f).takeOrElse { Size.Unspecified }.isSpecified)
     }
 
     @Test
     fun testTakeOrElseFalse() {
-        Assert.assertTrue(Size.Unspecified.takeOrElse { Size(1f, 1f) }.isSpecified)
+        assertTrue(Size.Unspecified.takeOrElse { Size(1f, 1f) }.isSpecified)
     }
 
     @Test
     fun testUnspecifiedSizeToString() {
-        Assert.assertEquals("Size.Unspecified", Size.Unspecified.toString())
+        assertEquals("Size.Unspecified", Size.Unspecified.toString())
     }
 
     @Test
     fun testSpecifiedSizeToString() {
-        Assert.assertEquals("Size(10.0, 20.0)", Size(10f, 20f).toString())
+        assertEquals("Size(10.0, 20.0)", Size(10f, 20f).toString())
+    }
+
+    @Test
+    fun testIsEmpty() {
+        assertFalse(Size(10.0f, 20.0f).isEmpty())
+        assertFalse(Size(10.0f, Float.POSITIVE_INFINITY).isEmpty())
+        assertFalse(Size(Float.POSITIVE_INFINITY, 20.0f).isEmpty())
+
+        assertTrue(Size(0.0f, 20.0f).isEmpty())
+        assertTrue(Size(10.0f, 0.0f).isEmpty())
+        assertTrue(Size(0.0f, 0.0f).isEmpty())
+        assertTrue(Size(-10.0f, 20.0f).isEmpty())
+        assertTrue(Size(10.0f, -20.0f).isEmpty())
+        assertTrue(Size(0.0f, Float.POSITIVE_INFINITY).isEmpty())
+        assertTrue(Size(Float.POSITIVE_INFINITY, 0.0f).isEmpty())
+        assertTrue(Size(0.0f, Float.NEGATIVE_INFINITY).isEmpty())
+        assertTrue(Size(Float.NEGATIVE_INFINITY, 0.0f).isEmpty())
+        assertTrue(Size(Float.NEGATIVE_INFINITY, 20.0f).isEmpty())
+        assertTrue(Size(10.0f, Float.NEGATIVE_INFINITY).isEmpty())
+
+        assertFails {
+            Size.Unspecified.isEmpty()
+        }
     }
 }
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
index 2ebbbd0..2a00b19 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/InlineClassHelper.kt
@@ -21,9 +21,11 @@
 
 // Masks everything but the sign bit
 internal const val UnsignedFloatMask = 0x7fffffffL
+internal const val DualUnsignedFloatMask = 0x7fffffff_7fffffffL
 
 // Any value greater than this is a NaN
 internal const val FloatInfinityBase = 0x7f800000L
+internal const val DualFloatInfinityBase = 0x7f800000_7f800000L
 
 // Same as Offset/Size.Unspecified.packedValue, but avoids a getstatic
 internal const val UnspecifiedPackedFloats = 0x7fc00000_7fc00000L // NaN_NaN
@@ -31,6 +33,8 @@
 // 0x80000000_80000000UL.toLong() but expressed as a const value
 // Mask for the sign bit of the two floats packed in a long
 internal const val DualFloatSignBit = -0x7fffffff_80000000L
+// Set the highest bit of each 32 bit chunk in a 64 bit word
+internal const val Uint64High32 = -0x7fffffff_80000000L
 
 // This function exists so we do *not* inline the throw. It keeps
 // the call site much smaller and since it's the slow path anyway,
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
index e09f97f..459e711 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
@@ -91,7 +91,7 @@
      * x or y parameter
      */
     fun copy(x: Float = unpackFloat1(packedValue), y: Float = unpackFloat2(packedValue)) =
-        Offset(x, y)
+        Offset(packFloats(x, y))
 
     companion object {
         /**
@@ -100,26 +100,23 @@
          * This can be used to represent the origin of a coordinate space.
          */
         @Stable
-        val Zero = Offset(0.0f, 0.0f)
+        val Zero = Offset(0x0L)
 
         /**
          * An offset with infinite x and y components.
          *
-         * See also:
-         *
-         *  * [isInfinite], which checks whether either component is infinite.
-         *  * [isFinite], which checks whether both components are finite.
+         * See also [isFinite] to check whether both components are finite.
          */
         // This is included for completeness, because [Size.infinite] exists.
         @Stable
-        val Infinite = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
+        val Infinite = Offset(DualFloatInfinityBase)
 
         /**
          * Represents an unspecified [Offset] value, usually a replacement for `null`
          * when a primitive value is desired.
          */
         @Stable
-        val Unspecified = Offset(Float.NaN, Float.NaN)
+        val Unspecified = Offset(UnspecifiedPackedFloats)
     }
 
     @Stable
@@ -193,8 +190,10 @@
             "Offset is unspecified"
         }
         return Offset(
-            unpackFloat1(packedValue) - unpackFloat1(other.packedValue),
-            unpackFloat2(packedValue) - unpackFloat2(other.packedValue),
+            packFloats(
+                unpackFloat1(packedValue) - unpackFloat1(other.packedValue),
+                unpackFloat2(packedValue) - unpackFloat2(other.packedValue)
+            )
         )
     }
 
@@ -214,8 +213,10 @@
             "Offset is unspecified"
         }
         return Offset(
-            unpackFloat1(packedValue) + unpackFloat1(other.packedValue),
-            unpackFloat2(packedValue) + unpackFloat2(other.packedValue),
+            packFloats(
+                unpackFloat1(packedValue) + unpackFloat1(other.packedValue),
+                unpackFloat2(packedValue) + unpackFloat2(other.packedValue)
+            )
         )
     }
 
@@ -232,8 +233,10 @@
             "Offset is unspecified"
         }
         return Offset(
-            unpackFloat1(packedValue) * operand,
-            unpackFloat2(packedValue) * operand,
+            packFloats(
+                unpackFloat1(packedValue) * operand,
+                unpackFloat2(packedValue) * operand
+            )
         )
     }
 
@@ -250,8 +253,10 @@
             "Offset is unspecified"
         }
         return Offset(
-            unpackFloat1(packedValue) / operand,
-            unpackFloat2(packedValue) / operand,
+            packFloats(
+                unpackFloat1(packedValue) / operand,
+                unpackFloat2(packedValue) / operand
+            )
         )
     }
 
@@ -268,8 +273,10 @@
             "Offset is unspecified"
         }
         return Offset(
-            unpackFloat1(packedValue) % operand,
-            unpackFloat2(packedValue) % operand,
+            packFloats(
+                unpackFloat1(packedValue) % operand,
+                unpackFloat2(packedValue) % operand
+            )
         )
     }
 
@@ -306,8 +313,10 @@
         "Offset is unspecified"
     }
     return Offset(
-        lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
-        lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+        packFloats(
+            lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
+            lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+        )
     )
 }
 
@@ -319,9 +328,11 @@
     checkPrecondition(packedValue != UnspecifiedPackedFloats) {
         "Offset is unspecified"
     }
-    val x = (packedValue shr 32) and FloatInfinityBase
-    val y = packedValue and FloatInfinityBase
-    return x != FloatInfinityBase && y != FloatInfinityBase
+    // Mask out the sign bit and do an equality check in each 32-bit lane
+    // against the "infinity base" mask (to check whether each packed float
+    // is infinite or not).
+    val v = (packedValue and DualUnsignedFloatMask) xor DualFloatInfinityBase
+    return (((v shr 1) or Uint64High32) - v) and Uint64High32 == 0L
 }
 
 /**
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
index 8bd6c27..df0c802 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
@@ -74,7 +74,7 @@
      * width or height parameter
      */
     fun copy(width: Float = unpackFloat1(packedValue), height: Float = unpackFloat2(packedValue)) =
-        Size(width, height)
+        Size(packFloats(width, height))
 
     companion object {
 
@@ -82,7 +82,7 @@
          * An empty size, one with a zero width and a zero height.
          */
         @Stable
-        val Zero = Size(0.0f, 0.0f)
+        val Zero = Size(0x0L)
 
         /**
          * A size whose [width] and [height] are unspecified. This is a sentinel
@@ -90,7 +90,7 @@
          * Access to width or height on an unspecified size is not allowed.
          */
         @Stable
-        val Unspecified = Size(Float.NaN, Float.NaN)
+        val Unspecified = Size(UnspecifiedPackedFloats)
     }
 
     /**
@@ -103,7 +103,17 @@
         if (packedValue == UnspecifiedPackedFloats) {
             throwIllegalStateException("Size is unspecified")
         }
-        return unpackFloat1(packedValue) <= 0.0f || unpackFloat2(packedValue) <= 0.0f
+        // Mask the sign bits, shift them to the right and replicate them by multiplying by -1.
+        // This will give us a mask of 0xffff_ffff for negative packed floats, and 0x0000_0000
+        // for positive packed floats. We invert the mask and do an and operation with the
+        // original value to set any negative float to 0.0f.
+        val v = packedValue and ((packedValue and DualFloatSignBit ushr 31) * -0x1).inv()
+        // At this point any negative float is set to 0, so the sign bit is  always 0.
+        // We take the 2 packed floats and "and" them together: if any of the two floats
+        // is 0.0f (either because the original value is 0.0f or because it was negative and
+        // we turned it into 0.0f with the line above), the result of the and operation will
+        // be 0 and we know our Size is empty.
+        return ((v ushr 32) and (v and 0xffffffffL)) == 0L
     }
 
     /**
@@ -119,8 +129,10 @@
             throwIllegalStateException("Size is unspecified")
         }
         return Size(
-            unpackFloat1(packedValue) * operand,
-            unpackFloat2(packedValue) * operand,
+            packFloats(
+                unpackFloat1(packedValue) * operand,
+                unpackFloat2(packedValue) * operand
+            )
         )
     }
 
@@ -137,8 +149,10 @@
             throwIllegalStateException("Size is unspecified")
         }
         return Size(
-            unpackFloat1(packedValue) / operand,
-            unpackFloat2(packedValue) / operand,
+            packFloats(
+                unpackFloat1(packedValue) / operand,
+                unpackFloat2(packedValue) / operand
+            )
         )
     }
 
@@ -221,8 +235,10 @@
         throwIllegalStateException("Offset is unspecified")
     }
     return Size(
-        lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
-        lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+        packFloats(
+            lerp(unpackFloat1(start.packedValue), unpackFloat1(stop.packedValue), fraction),
+            lerp(unpackFloat2(start.packedValue), unpackFloat2(stop.packedValue), fraction)
+        )
     )
 }
 
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index e374fff..4e5c0b0 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -571,9 +571,9 @@
     method public static void click(androidx.compose.ui.test.TouchInjectionScope, optional long position);
     method public static void doubleClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long delayMillis);
     method public static void longClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long durationMillis);
-    method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+    method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
     method public static void pinch(androidx.compose.ui.test.TouchInjectionScope, long start0, long end0, long start1, long end1, optional long durationMillis);
-    method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+    method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
     method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, long start, long end, optional long durationMillis);
     method public static void swipeDown(androidx.compose.ui.test.TouchInjectionScope, optional float startY, optional float endY, optional long durationMillis);
     method public static void swipeLeft(androidx.compose.ui.test.TouchInjectionScope, optional float startX, optional float endX, optional long durationMillis);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index fb5c367..fce6f17 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -572,9 +572,9 @@
     method public static void click(androidx.compose.ui.test.TouchInjectionScope, optional long position);
     method public static void doubleClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long delayMillis);
     method public static void longClick(androidx.compose.ui.test.TouchInjectionScope, optional long position, optional long durationMillis);
-    method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+    method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static void multiTouchSwipe(androidx.compose.ui.test.TouchInjectionScope, java.util.List<? extends kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset>> curves, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
     method public static void pinch(androidx.compose.ui.test.TouchInjectionScope, long start0, long end0, long start1, long end1, optional long durationMillis);
-    method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
+    method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, kotlin.jvm.functions.Function1<? super java.lang.Long,androidx.compose.ui.geometry.Offset> curve, optional long durationMillis, optional java.util.List<java.lang.Long> keyTimes);
     method public static void swipe(androidx.compose.ui.test.TouchInjectionScope, long start, long end, optional long durationMillis);
     method public static void swipeDown(androidx.compose.ui.test.TouchInjectionScope, optional float startY, optional float endY, optional long durationMillis);
     method public static void swipeLeft(androidx.compose.ui.test.TouchInjectionScope, optional float startX, optional float endX, optional long durationMillis);
diff --git a/compose/ui/ui-test/lint-baseline.xml b/compose/ui/ui-test/lint-baseline.xml
index 40df33c..199ff09 100644
--- a/compose/ui/ui-test/lint-baseline.xml
+++ b/compose/ui/ui-test/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
 
     <issue
         id="BanThreadSleep"
diff --git a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
index 6b948c8..29cd365 100644
--- a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
+++ b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TouchInjectionScopeSamples.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.test.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.assertHasClickAction
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.onNodeWithTag
@@ -76,3 +78,35 @@
             up()
         }
 }
+
+@Sampled
+fun touchInputMultiTouchWithHistory() {
+    // Move two fingers in a horizontal line, one on y=100 and one on y=500
+    composeTestRule.onNodeWithTag("myComponent")
+        .performTouchInput {
+            // First, make contact with the screen with both pointers:
+            down(0, Offset(300f, 100f))
+            down(1, Offset(300f, 500f))
+            // Update the pointer locations for the next event
+            updatePointerTo(0, Offset(400f, 100f))
+            updatePointerTo(1, Offset(400f, 500f))
+            // And send the move event with historical data
+            @OptIn(ExperimentalTestApi::class)
+            moveWithHistoryMultiPointer(
+                // Let's add 3 historical events
+                relativeHistoricalTimes = listOf(-12, -8, -4),
+                // Now, for each pointer we supply the historical coordinates
+                historicalCoordinates = listOf(
+                    // Pointer 0 moves along y=100
+                    listOf(Offset(325f, 100f), Offset(350f, 100f), Offset(375f, 100f)),
+                    // Pointer 1 moves along y=500
+                    listOf(Offset(325f, 500f), Offset(350f, 500f), Offset(375f, 500f)),
+                ),
+                // The actual move event will be sent 16ms after the previous event
+                delayMillis = 16
+            )
+            // And finish the gesture by lifting both fingers. Can be done in any order
+            up(1)
+            up(0)
+        }
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt
new file mode 100644
index 0000000..3b0a4b8
--- /dev/null
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/injectionscope/touch/SwipeMultiTouchTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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.ui.test.injectionscope.touch
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Press
+import androidx.compose.ui.input.pointer.PointerEventType.Companion.Release
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerType.Companion.Touch
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.multiTouchSwipe
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.runComposeUiTest
+import androidx.compose.ui.test.util.ClickableTestBox
+import androidx.compose.ui.test.util.SinglePointerInputRecorder
+import androidx.compose.ui.test.util.verify
+import androidx.compose.ui.test.util.verifyEvents
+import androidx.test.filters.MediumTest
+import org.junit.Test
+
+@MediumTest
+@OptIn(ExperimentalTestApi::class)
+class SwipeMultiTouchTest {
+    companion object {
+        private const val TAG = "widget"
+        // Duration is 4 * eventPeriod to get easily predictable results
+        private const val DURATION = 64L
+    }
+
+    private val recorder = SinglePointerInputRecorder()
+
+    @Test
+    fun test() = runComposeUiTest {
+        setContent {
+            Box(Modifier.fillMaxSize()) {
+                ClickableTestBox(modifier = recorder, tag = TAG)
+            }
+        }
+
+        // Move three fingers over the box from left to right simultaneously
+        // With a duration that is exactly 4 times the eventPeriod, each pointer will be sampled
+        // at t = 0, 16, 32, 48 and 64. That corresponds to x values of 10, 30, 50, 70 and 90.
+
+        val curve1 = line(fromX = 10f, toX = 90f, y = 20f, DURATION)
+        val curve2 = line(fromX = 10f, toX = 90f, y = 50f, DURATION)
+        val curve3 = line(fromX = 10f, toX = 90f, y = 80f, DURATION)
+
+        onNodeWithTag(TAG).performTouchInput {
+            multiTouchSwipe(
+                curves = listOf(curve1, curve2, curve3),
+                durationMillis = DURATION
+            )
+        }
+
+        val pointer1 = PointerId(0)
+        val pointer2 = PointerId(1)
+        val pointer3 = PointerId(2)
+
+        runOnIdle {
+            recorder.apply {
+                verifyEvents(
+                    // pointer1 down
+                    { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+                    // pointer2 down
+                    { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+                    { verify(0L, pointer2, true, Offset(10f, 50f), Touch, Press) },
+                    // pointer3 down
+                    { verify(0L, pointer1, true, Offset(10f, 20f), Touch, Press) },
+                    { verify(0L, pointer2, true, Offset(10f, 50f), Touch, Press) },
+                    { verify(0L, pointer3, true, Offset(10f, 80f), Touch, Press) },
+                    // first move
+                    { verify(16L, pointer1, true, Offset(30f, 20f), Touch, Move) },
+                    { verify(16L, pointer2, true, Offset(30f, 50f), Touch, Move) },
+                    { verify(16L, pointer3, true, Offset(30f, 80f), Touch, Move) },
+                    // second move
+                    { verify(32L, pointer1, true, Offset(50f, 20f), Touch, Move) },
+                    { verify(32L, pointer2, true, Offset(50f, 50f), Touch, Move) },
+                    { verify(32L, pointer3, true, Offset(50f, 80f), Touch, Move) },
+                    // third move
+                    { verify(48L, pointer1, true, Offset(70f, 20f), Touch, Move) },
+                    { verify(48L, pointer2, true, Offset(70f, 50f), Touch, Move) },
+                    { verify(48L, pointer3, true, Offset(70f, 80f), Touch, Move) },
+                    // last move
+                    { verify(64L, pointer1, true, Offset(90f, 20f), Touch, Move) },
+                    { verify(64L, pointer2, true, Offset(90f, 50f), Touch, Move) },
+                    { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Move) },
+                    // pointer1 up
+                    { verify(64L, pointer1, false, Offset(90f, 20f), Touch, Release) },
+                    { verify(64L, pointer2, true, Offset(90f, 50f), Touch, Release) },
+                    { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Release) },
+                    // pointer2 up
+                    { verify(64L, pointer2, false, Offset(90f, 50f), Touch, Release) },
+                    { verify(64L, pointer3, true, Offset(90f, 80f), Touch, Release) },
+                    // pointer3 up
+                    { verify(64L, pointer3, false, Offset(90f, 80f), Touch, Release) },
+                )
+            }
+        }
+    }
+
+    @Suppress("SameParameterValue")
+    private fun line(fromX: Float, toX: Float, y: Float, durationMillis: Long): (Long) -> Offset {
+        return { Offset(fromX + (toX - fromX) * it / durationMillis, y) }
+    }
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
index d814f96..5992018 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TouchInjectionScope.kt
@@ -234,15 +234,18 @@
 
     /**
      * Sends a move event [delayMillis] after the last sent event without updating any of the
-     * pointer positions.
+     * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
+     * to the move event. This corresponds to the scenario where a touch screen generates touch
+     * events quicker than can be dispatched and batches them together.
      *
-     * This overload supports gestures with multiple pointers.
+     * @sample androidx.compose.ui.test.samples.touchInputMultiTouchWithHistory
      *
      * @param relativeHistoricalTimes Time of each historical event, as a millisecond relative to
      * the time the actual event is sent. For example, -10L means 10ms earlier.
      * @param historicalCoordinates Coordinates of each historical event, in the same coordinate
      * space as [moveTo]. The outer list must have the same size as the number of pointers in the
-     * event, and each inner list must have the same size as [relativeHistoricalTimes].
+     * event, and each inner list must have the same size as [relativeHistoricalTimes]. The `i`th
+     * pointer is assigned the `i`th history, with the pointers sorted on ascending pointerId.
      * @param delayMillis The time between the last sent event and this event.
      * [eventPeriodMillis] by default.
      */
@@ -255,7 +258,9 @@
 
     /**
      * Sends a move event [delayMillis] after the last sent event without updating any of the
-     * pointer positions.
+     * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
+     * to the move event. This corresponds to the scenario where a touch screen generates touch
+     * events quicker than can be dispatched and batches them together.
      *
      * This overload is a convenience method for the common case where the gesture only has one
      * pointer.
@@ -464,7 +469,7 @@
  */
 fun TouchInjectionScope.swipe(
     curve: (Long) -> Offset,
-    durationMillis: Long,
+    durationMillis: Long = 200,
     keyTimes: List<Long> = emptyList()
 ) {
     @OptIn(ExperimentalTestApi::class)
@@ -491,7 +496,7 @@
 @ExperimentalTestApi
 fun TouchInjectionScope.multiTouchSwipe(
     curves: List<(Long) -> Offset>,
-    durationMillis: Long,
+    durationMillis: Long = 200,
     keyTimes: List<Long> = emptyList()
 ) {
     val startTime = 0L
diff --git a/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt b/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt
new file mode 100644
index 0000000..ff3ce54
--- /dev/null
+++ b/concurrent/concurrent-futures-ktx/src/main/resources/META-INF/NOTICE.txt
@@ -0,0 +1,220 @@
+List of 3rd party licenses:
+-----------------------------------------------------------------------------
+* kotlinx.coroutines library.
+
+ ****** NOTICE:
+
+=========================================================================
+==  NOTICE file corresponding to the section 4 d of                    ==
+==  the Apache License, Version 2.0,                                   ==
+==  in this case for the kotlinx.coroutines library.                   ==
+=========================================================================
+
+kotlinx.coroutines library.
+Copyright 2016-2024 JetBrains s.r.o and contributors
+
+ ****** LICENSE:
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
+
+   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.
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e34dcad..cd86e73 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -26,7 +26,7 @@
 autoValue = "1.6.3"
 binaryCompatibilityValidator = "0.15.0-Beta.2"
 byteBuddy = "1.14.9"
-asm = "9.3"
+asm = "9.7"
 cmake = "3.22.1"
 composeCompilerPlugin = "1.5.11"  # Update when pulling in new stable binaries
 dagger = "2.49"
diff --git a/input/input-motionprediction/api/1.0.0-beta04.txt b/input/input-motionprediction/api/1.0.0-beta04.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/1.0.0-beta04.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+  public interface MotionEventPredictor {
+    method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+    method public android.view.MotionEvent? predict();
+    method public void record(android.view.MotionEvent);
+  }
+
+}
+
diff --git a/input/input-motionprediction/api/res-1.0.0-beta04.txt b/input/input-motionprediction/api/res-1.0.0-beta04.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/input/input-motionprediction/api/res-1.0.0-beta04.txt
diff --git a/input/input-motionprediction/api/restricted_1.0.0-beta04.txt b/input/input-motionprediction/api/restricted_1.0.0-beta04.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/restricted_1.0.0-beta04.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+  public interface MotionEventPredictor {
+    method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+    method public android.view.MotionEvent? predict();
+    method public void record(android.view.MotionEvent);
+  }
+
+}
+
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index 65a95d3..d5a7f7d 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -176,7 +176,11 @@
 fun packageInspector(libraryProject: Project, inspectorProjectPath: String) {
     val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)
     if (inspectorProject == null) {
-        check(libraryProject.property("androidx.studio.type") == "playground") {
+        val extraProperties = libraryProject.extensions.extraProperties
+        check(
+            extraProperties.has("androidx.studio.type") &&
+            extraProperties.get("androidx.studio.type") == "playground"
+        ) {
             "Cannot find $inspectorProjectPath. This is optional only for playground builds."
         }
         // skip setting up inspector project
diff --git a/libraryversions.toml b/libraryversions.toml
index 722925b2..97e7a8d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -80,7 +80,7 @@
 HILT = "1.2.0-rc01"
 HILT_NAVIGATION = "1.2.0-rc01"
 HILT_NAVIGATION_COMPOSE = "1.2.0-rc01"
-INPUT_MOTIONPREDICTION = "1.0.0-beta03"
+INPUT_MOTIONPREDICTION = "1.0.0-beta04"
 INSPECTION = "1.0.0"
 INTERPOLATOR = "1.1.0-alpha01"
 JAVASCRIPTENGINE = "1.0.0-beta01"
diff --git a/lifecycle/lifecycle-compiler/src/main/resources/NOTICE.txt b/lifecycle/lifecycle-compiler/src/main/resources/META-INF/NOTICE.txt
similarity index 100%
rename from lifecycle/lifecycle-compiler/src/main/resources/NOTICE.txt
rename to lifecycle/lifecycle-compiler/src/main/resources/META-INF/NOTICE.txt
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index a8f7b18..b944fbb 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -131,6 +131,8 @@
             "findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
             "findProperty" to
                 Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+            "property" to
+                Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
             "iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
             "get" to Replacement(TASK_PROVIDER, null, EAGER_CONFIGURATION_ISSUE),
             "getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
diff --git a/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java b/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
index d3653f5..1a4f88e 100644
--- a/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
@@ -393,8 +393,9 @@
             bob.setIconBitmap(Api21Impl.getIconBitmap(description));
             bob.setIconUri(Api21Impl.getIconUri(description));
             Bundle extras = Api21Impl.getExtras(description);
+            extras = MediaSessionCompat.unparcelWithClassLoader(extras);
             if (extras != null) {
-                extras = new Bundle(MediaSessionCompat.unparcelWithClassLoader(extras));
+                extras = new Bundle(extras);
             }
             Uri mediaUri = null;
             if (extras != null) {
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 7a8b34a..777c33f 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -50,8 +50,6 @@
                 classpath = extraClasspath,
                 symbolProcessorProviders = symbolProcessorProviders,
                 processorOptions = processorOptions,
-                // b/328813158 to remove targeting of Java 17
-                javacArguments = listOf("-source", "17", "-target", "17")
             )
         )
     }
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 42a9e97..2bc4be2 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -81,7 +81,6 @@
     shadowed(libs.kotlinMetadataJvm) {
         exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
     }
-    implementation(libs.intellijAnnotations)
     implementation(libs.kspApi)
     implementation(libs.kotlinStdlibJdk8) // KSP defines older version as dependency, force update.
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
index bf9c0d6..7a80d08 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
@@ -26,6 +26,7 @@
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
 import com.google.devtools.ksp.symbol.Variance
 import com.squareup.kotlinpoet.ANY
 import com.squareup.kotlinpoet.KModifier
@@ -92,8 +93,16 @@
     val mutableBounds = mutableListOf(ANY.copy(nullable = true))
     val typeName = createModifiableTypeVariableName(name = name.asString(), bounds = mutableBounds)
     typeArgumentTypeLookup[name] = typeName
-    val resolvedBounds = bounds.map {
-        it.asKTypeName(resolver, typeArgumentTypeLookup)
+    val resolvedBounds = bounds.map { typeReference ->
+        typeReference.asKTypeName(resolver, typeArgumentTypeLookup).let { kTypeName ->
+            typeReference.resolve().let {
+                if (it.nullability == Nullability.PLATFORM) {
+                    kTypeName.copy(nullable = true)
+                } else {
+                    kTypeName
+                }
+            }
+        }
     }.toList()
     if (resolvedBounds.isNotEmpty()) {
         mutableBounds.addAll(resolvedBounds)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
index 5a9d520..748503b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodTypeVariableType.kt
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.XTypeVariableType
 import com.google.devtools.ksp.symbol.KSAnnotation
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.Nullability
 import com.squareup.javapoet.TypeName
 import kotlin.reflect.KClass
 
@@ -53,7 +54,16 @@
         )
     }
 
-    override val upperBounds: List<XType> = ksTypeVariable.bounds.map(env::wrap).toList()
+    override val upperBounds: List<XType> = ksTypeVariable.bounds.map {
+        val type = it.resolve().let {
+            if (it.nullability == Nullability.PLATFORM) {
+                it.withNullability(XNullability.NULLABLE)
+            } else {
+                it
+            }
+        }
+        env.wrap(it, type)
+    }.toList()
 
     override fun annotations(): Sequence<KSAnnotation> {
         return ksTypeVariable.annotations
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
index 64e518b..26fa62a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
@@ -18,10 +18,12 @@
 
 import androidx.room.compiler.processing.XAnnotated
 import androidx.room.compiler.processing.XMemberContainer
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeParameterElement
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_FIELD
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.Nullability
 import com.squareup.javapoet.TypeVariableName
 
 internal class KspTypeParameterElement(
@@ -43,7 +45,16 @@
     }
 
     override val bounds: List<XType> by lazy {
-        declaration.bounds.map { env.wrap(it, it.resolve()) }.toList().ifEmpty {
+        declaration.bounds.map {
+            val type = it.resolve().let {
+                if (it.nullability == Nullability.PLATFORM) {
+                    it.withNullability(XNullability.NULLABLE)
+                } else {
+                    it
+                }
+            }
+            env.wrap(it, type)
+        }.toList().ifEmpty {
             listOf(env.requireType(Any::class).makeNullable())
         }
     }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index e83bbbe..9a83501 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -244,24 +244,31 @@
 
             validateMethodElement(
                 element = it.processingEnv.requireTypeElement("foo.bar.Base"),
-                tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
-                rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+                tTypeName = XTypeName.getTypeVariableName("T", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true))),
+                rTypeName = XTypeName.getTypeVariableName("R", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true)))
             )
             validateMethodElement(
                 element = it.processingEnv.requireTypeElement("foo.bar.Child"),
-                tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
-                rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+                tTypeName = XTypeName.getTypeVariableName("T", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true))),
+                rTypeName = XTypeName.getTypeVariableName("R", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true)))
             )
 
             validateMethodTypeAsMemberOf(
                 element = it.processingEnv.requireTypeElement("foo.bar.Base"),
-                tTypeName = XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT)),
-                rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+                tTypeName = XTypeName.getTypeVariableName("T", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true))),
+                rTypeName = XTypeName.getTypeVariableName("R", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true)))
             )
             validateMethodTypeAsMemberOf(
                 element = it.processingEnv.requireTypeElement("foo.bar.Child"),
                 tTypeName = String::class.asClassName(),
-                rTypeName = XTypeName.getTypeVariableName("R", listOf(XTypeName.ANY_OBJECT))
+                rTypeName = XTypeName.getTypeVariableName("R", listOf(
+                    XTypeName.ANY_OBJECT.copy(nullable = true)))
             )
         }
     }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index 4d9dcea..8320561 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -22,8 +22,9 @@
 import androidx.room.compiler.processing.util.CONTINUATION_JCLASS_NAME
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.UNIT_JCLASS_NAME
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.getMethodByJvmName
-import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth
@@ -581,7 +582,8 @@
             """
             class KotlinSubject {
               fun <T> oneTypeVar(): Unit = TODO()
-              fun <T : MutableList<*>> oneBoundedTypeVar(): Unit = TODO()
+              fun <T : MutableList<*>?> oneBoundedTypeVar(): Unit = TODO()
+              fun <T : MutableList<*>> oneBoundedTypeVarNotNull(): Unit = TODO()
               fun <A, B> twoTypeVar(param: B): A = TODO()
             }
             """.trimIndent()
@@ -590,15 +592,18 @@
             "JavaSubject",
             """
             import java.util.List;
+            import org.jetbrains.annotations.NotNull;
             class JavaSubject {
               <T> void oneTypeVar() {}
               <T extends List<?>> void oneBoundedTypeVar() { }
+              <T extends @NotNull List<?>> void oneBoundedTypeVarNotNull() { }
               <A, B> A twoTypeVar(B param) { return null; }
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
-            listOf("KotlinSubject", "JavaSubject",).forEach { subjectFqn ->
+
+        fun handler(invocation: XTestInvocation) {
+            listOf("KotlinSubject", "JavaSubject").forEach { subjectFqn ->
                 val subject = invocation.processingEnv.requireTypeElement(subjectFqn)
                 subject.getMethodByJvmName("oneTypeVar").let {
                     val typeVar = it.executableType.typeVariables.single()
@@ -618,6 +623,29 @@
                                 bounds = listOf(
                                     List::class.asMutableClassName()
                                         .parametrizedBy(XTypeName.ANY_WILDCARD)
+                                        .copy(nullable = true)
+                                )
+                            )
+                        )
+                    assertThat(typeVar.superTypes.map { it.asTypeName() })
+                        .containsExactly(
+                            XTypeName.ANY_OBJECT.copy(nullable = true),
+                            List::class.asMutableClassName()
+                                .parametrizedBy(XTypeName.ANY_WILDCARD)
+                                .copy(nullable = true)
+                        )
+                    assertThat(typeVar.typeArguments).isEmpty()
+                    assertThat(typeVar.typeElement).isNull()
+                }
+                subject.getMethodByJvmName("oneBoundedTypeVarNotNull").let {
+                    val typeVar = it.executableType.typeVariables.single()
+                    assertThat(typeVar.asTypeName())
+                        .isEqualTo(
+                            XTypeName.getTypeVariableName(
+                                name = "T",
+                                bounds = listOf(
+                                    List::class.asMutableClassName()
+                                        .parametrizedBy(XTypeName.ANY_WILDCARD)
                                 )
                             )
                         )
@@ -646,5 +674,7 @@
                 }
             }
         }
+        runProcessorTest(sources = listOf(kotlinSrc, javaSrc), handler = ::handler)
+        runProcessorTest(classpath = compileFiles(listOf(kotlinSrc, javaSrc)), handler = ::handler)
     }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
index d14fea3..f3e289d 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeParameterElementTest.kt
@@ -182,20 +182,28 @@
             val bound0 = t.bounds[0]
             assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
             if (invocation.isKsp) {
-                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
             }
-            val bar = invocation.processingEnv.requireType("Bar")
+            val bar = invocation.processingEnv.requireType("Bar").makeNullable()
             assertThat(bound0.isSameType(bar)).isTrue()
-            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
 
             val bound1 = t.bounds[1]
             assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
             if (invocation.isKsp) {
-                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
             }
-            val baz = invocation.processingEnv.requireType("Baz")
+            val baz = invocation.processingEnv.requireType("Baz").makeNullable()
             assertThat(bound1.isSameType(baz)).isTrue()
-            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
         }
     }
 
@@ -333,20 +341,28 @@
             val bound0 = t.bounds[0]
             assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
             if (invocation.isKsp) {
-                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
             }
-            val bar = invocation.processingEnv.requireType("Bar")
+            val bar = invocation.processingEnv.requireType("Bar").makeNullable()
             assertThat(bound0.isSameType(bar)).isTrue()
-            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
 
             val bound1 = t.bounds[1]
             assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
             if (invocation.isKsp) {
-                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
             }
-            val baz = invocation.processingEnv.requireType("Baz")
+            val baz = invocation.processingEnv.requireType("Baz").makeNullable()
             assertThat(bound1.isSameType(baz)).isTrue()
-            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
         }
     }
 
@@ -381,20 +397,28 @@
             val bound0 = t.bounds[0]
             assertThat(bound0.asTypeName().java.toString()).isEqualTo("Bar")
             if (invocation.isKsp) {
-                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar")
+                assertThat(bound0.asTypeName().kotlin.toString()).isEqualTo("Bar?")
             }
-            val bar = invocation.processingEnv.requireType("Bar")
+            val bar = invocation.processingEnv.requireType("Bar").makeNullable()
             assertThat(bound0.isSameType(bar)).isTrue()
-            assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound0.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound0.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
 
             val bound1 = t.bounds[1]
             assertThat(bound1.asTypeName().java.toString()).isEqualTo("Baz")
             if (invocation.isKsp) {
-                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz")
+                assertThat(bound1.asTypeName().kotlin.toString()).isEqualTo("Baz?")
             }
-            val baz = invocation.processingEnv.requireType("Baz")
+            val baz = invocation.processingEnv.requireType("Baz").makeNullable()
             assertThat(bound1.isSameType(baz)).isTrue()
-            assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            if (invocation.isKsp) {
+                assertThat(bound1.nullability).isEqualTo(XNullability.NULLABLE)
+            } else {
+                assertThat(bound1.nullability).isEqualTo(XNullability.UNKNOWN)
+            }
 
             assertThat(constructor.parameters).hasSize(1)
             val parameter = constructor.parameters[0]
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 7b32220..30422ec 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -94,6 +94,7 @@
                             KTypeVariableName(
                                 "InputStreamType",
                                 KClassName("java.io", "InputStream")
+                                    .copy(nullable = true)
                             )
                         )
                 )
@@ -120,12 +121,14 @@
                     val expected = KTypeVariableName(
                         "InputStreamType",
                         KClassName("java.io", "InputStream")
+                            .copy(nullable = true)
                     )
                     assertThat(firstType.asTypeName().kotlin).isEqualTo(expected)
                     assertThat(
                         (firstType.asTypeName().kotlin as KTypeVariableName).bounds
                     ).containsExactly(
                         KClassName("java.io", "InputStream")
+                            .copy(nullable = true)
                     )
                 }
             }
@@ -622,8 +625,16 @@
             assertThat(typeElement.type.asTypeName().java.dumpToString(5))
                 .isEqualTo(expectedTypeStringDump)
             if (invocation.isKsp) {
+                val expectedTypeStringDumpKotlin = """
+                SelfReferencing<T>
+                | T
+                | > SelfReferencing<T>?
+                | > | T
+                | > | > SelfReferencing<T>?
+                | > | > | T
+                """.trimIndent()
                 assertThat(typeElement.type.asTypeName().kotlin.dumpToString(5))
-                    .isEqualTo(expectedTypeStringDump)
+                    .isEqualTo(expectedTypeStringDumpKotlin)
             }
             val expectedParamStringDump = """
                 SelfReferencing
diff --git a/room/room-compiler/src/main/resources/NOTICE.txt b/room/room-compiler/src/main/resources/META-INF/NOTICE.txt
similarity index 100%
rename from room/room-compiler/src/main/resources/NOTICE.txt
rename to room/room-compiler/src/main/resources/META-INF/NOTICE.txt
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index bbf992b..028b786 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -335,7 +335,8 @@
                 """
         ) { parsedQuery, invocation ->
             val expected = MUTABLE_LIST.parametrizedBy(
-                XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT))
+                XTypeName.getTypeVariableName("T", listOf(XTypeName.ANY_OBJECT.copy(
+                    nullable = true)))
             )
             assertThat(parsedQuery.returnType.asTypeName(), `is`(expected))
             invocation.assertCompilationResult {
diff --git a/settings.gradle b/settings.gradle
index 20624d7..8473061 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -405,6 +405,7 @@
 includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
 includeProject(":benchmark:integration-tests:startup-benchmark", [BuildType.MAIN])
+includeProject(":binarycompatibilityvalidator:binarycompatibilityvalidator", [BuildType.MAIN])
 includeProject(":biometric:biometric", [BuildType.MAIN])
 includeProject(":biometric:biometric-ktx", [BuildType.MAIN])
 includeProject(":biometric:biometric-ktx-samples", "biometric/biometric-ktx/samples", [BuildType.MAIN])
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
index e3cd485..72e6f56 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
@@ -69,6 +69,16 @@
  * LazyList and others. [ScalingLazyColumn] has a build-in rotary support, and accepts
  * [RotaryScrollableBehavior] directly as a parameter.
  *
+ * This modifier handles rotary input devices, used for scrolling. These devices can be categorized
+ * as high-resolution or low-resolution based on their precision.
+ *
+ *  - High-res devices: Offer finer control and can detect smaller rotations.
+ *    This allows for more precise adjustments during scrolling. One example of a high-res
+ *    device is the crown (also known as rotating side button), located on the side of the watch.
+ *  - Low-res devices: Have less granular control, registering larger rotations
+ *    at a time. Scrolling behavior is adapted to compensate for these larger jumps. Examples
+ *    include physical or virtual bezels, positioned around the screen.
+ *
  * This modifier supports rotary scrolling and snapping.
  * The behaviour is configured by the provided [RotaryScrollableBehavior]:
  * either provide [RotaryScrollableDefaults.behavior] for scrolling with/without fling
@@ -103,7 +113,8 @@
 /**
  * An interface for handling scroll events. Has implementations for handling scroll
  * with/without fling [FlingRotaryScrollableBehavior] and for handling snap
- * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior].
+ * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior] (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices).
  */
 interface RotaryScrollableBehavior {
 
@@ -321,7 +332,8 @@
  * Handles scroll with fling.
  *
  * @return A scroll with fling implementation of [RotaryScrollableBehavior] which is suitable
- * for both low-res and high-res inputs.
+ * for both low-res and high-res inputs (see [Modifier.rotaryScrollable] for descriptions
+ * of low-res and high-res devices).
  *
  * @param scrollableState Scrollable state which will be scrolled while receiving rotary events
  * @param flingBehavior Logic describing Fling behavior. If null - fling will not happen
@@ -360,7 +372,8 @@
  * Handles scroll with snap.
  *
  * @return A snap implementation of [RotaryScrollableBehavior] which is either suitable for low-res
- * or high-res input.
+ * or high-res input (see [Modifier.rotaryScrollable] for descriptions of low-res
+ * and high-res devices).
  *
  * @param layoutInfoProvider Implementation of [RotarySnapLayoutInfoProvider], which connects
  * scrollableState to a rotary input for snapping scroll actions.
@@ -419,8 +432,9 @@
 
 /**
  * An abstract base class for handling scroll events. Has implementations for handling scroll
- * with/without fling [FlingRotaryScrollableBehavior] and for handling snap [LowResSnapRotaryScrollableBehavior],
- * [HighResSnapRotaryScrollableBehavior].
+ * with/without fling [FlingRotaryScrollableBehavior] and for handling snap
+ * [LowResSnapRotaryScrollableBehavior], [HighResSnapRotaryScrollableBehavior] (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
  */
 internal abstract class BaseRotaryScrollableBehavior : RotaryScrollableBehavior {
 
@@ -797,7 +811,8 @@
  *
  * For a high-res input it has a filtering for events which are coming
  * with an opposite sign (this might happen to devices with rsb,
- * especially at the end of the scroll )
+ * especially at the end of the scroll ) - see [Modifier.rotaryScrollable] for descriptions
+ * of low-res and high-res devices.
  *
  * This scroll behavior supports fling. It can be set with [RotaryFlingHandler].
  */
@@ -873,7 +888,8 @@
 }
 
 /**
- * A scroll behavior for RSB(high-res) input with snapping and without fling.
+ * A scroll behavior for RSB(high-res) input with snapping and without fling (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
  *
  * Threshold for snapping is set dynamically in ThresholdBehavior, which depends
  * on the scroll speed and the average size of the items.
@@ -1030,7 +1046,8 @@
 }
 
 /**
- * A scroll behavior for Bezel(low-res) input with snapping and without fling
+ * A scroll behavior for Bezel(low-res) input with snapping and without fling (see
+ * [Modifier.rotaryScrollable] for descriptions of low-res and high-res devices ).
  *
  * This scroll behavior doesn't support fling.
  */
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index fa15794..0518e7a 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Button
 import androidx.wear.compose.material3.ButtonColors
 import androidx.wear.compose.material3.ButtonDefaults
@@ -51,7 +51,7 @@
 
 @Composable
 fun ButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -98,7 +98,7 @@
 
 @Composable
 fun FilledTonalButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -145,7 +145,7 @@
 
 @Composable
 fun OutlinedButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -192,7 +192,7 @@
 
 @Composable
 fun ChildButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -239,7 +239,7 @@
 
 @Composable
 fun CompactButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -341,7 +341,7 @@
 
 @Composable
 fun MultilineButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -384,7 +384,7 @@
 
 @Composable
 fun AvatarButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
index 19c409d..9180758 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.AppCard
 import androidx.wear.compose.material3.CardDefaults
 import androidx.wear.compose.material3.ListHeader
@@ -48,7 +48,7 @@
 
 @Composable
 fun CardDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index fcb7c22..8ecebb0 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.FilledIconButton
 import androidx.wear.compose.material3.FilledTonalIconButton
@@ -42,7 +42,7 @@
 
 @Composable
 fun IconButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
index 2216bdd..86ac15d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconToggleButtonDemo.kt
@@ -33,7 +33,7 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.IconButtonDefaults
 import androidx.wear.compose.material3.IconToggleButton
@@ -44,7 +44,7 @@
 
 @Composable
 fun IconToggleButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
index 7488d61..f5094c4 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
@@ -19,14 +19,14 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.samples.ListHeaderSample
 import androidx.wear.compose.material3.samples.ListSubheaderSample
 import androidx.wear.compose.material3.samples.ListSubheaderWithIconSample
 
 @Composable
 fun ListHeaderDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxWidth()
     ) {
         item {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
index fd546275..cee65ef 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
@@ -31,7 +31,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.RadioButton
@@ -40,7 +40,7 @@
 @Composable
 fun RadioButtonDemo() {
     var selectedRadioIndex by remember { mutableIntStateOf(0) }
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize().selectableGroup(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
index 0f8bf1b..5e47afb 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SelectionControlsDemo.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.RadioButton
 import androidx.wear.compose.material3.Text
@@ -34,7 +34,7 @@
 @Composable
 fun RadioDemos() {
     var selectedIndex by remember { mutableIntStateOf(0) }
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
index dc81baf..f0415ce 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SettingsDemo.kt
@@ -22,8 +22,8 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material3.Button
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.Icon
@@ -34,7 +34,7 @@
 fun SettingsDemo() {
     // TODO: Add Scaffold and TimeText when available
     val scalingLazyListState = rememberScalingLazyListState()
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         state = scalingLazyListState,
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
index 1597b69..feab507 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
@@ -34,10 +34,10 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.integration.demos.common.Centralize
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.InlineSlider
@@ -90,7 +90,7 @@
     var enabledValue by remember { mutableFloatStateOf(5f) }
     var disabledValue by remember { mutableFloatStateOf(5f) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
@@ -134,7 +134,7 @@
     var valueWithoutSegments by remember { mutableIntStateOf(5) }
     var valueWithSegments by remember { mutableIntStateOf(5) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
index b34aba8..8327371 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.SplitRadioButton
 import androidx.wear.compose.material3.Text
@@ -37,7 +37,7 @@
 @Composable
 fun SplitRadioButtonDemo() {
     var selectedRadioIndex by remember { mutableIntStateOf(0) }
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
index 108386c..62e3461 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitToggleButtonDemo.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Checkbox
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.SplitToggleButton
@@ -38,7 +38,7 @@
 
 @Composable
 fun SplitToggleButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
index c6e0246..807cd0f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.MaterialTheme
@@ -42,7 +42,7 @@
 
 @Composable
 fun TextButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
index 76d66b5..955f9a8 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TextButtonDefaults
@@ -41,7 +41,7 @@
 
 @Composable
 fun TextToggleButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
index fc7d844..fc3460f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleButtonDemo.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Checkbox
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.ListHeader
@@ -40,7 +40,7 @@
 
 @Composable
 fun ToggleButtonDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
index 3dcdcc6..f075f0f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ToggleControlsDemo.kt
@@ -28,7 +28,7 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Checkbox
 import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.Switch
@@ -37,7 +37,7 @@
 
 @Composable
 fun CheckboxDemos() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
@@ -63,7 +63,7 @@
 
 @Composable
 fun SwitchDemos() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
diff --git a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt
deleted file mode 100644
index f53977e..0000000
--- a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Rotary.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2023 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.wear.compose.integration.demos.common
-
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.rotary.onRotaryScrollEvent
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.lazy.AutoCenteringParams
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
-import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
-import androidx.wear.compose.foundation.lazy.ScalingParams
-import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.receiveAsFlow
-
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalWearFoundationApi::class)
-@Composable
-fun ScalingLazyColumnWithRSB(
-    modifier: Modifier = Modifier,
-    state: ScalingLazyListState = rememberScalingLazyListState(),
-    contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
-    scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
-    reverseLayout: Boolean = false,
-    snap: Boolean = true,
-    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
-    verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(
-        space = 4.dp,
-        alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom
-    ),
-    autoCentering: AutoCenteringParams? = AutoCenteringParams(),
-    content: ScalingLazyListScope.() -> Unit
-) {
-    val flingBehavior = if (snap) ScalingLazyColumnDefaults.snapFlingBehavior(
-        state = state
-    ) else ScrollableDefaults.flingBehavior()
-    val focusRequester = rememberActiveFocusRequester()
-    ScalingLazyColumn(
-        modifier = modifier.rsbScroll(
-            scrollableState = state,
-            flingBehavior = flingBehavior,
-            focusRequester = focusRequester
-        ),
-        state = state,
-        contentPadding = contentPadding,
-        reverseLayout = reverseLayout,
-        scalingParams = scalingParams,
-        flingBehavior = flingBehavior,
-        horizontalAlignment = horizontalAlignment,
-        verticalArrangement = verticalArrangement,
-        autoCentering = autoCentering,
-        content = content
-    )
-}
-
-@Suppress("ComposableModifierFactory")
-@Composable
-@Deprecated("Use .rotary modifier instead")
-fun Modifier.rsbScroll(
-    scrollableState: ScrollableState,
-    flingBehavior: FlingBehavior,
-    focusRequester: FocusRequester? = null
-): Modifier {
-    val channel = remember {
-        Channel<TimestampedDelta>(
-            capacity = 10,
-            onBufferOverflow = BufferOverflow.DROP_OLDEST
-        )
-    }
-
-    var lastTimeMillis = remember { 0L }
-    var smoothSpeed = remember { 0f }
-    val speedWindowMillis = 200L
-    val timeoutToFling = 100L
-
-    return composed {
-        var rsbScrollInProgress by remember { mutableStateOf(false) }
-        LaunchedEffect(rsbScrollInProgress) {
-            if (rsbScrollInProgress) {
-                scrollableState.scroll(MutatePriority.UserInput) {
-                    channel.receiveAsFlow().collectLatest {
-                        val toScroll = if (lastTimeMillis > 0L && it.time > lastTimeMillis) {
-                            val timeSinceLastEventMillis = it.time - lastTimeMillis
-
-                            // Speed is in pixels per second.
-                            val speed = it.delta * 1000 / timeSinceLastEventMillis
-                            val cappedElapsedTimeMillis =
-                                timeSinceLastEventMillis.coerceAtMost(speedWindowMillis)
-                            smoothSpeed = ((speedWindowMillis - cappedElapsedTimeMillis) * speed +
-                                cappedElapsedTimeMillis * smoothSpeed) / speedWindowMillis
-                            smoothSpeed * cappedElapsedTimeMillis / 1000
-                        } else {
-                            0f
-                        }
-                        lastTimeMillis = it.time
-                        scrollBy(toScroll)
-
-                        // If more than the given time pass, start a fling.
-                        delay(timeoutToFling)
-
-                        lastTimeMillis = 0L
-
-                        if (smoothSpeed != 0f) {
-                            val launchSpeed = smoothSpeed
-                            smoothSpeed = 0f
-                            with(flingBehavior) {
-                                performFling(launchSpeed)
-                            }
-                            rsbScrollInProgress = false
-                        }
-                    }
-                }
-            }
-        }
-        this
-            .onRotaryScrollEvent {
-                channel.trySend(TimestampedDelta(it.uptimeMillis, it.verticalScrollPixels))
-                rsbScrollInProgress = true
-                true
-            }
-            .let {
-                if (focusRequester != null) {
-                    it
-                        .focusRequester(focusRequester)
-                        .focusable()
-                } else it
-            }
-    }
-}
-
-internal data class TimestampedDelta(val time: Long, val delta: Float)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
index 93d971b..22fa29d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ButtonDemo.kt
@@ -35,8 +35,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.ButtonDefaults
 import androidx.wear.compose.material.CompactButton
@@ -115,7 +115,7 @@
     var enabled by remember { mutableStateOf(true) }
     val context = LocalContext.current
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         modifier = Modifier.fillMaxSize(),
         horizontalAlignment = Alignment.CenterHorizontally
     ) {
@@ -315,7 +315,7 @@
 @Composable
 fun ButtonGallery() {
     val state = rememberScalingLazyListState()
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         state = state,
         horizontalAlignment = Alignment.CenterHorizontally,
         modifier = Modifier.fillMaxSize(),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
index b20cf49..fc7b485 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
@@ -32,7 +32,7 @@
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.Card
 import androidx.wear.compose.material.CardDefaults
@@ -42,7 +42,7 @@
 
 @Composable
 fun CardDemo() {
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
index 0c06756..0c549bd 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
@@ -49,7 +49,6 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipColors
 import androidx.wear.compose.material.ChipDefaults
@@ -70,7 +69,7 @@
     var enabled by remember { mutableStateOf(true) }
     var chipStyle by remember { mutableStateOf(ChipStyle.Primary) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
     ) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 8929f17..c187929 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -30,6 +30,7 @@
 import androidx.wear.compose.foundation.SwipeToDismissKeys
 import androidx.wear.compose.foundation.SwipeToDismissValue
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
@@ -38,7 +39,6 @@
 import androidx.wear.compose.integration.demos.common.Demo
 import androidx.wear.compose.integration.demos.common.DemoCategory
 import androidx.wear.compose.integration.demos.common.DemoParameters
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
@@ -122,13 +122,12 @@
 ) {
     val state = rememberScalingLazyListState()
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         modifier = Modifier
             .fillMaxWidth()
             .testTag(DemoListTag),
         state = scrollStates[scrollStateIndex],
-        snap = false,
         autoCentering = AutoCenteringParams(itemIndex = if (category.demos.size >= 2) 2 else 1),
     ) {
         item {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index be9889d..b68787e 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -27,7 +27,6 @@
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -67,12 +66,10 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.wear.compose.integration.demos.common.rsbScroll
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Icon
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Picker
-import androidx.wear.compose.material.PickerDefaults
 import androidx.wear.compose.material.PickerGroup
 import androidx.wear.compose.material.PickerGroupItem
 import androidx.wear.compose.material.PickerGroupState
@@ -194,7 +191,7 @@
                     horizontalArrangement = Arrangement.Center,
                 ) {
                     PickerGroup(
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = hourState,
                             modifier = Modifier
                                 .size(40.dp, 100.dp),
@@ -207,7 +204,7 @@
                             contentDescription = hourContentDescription,
                             option = pickerOption
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = minuteState,
                             modifier = Modifier
                                 .size(40.dp, 100.dp),
@@ -220,7 +217,7 @@
                             contentDescription = minuteContentDescription,
                             option = pickerOption
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = secondState,
                             modifier = Modifier
                                 .size(40.dp, 100.dp),
@@ -400,7 +397,7 @@
                         }
                     Spacer(Modifier.width(16.dp))
                     PickerGroup(
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = hourState,
                             modifier = Modifier.size(48.dp, 100.dp),
                             onSelected = {
@@ -412,7 +409,7 @@
                             contentDescription = hoursContentDescription,
                             option = pickerTextOption(textStyle) { "%02d".format(it + 1) }
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = minuteState,
                             modifier = Modifier.size(48.dp, 100.dp),
                             onSelected = {
@@ -424,7 +421,7 @@
                             contentDescription = minutesContentDescription,
                             option = pickerTextOption(textStyle) { "%02d".format(it) }
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = periodState,
                             modifier = Modifier.size(64.dp, 100.dp),
                             contentDescription = periodContentDescription,
@@ -666,7 +663,7 @@
                     horizontalArrangement = Arrangement.Center
                 ) {
                     PickerGroup(
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = datePickerState.dayState,
                             modifier = Modifier.size(dayWidth, 100.dp),
                             contentDescription = dayContentDescription,
@@ -680,7 +677,7 @@
                                 "%d".format(datePickerState.currentDay(it))
                             }
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = datePickerState.monthState,
                             modifier = Modifier.size(monthWidth, 100.dp),
                             onSelected = {
@@ -694,7 +691,7 @@
                                 shortMonthNames[(datePickerState.currentMonth(it) - 1) % 12]
                             }
                         ),
-                        pickerGroupItemWithRSB(
+                        PickerGroupItem(
                             pickerState = datePickerState.yearState,
                             modifier = Modifier.size(yearWidth, 100.dp),
                             onSelected = {
@@ -817,27 +814,6 @@
     Spacer(Modifier.width(width))
 }
 
-@Suppress("DEPRECATION")
-@Composable
-fun pickerGroupItemWithRSB(
-    pickerState: PickerState,
-    modifier: Modifier,
-    contentDescription: String?,
-    onSelected: () -> Unit,
-    readOnlyLabel: @Composable (BoxScope.() -> Unit)? = null,
-    option: @Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit
-) = PickerGroupItem(
-    pickerState = pickerState,
-    modifier = modifier.rsbScroll(
-        scrollableState = pickerState,
-        flingBehavior = PickerDefaults.flingBehavior(pickerState)
-    ),
-    contentDescription = contentDescription,
-    onSelected = onSelected,
-    readOnlyLabel = readOnlyLabel,
-    option = option
-)
-
 @Composable
 fun PickerWithoutGradient() {
     val items = listOf("One", "Two", "Three", "Four", "Five")
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
index 4aa990c..a736de1 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
@@ -41,8 +41,8 @@
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.integration.demos.common.Centralize
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipColors
@@ -62,7 +62,7 @@
 fun PlaceholderChips() {
     var resetCount by remember { mutableIntStateOf(0) }
     Box {
-        ScalingLazyColumnWithRSB {
+        ScalingLazyColumn {
             item {
                 ListHeader {
                     Text(text = "Primary Label Center Aligned", textAlign = TextAlign.Center)
@@ -313,7 +313,7 @@
         }
     }
 
-    ScalingLazyColumnWithRSB {
+    ScalingLazyColumn {
         item {
             ListHeader {
                 Text("Overlaid Placeholders", textAlign = TextAlign.Center)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
index 17a6fa6..628206c 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ProgressIndicatorDemo.kt
@@ -32,7 +32,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.ButtonDefaults
 import androidx.wear.compose.material.CircularProgressIndicator
@@ -58,7 +58,7 @@
     val animatedProgress: Float by animateFloatAsState(targetValue = progress)
 
     Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-        ScalingLazyColumnWithRSB(
+        ScalingLazyColumn(
             modifier = Modifier
                 .fillMaxSize()
                 .padding(horizontal = 8.dp),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 60e8213..7bb195c 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -36,10 +36,11 @@
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
 import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
-import androidx.wear.compose.integration.demos.common.rsbScroll
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
 import androidx.wear.compose.material.Card
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
@@ -154,9 +155,11 @@
         Column(
             modifier = Modifier
                 .verticalScroll(scrollState)
-                .rsbScroll(
-                    scrollableState = scrollState,
-                    flingBehavior = ScrollableDefaults.flingBehavior(),
+                .rotaryScrollable(
+                    RotaryScrollableDefaults.behavior(
+                        scrollableState = scrollState,
+                        flingBehavior = ScrollableDefaults.flingBehavior()
+                    ),
                     focusRequester = focusRequester
                 )
         ) {
@@ -190,9 +193,11 @@
     ) {
         LazyColumn(
             state = scrollState,
-            modifier = Modifier.rsbScroll(
-                scrollableState = scrollState,
-                flingBehavior = ScrollableDefaults.flingBehavior(),
+            modifier = Modifier.rotaryScrollable(
+                RotaryScrollableDefaults.behavior(
+                    scrollableState = scrollState,
+                    flingBehavior = ScrollableDefaults.flingBehavior()
+                ),
                 focusRequester = focusRequester
             )
         ) {
@@ -230,7 +235,7 @@
             PositionIndicator(scalingLazyListState = scrollState)
         }
     ) {
-        ScalingLazyColumnWithRSB(
+        ScalingLazyColumn(
             contentPadding = PaddingValues(10.dp),
             state = scrollState,
             autoCentering = AutoCenteringParams(itemIndex = 1, itemOffset = 0)
@@ -272,7 +277,7 @@
             PositionIndicator(scalingLazyListState = scrollState)
         }
     ) {
-        ScalingLazyColumnWithRSB(
+        ScalingLazyColumn(
             contentPadding = PaddingValues(10.dp),
             state = scrollState,
         ) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
index c187ef4..7137f26 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SelectableChipDemo.kt
@@ -32,9 +32,9 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.RadioButton
@@ -60,7 +60,7 @@
     var radioIconWithSecondarySelected by remember { mutableStateOf(true) }
     var splitWithRadioIconSelected by remember { mutableStateOf(true) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         state = scrollState,
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
index 5f80379..cb68ec5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SettingsDemo.kt
@@ -22,8 +22,8 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.painterResource
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.Icon
@@ -41,7 +41,7 @@
             TimeText(modifier = Modifier.scrollAway(scalingLazyListState))
         }
     ) {
-        ScalingLazyColumnWithRSB(
+        ScalingLazyColumn(
             state = scalingLazyListState,
             modifier = Modifier.fillMaxSize(),
             horizontalAlignment = Alignment.CenterHorizontally
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
index a1d22eb..585c7d1 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SliderDemo.kt
@@ -34,7 +34,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.Icon
 import androidx.wear.compose.material.InlineSlider
 import androidx.wear.compose.material.InlineSliderColors
@@ -49,7 +49,7 @@
     var valueWithSegments by remember { mutableFloatStateOf(2f) }
     var enabled by remember { mutableStateOf(true) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
@@ -105,7 +105,7 @@
     var valueWithoutSegments by remember { mutableIntStateOf(5) }
     var valueWithSegments by remember { mutableIntStateOf(2) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
@@ -167,7 +167,7 @@
     var numberOfSegments by remember { mutableFloatStateOf(5f) }
     var progress by remember { mutableFloatStateOf(10f) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(
             space = 4.dp,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
index 7807eaf..0306e38 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ThemeDemo.kt
@@ -29,13 +29,13 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Text
 
 @Composable
 fun ThemeFonts() {
-    ScalingLazyColumnWithRSB {
+    ScalingLazyColumn {
         item {
             ThemeFontRow(style = MaterialTheme.typography.display1, description = "display1")
         }
@@ -88,7 +88,7 @@
 
 @Composable
 fun ThemeColors() {
-    ScalingLazyColumnWithRSB {
+    ScalingLazyColumn {
         item {
             ThemeColorRow(
                 backgroundColor = MaterialTheme.colors.background,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
index e932b6a..bcebbf3 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
@@ -32,9 +32,9 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.integration.demos.common.ScalingLazyColumnWithRSB
 import androidx.wear.compose.material.Checkbox
 import androidx.wear.compose.material.CheckboxDefaults
 import androidx.wear.compose.material.ListHeader
@@ -66,7 +66,7 @@
     var switchIconWithIconChecked by remember { mutableStateOf(true) }
     var splitWithCustomColorChecked by remember { mutableStateOf(true) }
 
-    ScalingLazyColumnWithRSB(
+    ScalingLazyColumn(
         state = scrollState,
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
index d92eaa5..9693204 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/Fingerprint.java
@@ -40,10 +40,34 @@
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
 public final class Fingerprint {
+    private static final int[] POW_31 = {
+        1,
+        31,
+        961,
+        29791,
+        923521,
+        28629151,
+        887503681,
+        1742810335,
+        -1807454463,
+        -196513505,
+        -1796951359,
+        129082719,
+        -293403007,
+        -505558625,
+        1507551809,
+        -510534177,
+        1353309697,
+        -997072353,
+        -844471871,
+        -408824225
+    };
     private static final int DEFAULT_VALUE = 0;
     private final int selfTypeValue;
     private int selfPropsValue;
     private int childNodesValue;
+    // itself + children + grand children + grand grand children + ...
+    private int subNodeCount;
     private @Nullable List<Fingerprint> childNodes;
 
     public Fingerprint(int selfTypeValue) {
@@ -51,6 +75,7 @@
         this.selfPropsValue = DEFAULT_VALUE;
         this.childNodesValue = DEFAULT_VALUE;
         this.childNodes = null;
+        this.subNodeCount = 1; // self
     }
 
     public Fingerprint(@NonNull NodeFingerprint proto) {
@@ -102,7 +127,12 @@
             childNodes = new ArrayList<>();
         }
         childNodes.add(childNode);
-        childNodesValue = (31 * childNodesValue) + childNode.aggregateValueAsInt();
+        // We need to include the number of grandchildren of the new node, otherwise swapping the
+        // place of "b" and "c" in a layout like "[a b] [c d]" could result in the same fingerprint.
+        int coeff = pow31Unsafe(childNode.subNodeCount);
+        childNodesValue =
+                (coeff * childNodesValue) + (childNodes.size() * childNode.aggregateValueAsInt());
+        subNodeCount += childNode.subNodeCount;
     }
 
     /** Record a property value being updated. */
@@ -115,6 +145,25 @@
         selfPropsValue = (31 * selfPropsValue) + entry;
     }
 
+    /**
+     * An int version of {@link Math#pow(double, double)} }. The result will overflow if it's larger
+     * than {@link Integer#MAX_VALUE}.
+     *
+     * <p>Note that this only support positive exponents
+     */
+    private static int pow31Unsafe(int n) {
+        // Check for available precomputed result (as an optimization).
+        if (n < POW_31.length) {
+            return POW_31[n];
+        }
+
+        int result = POW_31[POW_31.length - 1];
+        for (int i = POW_31.length; i <= n; i++) {
+            result *= 31;
+        }
+        return result;
+    }
+
     NodeFingerprint toProto() {
         NodeFingerprint.Builder builder = NodeFingerprint.newBuilder();
         if (selfTypeValue() != 0) {
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
index 2f99ad8..a6eda1c 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
@@ -28,9 +28,13 @@
 public final class FingerprintTest {
     private static final int SELF_TYPE_VALUE = 1234;
     private static final int FIELD_1 = 1;
-    private static final int VALUE_HASH1 = 10;
-
-    private static final int DISCARDED_VALUE = -1;
+    private static final int VALUE_HASH1 = 101;
+    private static final int FIELD_2 = 2;
+    private static final int VALUE_HASH2 = 202;
+    private static final int FIELD_3 = 3;
+    private static final int VALUE_HASH3 = 301;
+    private static final int FIELD_4 = 4;
+    private static final int VALUE_HASH4 = 401;
 
     @Test
     public void addChildNode() {
@@ -59,4 +63,33 @@
         assertThat(child.selfTypeValue()).isEqualTo(SELF_TYPE_VALUE);
         assertThat(child.selfPropsValue()).isEqualTo(31 * FIELD_1 + VALUE_HASH1);
     }
+
+    @Test
+    public void childNodeOrderMatters() {
+        Fingerprint root1 = new Fingerprint(SELF_TYPE_VALUE);
+        Fingerprint root2 = new Fingerprint(SELF_TYPE_VALUE);
+        Fingerprint parent12 = createParentFor(FIELD_1, VALUE_HASH1, FIELD_2, VALUE_HASH2);
+        Fingerprint parent34 = createParentFor(FIELD_3, VALUE_HASH3, FIELD_4, VALUE_HASH4);
+        Fingerprint parent13 = createParentFor(FIELD_1, VALUE_HASH1, FIELD_3, VALUE_HASH3);
+        Fingerprint parent24 = createParentFor(FIELD_2, VALUE_HASH2, FIELD_4, VALUE_HASH4);
+
+        root1.addChildNode(parent12);
+        root1.addChildNode(parent34);
+        root2.addChildNode(parent13);
+        root2.addChildNode(parent24);
+
+        assertThat(root1.childNodesValue()).isNotEqualTo(root2.childNodesValue());
+    }
+
+    private Fingerprint createParentFor(
+            int fieldId1, int fieldValue1, int fieldId2, int fieldValue2) {
+        Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+        Fingerprint child1FingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+        child1FingerPrint.recordPropertyUpdate(fieldId1, fieldValue1);
+        parentFingerPrint.addChildNode(child1FingerPrint);
+        Fingerprint child2FingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+        child2FingerPrint.recordPropertyUpdate(fieldId2, fieldValue2);
+        parentFingerPrint.addChildNode(child2FingerPrint);
+        return parentFingerPrint;
+    }
 }
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
index f0a0f04..0b7b008 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
@@ -634,7 +634,7 @@
   string zone_id = 2;
 }
 
-// The datetime part to retrieve using ZonedDateTimePartOp.
+// The date-time part to retrieve using ZonedDateTimePartOp.
 enum ZonedDateTimePartType {
   // Undefined date-time part type.
   ZONED_DATE_TIME_PART_UNDEFINED = 0;
@@ -654,12 +654,12 @@
   ZONED_DATE_TIME_PART_YEAR = 7;
 }
 
-// Retrieve the specified datetime part of a DynamicZonedDateTime instance as a
+// Retrieve the specified date-time part of a DynamicZonedDateTime instance as a
 // DynamicInt32.
 message GetZonedDateTimePartOp {
-  // The zoned datetime input.
+  // The zoned date-time input.
   DynamicZonedDateTime input = 1;
-  // The datetime part to retrieve.
+  // The date-time part to retrieve.
   ZonedDateTimePartType part_type = 2;
 }
 
@@ -755,7 +755,6 @@
   DurationPartType duration_part = 2;
 }
 
-
 // A dynamic Instant which sources its data from the a state entry.
 message StateInstantSource {
   // The key in the state to bind to.
@@ -772,4 +771,4 @@
 
   // The namespace for the state key.
   string source_namespace = 2;
-}
\ No newline at end of file
+}